]> granicus.if.org Git - apache/blob - support/htpasswd.c
Optionally read passwords from stdin
[apache] / support / htpasswd.c
1 /* Licensed to the Apache Software Foundation (ASF) under one or more
2  * contributor license agreements.  See the NOTICE file distributed with
3  * this work for additional information regarding copyright ownership.
4  * The ASF licenses this file to You under the Apache License, Version 2.0
5  * (the "License"); you may not use this file except in compliance with
6  * the License.  You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 /******************************************************************************
18  ******************************************************************************
19  * NOTE! This program is not safe as a setuid executable!  Do not make it
20  * setuid!
21  ******************************************************************************
22  *****************************************************************************/
23 /*
24  * htpasswd.c: simple program for manipulating password file for
25  * the Apache HTTP server
26  *
27  * Originally by Rob McCool
28  *
29  * Exit values:
30  *  0: Success
31  *  1: Failure; file access/permission problem
32  *  2: Failure; command line syntax problem (usage message issued)
33  *  3: Failure; password verification failure
34  *  4: Failure; operation interrupted (such as with CTRL/C)
35  *  5: Failure; buffer would overflow (username, filename, or computed
36  *     record too long)
37  *  6: Failure; username contains illegal or reserved characters
38  *  7: Failure; file is not a valid htpasswd file
39  */
40
41 #include "passwd_common.h"
42 #include "apr_signal.h"
43 #include "apr_getopt.h"
44
45 #if APR_HAVE_STDIO_H
46 #include <stdio.h>
47 #endif
48
49 #include "apr_md5.h"
50 #include "apr_sha1.h"
51
52 #if APR_HAVE_STDLIB_H
53 #include <stdlib.h>
54 #endif
55 #if APR_HAVE_STRING_H
56 #include <string.h>
57 #endif
58 #if APR_HAVE_UNISTD_H
59 #include <unistd.h>
60 #endif
61
62 #ifdef WIN32
63 #include <conio.h>
64 #define unlink _unlink
65 #endif
66
67 #define APHTP_NEWFILE        1
68 #define APHTP_NOFILE         2
69 #define APHTP_DELUSER        4
70
71 apr_file_t *ftemp = NULL;
72
73 static int mkrecord(struct passwd_ctx *ctx, char *user)
74 {
75     char hash_str[MAX_STRING_LEN];
76     int ret;
77     ctx->out = hash_str;
78     ctx->out_len = sizeof(hash_str);
79
80     ret = mkhash(ctx);
81     if (ret)
82         return ret;
83
84     ctx->out = apr_pstrcat(ctx->pool, user, ":", hash_str, NL, NULL);
85     if (strlen(ctx->out) >= MAX_STRING_LEN) {
86         ctx->errstr = "resultant record too long";
87         return ERR_OVERFLOW;
88     }
89     return 0;
90 }
91
92 static void usage(void)
93 {
94     apr_file_printf(errfile, "Usage:" NL
95         "\thtpasswd [-cimBdpsD] [-C cost] passwordfile username" NL
96         "\thtpasswd -b[cmBdpsD] [-C cost] passwordfile username password" NL
97         NL
98         "\thtpasswd -n[imBdps] [-C cost] username" NL
99         "\thtpasswd -nb[mBdps] [-C cost] username password" NL
100         " -c  Create a new file." NL
101         " -n  Don't update file; display results on stdout." NL
102         " -i  Read password from stdin without verification (for script usage)" NL
103         " -m  Force MD5 encryption of the password (default)." NL
104         " -B  Force bcrypt encryption of the password (very secure)." NL
105         " -C  Set the computing time used for the bcrypt algorithm" NL
106         "     (higher is more secure but slower, default: %d, valid: 4 to 31)" NL
107         " -d  Force CRYPT encryption of the password (8 chars max, "
108             "insecure)." NL
109         " -p  Do not encrypt the password (plaintext, insecure)." NL
110         " -s  Force SHA encryption of the password (insecure)." NL
111         " -b  Use the password from the command line rather than prompting "
112             "for it." NL
113         " -D  Delete the specified user." NL
114         "On other systems than Windows and NetWare the '-p' flag will "
115             "probably not work." NL
116         "The SHA algorithm does not use a salt and is less secure than the "
117             "MD5 algorithm." NL,
118         BCRYPT_DEFAULT_COST
119     );
120     exit(ERR_SYNTAX);
121 }
122
123 /*
124  * Check to see if the specified file can be opened for the given
125  * access.
126  */
127 static int accessible(apr_pool_t *pool, char *fname, int mode)
128 {
129     apr_file_t *f = NULL;
130
131     if (apr_file_open(&f, fname, mode, APR_OS_DEFAULT, pool) != APR_SUCCESS) {
132         return 0;
133     }
134     apr_file_close(f);
135     return 1;
136 }
137
138 /*
139  * Return true if the named file exists, regardless of permissions.
140  */
141 static int exists(char *fname, apr_pool_t *pool)
142 {
143     apr_finfo_t sbuf;
144     apr_status_t check;
145
146     check = apr_stat(&sbuf, fname, APR_FINFO_TYPE, pool);
147     return ((check || sbuf.filetype != APR_REG) ? 0 : 1);
148 }
149
150 static void terminate(void)
151 {
152     apr_terminate();
153 #ifdef NETWARE
154     pressanykey();
155 #endif
156 }
157
158 static void check_args(int argc, const char *const argv[],
159                        struct passwd_ctx *ctx, int *mask, char **user,
160                        char **pwfilename)
161 {
162     const char *arg;
163     int args_left = 2;
164     int i, ret;
165     apr_getopt_t *state;
166     apr_status_t rv;
167     char opt;
168     const char *opt_arg;
169     apr_pool_t *pool = ctx->pool;
170
171     rv = apr_getopt_init(&state, pool, argc, argv);
172     if (rv != APR_SUCCESS)
173         exit(ERR_SYNTAX);
174
175     while ((rv = apr_getopt(state, "cnmspdBbDiC:", &opt, &opt_arg)) == APR_SUCCESS) {
176         switch (opt) {
177         case 'c':
178             *mask |= APHTP_NEWFILE;
179             break;
180         case 'n':
181             args_left--;
182             *mask |= APHTP_NOFILE;
183             break;
184         case 'D':
185             *mask |= APHTP_DELUSER;
186             break;
187         default:
188             ret = parse_common_options(ctx, opt, opt_arg);
189             if (ret) {
190                 apr_file_printf(errfile, "%s: %s" NL, argv[0], ctx->errstr);
191                 exit(ret);
192             }
193         }
194     }
195     if (ctx->passwd_src == PW_ARG)
196         args_left++;
197     if (rv != APR_EOF)
198         usage();
199
200     if ((*mask & APHTP_NEWFILE) && (*mask & APHTP_NOFILE)) {
201         apr_file_printf(errfile, "%s: -c and -n options conflict" NL, argv[0]);
202         exit(ERR_SYNTAX);
203     }
204     if ((*mask & APHTP_NEWFILE) && (*mask & APHTP_DELUSER)) {
205         apr_file_printf(errfile, "%s: -c and -D options conflict" NL, argv[0]);
206         exit(ERR_SYNTAX);
207     }
208     if ((*mask & APHTP_NOFILE) && (*mask & APHTP_DELUSER)) {
209         apr_file_printf(errfile, "%s: -n and -D options conflict" NL, argv[0]);
210         exit(ERR_SYNTAX);
211     }
212     /*
213      * Make sure we still have exactly the right number of arguments left
214      * (the filename, the username, and possibly the password if -b was
215      * specified).
216      */
217     i = state->ind;
218     if ((argc - i) != args_left) {
219         usage();
220     }
221
222     if (!(*mask & APHTP_NOFILE)) {
223         if (strlen(argv[i]) > (APR_PATH_MAX - 1)) {
224             apr_file_printf(errfile, "%s: filename too long" NL, argv[0]);
225             exit(ERR_OVERFLOW);
226         }
227         *pwfilename = apr_pstrdup(pool, argv[i++]);
228     }
229     if (strlen(argv[i]) > (MAX_STRING_LEN - 1)) {
230         apr_file_printf(errfile, "%s: username too long (> %d)" NL,
231                         argv[0], MAX_STRING_LEN - 1);
232         exit(ERR_OVERFLOW);
233     }
234     *user = apr_pstrdup(pool, argv[i++]);
235     if ((arg = strchr(*user, ':')) != NULL) {
236         apr_file_printf(errfile, "%s: username contains illegal "
237                         "character '%c'" NL, argv[0], *arg);
238         exit(ERR_BADUSER);
239     }
240     if (ctx->passwd_src == PW_ARG) {
241         if (strlen(argv[i]) > (MAX_STRING_LEN - 1)) {
242             apr_file_printf(errfile, "%s: password too long (> %d)" NL,
243                 argv[0], MAX_STRING_LEN);
244             exit(ERR_OVERFLOW);
245         }
246         ctx->passwd = apr_pstrdup(pool, argv[i]);
247     }
248 }
249
250 /*
251  * Let's do it.  We end up doing a lot of file opening and closing,
252  * but what do we care?  This application isn't run constantly.
253  */
254 int main(int argc, const char * const argv[])
255 {
256     apr_file_t *fpw = NULL;
257     const char *errstr = NULL;
258     char line[MAX_STRING_LEN];
259     char *pwfilename = NULL;
260     char *user = NULL;
261     char tn[] = "htpasswd.tmp.XXXXXX";
262     char *dirname;
263     char *scratch, cp[MAX_STRING_LEN];
264     int found = 0;
265     int i;
266     int mask = 0;
267     apr_pool_t *pool;
268     int existing_file = 0;
269     struct passwd_ctx ctx = { 0 };
270 #if APR_CHARSET_EBCDIC
271     apr_status_t rv;
272     apr_xlate_t *to_ascii;
273 #endif
274
275     apr_app_initialize(&argc, &argv, NULL);
276     atexit(terminate);
277     apr_pool_create(&pool, NULL);
278     apr_file_open_stderr(&errfile, pool);
279     ctx.pool = pool;
280     ctx.alg = ALG_APMD5;
281
282 #if APR_CHARSET_EBCDIC
283     rv = apr_xlate_open(&to_ascii, "ISO-8859-1", APR_DEFAULT_CHARSET, pool);
284     if (rv) {
285         apr_file_printf(errfile, "apr_xlate_open(to ASCII)->%d" NL, rv);
286         exit(1);
287     }
288     rv = apr_SHA1InitEBCDIC(to_ascii);
289     if (rv) {
290         apr_file_printf(errfile, "apr_SHA1InitEBCDIC()->%d" NL, rv);
291         exit(1);
292     }
293     rv = apr_MD5InitEBCDIC(to_ascii);
294     if (rv) {
295         apr_file_printf(errfile, "apr_MD5InitEBCDIC()->%d" NL, rv);
296         exit(1);
297     }
298 #endif /*APR_CHARSET_EBCDIC*/
299
300     check_args(argc, argv, &ctx, &mask, &user, &pwfilename);
301
302     /*
303      * Only do the file checks if we're supposed to frob it.
304      */
305     if (!(mask & APHTP_NOFILE)) {
306         existing_file = exists(pwfilename, pool);
307         if (existing_file) {
308             /*
309              * Check that this existing file is readable and writable.
310              */
311             if (!accessible(pool, pwfilename, APR_FOPEN_READ|APR_FOPEN_WRITE)) {
312                 apr_file_printf(errfile, "%s: cannot open file %s for "
313                                 "read/write access" NL, argv[0], pwfilename);
314                 exit(ERR_FILEPERM);
315             }
316         }
317         else {
318             /*
319              * Error out if -c was omitted for this non-existant file.
320              */
321             if (!(mask & APHTP_NEWFILE)) {
322                 apr_file_printf(errfile,
323                         "%s: cannot modify file %s; use '-c' to create it" NL,
324                         argv[0], pwfilename);
325                 exit(ERR_FILEPERM);
326             }
327             /*
328              * As it doesn't exist yet, verify that we can create it.
329              */
330             if (!accessible(pool, pwfilename, APR_FOPEN_WRITE|APR_FOPEN_CREATE)) {
331                 apr_file_printf(errfile, "%s: cannot create file %s" NL,
332                                 argv[0], pwfilename);
333                 exit(ERR_FILEPERM);
334             }
335         }
336     }
337
338     /*
339      * All the file access checks (if any) have been made.  Time to go to work;
340      * try to create the record for the username in question.  If that
341      * fails, there's no need to waste any time on file manipulations.
342      * Any error message text is returned in the record buffer, since
343      * the mkrecord() routine doesn't have access to argv[].
344      */
345     if (!(mask & APHTP_DELUSER)) {
346         i = mkrecord(&ctx, user);
347         if (i != 0) {
348             apr_file_printf(errfile, "%s: %s" NL, argv[0], errstr);
349             exit(i);
350         }
351         if (mask & APHTP_NOFILE) {
352             printf("%s" NL, ctx.out);
353             exit(0);
354         }
355     }
356
357     /*
358      * We can access the files the right way, and we have a record
359      * to add or update.  Let's do it..
360      */
361     if (apr_temp_dir_get((const char**)&dirname, pool) != APR_SUCCESS) {
362         apr_file_printf(errfile, "%s: could not determine temp dir" NL,
363                         argv[0]);
364         exit(ERR_FILEPERM);
365     }
366     dirname = apr_psprintf(pool, "%s/%s", dirname, tn);
367
368     if (apr_file_mktemp(&ftemp, dirname, 0, pool) != APR_SUCCESS) {
369         apr_file_printf(errfile, "%s: unable to create temporary file %s" NL,
370                         argv[0], dirname);
371         exit(ERR_FILEPERM);
372     }
373
374     /*
375      * If we're not creating a new file, copy records from the existing
376      * one to the temporary file until we find the specified user.
377      */
378     if (existing_file && !(mask & APHTP_NEWFILE)) {
379         if (apr_file_open(&fpw, pwfilename, APR_READ | APR_BUFFERED,
380                           APR_OS_DEFAULT, pool) != APR_SUCCESS) {
381             apr_file_printf(errfile, "%s: unable to read file %s" NL,
382                             argv[0], pwfilename);
383             exit(ERR_FILEPERM);
384         }
385         while (apr_file_gets(line, sizeof(line), fpw) == APR_SUCCESS) {
386             char *colon;
387
388             strcpy(cp, line);
389             scratch = cp;
390             while (apr_isspace(*scratch)) {
391                 ++scratch;
392             }
393
394             if (!*scratch || (*scratch == '#')) {
395                 putline(ftemp, line);
396                 continue;
397             }
398             /*
399              * See if this is our user.
400              */
401             colon = strchr(scratch, ':');
402             if (colon != NULL) {
403                 *colon = '\0';
404             }
405             else {
406                 /*
407                  * If we've not got a colon on the line, this could well
408                  * not be a valid htpasswd file.
409                  * We should bail at this point.
410                  */
411                 apr_file_printf(errfile, "%s: The file %s does not appear "
412                                          "to be a valid htpasswd file." NL,
413                                 argv[0], pwfilename);
414                 apr_file_close(fpw);
415                 exit(ERR_INVALID);
416             }
417             if (strcmp(user, scratch) != 0) {
418                 putline(ftemp, line);
419                 continue;
420             }
421             else {
422                 if (!(mask & APHTP_DELUSER)) {
423                     /* We found the user we were looking for.
424                      * Add him to the file.
425                     */
426                     apr_file_printf(errfile, "Updating ");
427                     putline(ftemp, ctx.out);
428                     found++;
429                 }
430                 else {
431                     /* We found the user we were looking for.
432                      * Delete them from the file.
433                      */
434                     apr_file_printf(errfile, "Deleting ");
435                     found++;
436                 }
437             }
438         }
439         apr_file_close(fpw);
440     }
441     if (!found && !(mask & APHTP_DELUSER)) {
442         apr_file_printf(errfile, "Adding ");
443         putline(ftemp, ctx.out);
444     }
445     else if (!found && (mask & APHTP_DELUSER)) {
446         apr_file_printf(errfile, "User %s not found" NL, user);
447         exit(0);
448     }
449     apr_file_printf(errfile, "password for user %s" NL, user);
450
451     /* The temporary file has all the data, just copy it to the new location.
452      */
453     if (apr_file_copy(dirname, pwfilename, APR_FILE_SOURCE_PERMS, pool) !=
454         APR_SUCCESS) {
455         apr_file_printf(errfile, "%s: unable to update file %s" NL,
456                         argv[0], pwfilename);
457         exit(ERR_FILEPERM);
458     }
459     apr_file_close(ftemp);
460     return 0;
461 }