]> granicus.if.org Git - apache/blob - support/htpasswd.c
Be more expliciti in our error messages if the tmpnam() call fails,
[apache] / support / htpasswd.c
1 /* ====================================================================
2  * The Apache Software License, Version 1.1
3  *
4  * Copyright (c) 2000 The Apache Software Foundation.  All rights
5  * reserved.
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions
9  * are met:
10  *
11  * 1. Redistributions of source code must retain the above copyright
12  *    notice, this list of conditions and the following disclaimer.
13  *
14  * 2. Redistributions in binary form must reproduce the above copyright
15  *    notice, this list of conditions and the following disclaimer in
16  *    the documentation and/or other materials provided with the
17  *    distribution.
18  *
19  * 3. The end-user documentation included with the redistribution,
20  *    if any, must include the following acknowledgment:
21  *       "This product includes software developed by the
22  *        Apache Software Foundation (http://www.apache.org/)."
23  *    Alternately, this acknowledgment may appear in the software itself,
24  *    if and wherever such third-party acknowledgments normally appear.
25  *
26  * 4. The names "Apache" and "Apache Software Foundation" must
27  *    not be used to endorse or promote products derived from this
28  *    software without prior written permission. For written
29  *    permission, please contact apache@apache.org.
30  *
31  * 5. Products derived from this software may not be called "Apache",
32  *    nor may "Apache" appear in their name, without prior written
33  *    permission of the Apache Software Foundation.
34  *
35  * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
36  * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
37  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
38  * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
39  * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
40  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
41  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
42  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
43  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
44  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
45  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
46  * SUCH DAMAGE.
47  * ====================================================================
48  *
49  * This software consists of voluntary contributions made by many
50  * individuals on behalf of the Apache Software Foundation.  For more
51  * information on the Apache Software Foundation, please see
52  * <http://www.apache.org/>.
53  *
54  * Portions of this software are based upon public domain software
55  * originally written at the National Center for Supercomputing Applications,
56  * University of Illinois, Urbana-Champaign.
57  */
58
59 /******************************************************************************
60  ******************************************************************************
61  * NOTE! This program is not safe as a setuid executable!  Do not make it
62  * setuid!
63  ******************************************************************************
64  *****************************************************************************/
65 /*
66  * htpasswd.c: simple program for manipulating password file for
67  * the Apache HTTP server
68  * 
69  * Originally by Rob McCool
70  *
71  * Exit values:
72  *  0: Success
73  *  1: Failure; file access/permission problem
74  *  2: Failure; command line syntax problem (usage message issued)
75  *  3: Failure; password verification failure
76  *  4: Failure; operation interrupted (such as with CTRL/C)
77  *  5: Failure; buffer would overflow (username, filename, or computed
78  *     record too long)
79  *  6: Failure; username contains illegal or reserved characters
80  */
81
82 #include "apr_lib.h"
83 #include "apr_errno.h"
84 #include "ap_config.h"
85 #include "apr_md5.h"
86 #include "ap_sha1.h"
87 #include <signal.h>
88
89 #ifdef WIN32
90 #include <conio.h>
91 #define unlink _unlink
92 #endif
93
94 #ifndef CHARSET_EBCDIC
95 #define LF 10
96 #define CR 13
97 #else /*CHARSET_EBCDIC*/
98 #define LF '\n'
99 #define CR '\r'
100 #endif /*CHARSET_EBCDIC*/
101
102 #define MAX_STRING_LEN 256
103 #define ALG_PLAIN 0
104 #define ALG_CRYPT 1
105 #define ALG_APMD5 2
106 #define ALG_APSHA 3 
107
108 #define ERR_FILEPERM 1
109 #define ERR_SYNTAX 2
110 #define ERR_PWMISMATCH 3
111 #define ERR_INTERRUPTED 4
112 #define ERR_OVERFLOW 5
113 #define ERR_BADUSER 6
114
115 /*
116  * This needs to be declared statically so the signal handler can
117  * access it.
118  */
119 static char *tempfilename;
120
121 /*
122  * Get a line of input from the user, not including any terminating
123  * newline.
124  */
125 static int getline(char *s, int n, FILE *f)
126 {
127     register int i = 0;
128
129     while (1) {
130         s[i] = (char) fgetc(f);
131
132         if (s[i] == CR) {
133             s[i] = fgetc(f);
134         }
135
136         if ((s[i] == 0x4) || (s[i] == LF) || (i == (n - 1))) {
137             s[i] = '\0';
138             return (feof(f) ? 1 : 0);
139         }
140         ++i;
141     }
142 }
143
144 static void putline(FILE *f, char *l)
145 {
146     int x;
147
148     for (x = 0; l[x]; x++) {
149         fputc(l[x], f);
150     }
151     fputc('\n', f);
152 }
153
154 static void to64(char *s, unsigned long v, int n)
155 {
156     static unsigned char itoa64[] =         /* 0 ... 63 => ASCII - 64 */
157         "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
158
159     while (--n >= 0) {
160         *s++ = itoa64[v&0x3f];
161         v >>= 6;
162     }
163 }
164
165 /*
166  * Make a password record from the given information.  A zero return
167  * indicates success; failure means that the output buffer contains an
168  * error message instead.
169  */
170 static int mkrecord(char *user, char *record, size_t rlen, char *passwd,
171                     int alg)
172 {
173     char *pw;
174     char cpw[120];
175     char pwin[MAX_STRING_LEN];
176     char pwv[MAX_STRING_LEN];
177     char salt[9];
178     size_t bufsize;
179
180     if (passwd != NULL) {
181         pw = passwd;
182     }
183     else {
184         bufsize = sizeof(pwin);
185         if (ap_getpass("New password: ", pwin, &bufsize) != 0) {
186             ap_snprintf(record, (rlen - 1), "password too long (>%d)",
187                         sizeof(pwin) - 1);
188             return ERR_OVERFLOW;
189         }
190         bufsize = sizeof(pwv);
191         ap_getpass("Re-type new password: ", pwv, &bufsize);
192         if (strcmp(pwin, pwv) != 0) {
193             ap_cpystrn(record, "password verification error", (rlen - 1));
194             return ERR_PWMISMATCH;
195         }
196         pw = pwin;
197         memset(pwv, '\0', sizeof(pwin));
198     }
199     switch (alg) {
200
201     case ALG_APSHA:
202         /* XXX cpw >= 28 + strlen(sha1) chars - fixed len SHA */
203         ap_sha1_base64(pw,strlen(pw),cpw);
204         break;
205
206     case ALG_APMD5: 
207         (void) srand((int) time((time_t *) NULL));
208         to64(&salt[0], rand(), 8);
209         salt[8] = '\0';
210
211         ap_MD5Encode((const unsigned char *)pw, (const unsigned char *)salt,
212                      cpw, sizeof(cpw));
213         break;
214
215     case ALG_PLAIN:
216         /* XXX this len limitation is not in sync with any HTTPd len. */
217         ap_cpystrn(cpw,pw,sizeof(cpw));
218         break;
219
220     case ALG_CRYPT:
221     default:
222         (void) srand((int) time((time_t *) NULL));
223         to64(&salt[0], rand(), 8);
224         salt[8] = '\0';
225
226         ap_cpystrn(cpw, (char *)crypt(pw, salt), sizeof(cpw) - 1);
227         break;
228     }
229     memset(pw, '\0', strlen(pw));
230
231     /*
232      * Check to see if the buffer is large enough to hold the username,
233      * hash, and delimiters.
234      */
235     if ((strlen(user) + 1 + strlen(cpw)) > (rlen - 1)) {
236         ap_cpystrn(record, "resultant record too long", (rlen - 1));
237         return ERR_OVERFLOW;
238     }
239     strcpy(record, user);
240     strcat(record, ":");
241     strcat(record, cpw);
242     return 0;
243 }
244
245 static int usage(void)
246 {
247     fprintf(stderr, "Usage:\n");
248     fprintf(stderr, "\thtpasswd [-cmdps] passwordfile username\n");
249     fprintf(stderr, "\thtpasswd -b[cmdps] passwordfile username password\n\n");
250     fprintf(stderr, " -c  Create a new file.\n");
251     fprintf(stderr, " -m  Force MD5 encryption of the password"
252 #if defined(WIN32) || defined(TPF)
253         " (default)"
254 #endif
255         ".\n");
256     fprintf(stderr, " -d  Force CRYPT encryption of the password"
257 #if (!(defined(WIN32) || defined(TPF)))
258             " (default)"
259 #endif
260             ".\n");
261     fprintf(stderr, " -p  Do not encrypt the password (plaintext).\n");
262     fprintf(stderr, " -s  Force SHA encryption of the password.\n");
263     fprintf(stderr, " -b  Use the password from the command line rather "
264             "than prompting for it.\n");
265     fprintf(stderr,
266             "On Windows and TPF systems the '-m' flag is used by default.\n");
267     fprintf(stderr,
268             "On all other systems, the '-p' flag will probably not work.\n");
269     return ERR_SYNTAX;
270 }
271
272 static void interrupted(void)
273 {
274     fprintf(stderr, "Interrupted.\n");
275     if (tempfilename != NULL) {
276         unlink(tempfilename);
277     }
278     exit(ERR_INTERRUPTED);
279 }
280
281 /*
282  * Check to see if the specified file can be opened for the given
283  * access.
284  */
285 static int accessible(char *fname, char *mode)
286 {
287     FILE *s;
288
289     s = fopen(fname, mode);
290     if (s == NULL) {
291         return 0;
292     }
293     fclose(s);
294     return 1;
295 }
296
297 /*
298  * Return true if a file is readable.
299  */
300 static int readable(char *fname)
301 {
302     return accessible(fname, "r");
303 }
304
305 /*
306  * Return true if the specified file can be opened for write access.
307  */
308 static int writable(char *fname)
309 {
310     return accessible(fname, "a");
311 }
312
313 /*
314  * Return true if the named file exists, regardless of permissions.
315  */
316 static int exists(char *fname)
317 {
318 #ifdef WIN32
319     struct _stat sbuf;
320 #else
321     struct stat sbuf;
322 #endif
323     int check;
324
325 #ifdef WIN32
326     check = _stat(fname, &sbuf);
327 #else
328     check = stat(fname, &sbuf);
329 #endif
330     return ((check == -1) && (errno == ENOENT)) ? 0 : 1;
331 }
332
333 /*
334  * Copy from the current position of one file to the current position
335  * of another.
336  */
337 static void copy_file(FILE *target, FILE *source)
338 {
339     static char line[MAX_STRING_LEN];
340
341     while (fgets(line, sizeof(line), source) != NULL) {
342         fputs(line, target);
343     }
344 }
345
346 /*
347  * Let's do it.  We end up doing a lot of file opening and closing,
348  * but what do we care?  This application isn't run constantly.
349  */
350 int main(int argc, char *argv[])
351 {
352     FILE *ftemp = NULL;
353     FILE *fpw = NULL;
354     char user[MAX_STRING_LEN];
355     char password[MAX_STRING_LEN];
356     char record[MAX_STRING_LEN];
357     char line[MAX_STRING_LEN];
358     char pwfilename[MAX_STRING_LEN];
359     char *arg;
360     int found = 0;
361     int alg = ALG_CRYPT;
362     int newfile = 0;
363     int noninteractive = 0;
364     int i;
365     int args_left = 2;
366
367     tempfilename = NULL;
368     signal(SIGINT, (void (*)(int)) interrupted);
369
370     /*
371      * Preliminary check to make sure they provided at least
372      * three arguments, we'll do better argument checking as 
373      * we parse the command line.
374      */
375     if (argc < 3) {
376         return usage();
377     }
378
379     /*
380      * Go through the argument list and pick out any options.  They
381      * have to precede any other arguments.
382      */
383     for (i = 1; i < argc; i++) {
384         arg = argv[i];
385         if (*arg != '-') {
386             break;
387         }
388         while (*++arg != '\0') {
389             if (*arg == 'c') {
390                 newfile++;
391             }
392             else if (*arg == 'm') {
393                 alg = ALG_APMD5;
394             }
395             else if (*arg == 's') {
396                 alg = ALG_APSHA;
397             }
398             else if (*arg == 'p') {
399                 alg = ALG_PLAIN;
400             }
401             else if (*arg == 'd') {
402                 alg = ALG_CRYPT;
403             }
404             else if (*arg == 'b') {
405                 noninteractive++;
406                 args_left++;
407             }
408             else {
409                 return usage();
410             }
411         }
412     }
413
414     /*
415      * Make sure we still have exactly the right number of arguments left
416      * (the filename, the username, and possibly the password if -b was
417      * specified).
418      */
419     if ((argc - i) != args_left) {
420         return usage();
421     }
422     if (strlen(argv[i]) > (sizeof(pwfilename) - 1)) {
423         fprintf(stderr, "%s: filename too long\n", argv[0]);
424         return ERR_OVERFLOW;
425     }
426     strcpy(pwfilename, argv[i]);
427     if (strlen(argv[i + 1]) > (sizeof(user) - 1)) {
428         fprintf(stderr, "%s: username too long (>%d)\n", argv[0],
429                 sizeof(user) - 1);
430         return ERR_OVERFLOW;
431     }
432     strcpy(user, argv[i + 1]);
433     if ((arg = strchr(user, ':')) != NULL) {
434         fprintf(stderr, "%s: username contains illegal character '%c'\n",
435                 argv[0], *arg);
436         return ERR_BADUSER;
437     }
438     if (noninteractive) {
439         if (strlen(argv[i + 2]) > (sizeof(password) - 1)) {
440             fprintf(stderr, "%s: password too long (>%d)\n", argv[0],
441                     sizeof(password) - 1);
442             return ERR_OVERFLOW;
443         }
444         strcpy(password, argv[i + 2]);
445     }
446
447 #ifdef WIN32
448     if (alg == ALG_CRYPT) {
449         alg = ALG_APMD5;
450         fprintf(stderr, "Automatically using MD5 format on Windows.\n");
451     }
452 #endif
453
454 #if (!(defined(WIN32) || defined(TPF)))
455     if (alg == ALG_PLAIN) {
456         fprintf(stderr,"Warning: storing passwords as plain text might "
457                 "just not work on this platform.\n");
458     }
459 #endif
460     /*
461      * Verify that the file exists if -c was omitted.  We give a special
462      * message if it doesn't.
463      */
464     if ((! newfile) && (! exists(pwfilename))) {
465         fprintf(stderr, "%s: cannot modify file %s; use '-c' to create it\n",
466                 argv[0], pwfilename);
467         perror("fopen");
468         exit(ERR_FILEPERM);
469     }
470     /*
471      * Verify that we can read the existing file in the case of an update
472      * to it (rather than creation of a new one).
473      */
474     if ((! newfile) && (! readable(pwfilename))) {
475         fprintf(stderr, "%s: cannot open file %s for read access\n",
476                 argv[0], pwfilename);
477         perror("fopen");
478         exit(ERR_FILEPERM);
479     }
480     /*
481      * Now check to see if we can preserve an existing file in case
482      * of password verification errors on a -c operation.
483      */
484     if (newfile && exists(pwfilename) && (! readable(pwfilename))) {
485         fprintf(stderr, "%s: cannot open file %s for read access\n"
486                 "%s: existing auth data would be lost on password mismatch",
487                 argv[0], pwfilename, argv[0]);
488         perror("fopen");
489         exit(ERR_FILEPERM);
490     }
491     /*
492      * Now verify that the file is writable!
493      */
494     if (! writable(pwfilename)) {
495         fprintf(stderr, "%s: cannot open file %s for write access\n",
496                 argv[0], pwfilename);
497         perror("fopen");
498         exit(ERR_FILEPERM);
499     }
500
501     /*
502      * All the file access checks have been made.  Time to go to work;
503      * try to create the record for the username in question.  If that
504      * fails, there's no need to waste any time on file manipulations.
505      * Any error message text is returned in the record buffer, since
506      * the mkrecord() routine doesn't have access to argv[].
507      */
508     i = mkrecord(user, record, sizeof(record) - 1,
509                  noninteractive ? password : NULL,
510                  alg);
511     if (i != 0) {
512         fprintf(stderr, "%s: %s\n", argv[0], record);
513         exit(i);
514     }
515
516     /*
517      * We can access the files the right way, and we have a record
518      * to add or update.  Let's do it..
519      */
520     tempfilename = tmpnam(NULL);
521     if ((tempfilename == NULL) || (strlen(tempfilename) == 0)) {
522         fprintf(stderr, "%s: unable to generate temporary filename\n",
523                 argv[0]);
524         errno = ENOENT;
525         perror("tmpnam");
526         exit(ERR_FILEPERM);
527     }
528     ftemp = fopen(tempfilename, "w+");
529     if (ftemp == NULL) {
530         fprintf(stderr, "%s: unable to create temporary file '%s'\n", argv[0],
531                 tempfilename);
532         perror("fopen");
533         exit(ERR_FILEPERM);
534     }
535     /*
536      * If we're not creating a new file, copy records from the existing
537      * one to the temporary file until we find the specified user.
538      */
539     if (! newfile) {
540         char scratch[MAX_STRING_LEN];
541
542         fpw = fopen(pwfilename, "r");
543         while (! (getline(line, sizeof(line), fpw))) {
544             char *colon;
545
546             if ((line[0] == '#') || (line[0] == '\0')) {
547                 putline(ftemp, line);
548                 continue;
549             }
550             strcpy(scratch, line);
551             /*
552              * See if this is our user.
553              */
554             colon = strchr(scratch, ':');
555             if (colon != NULL) {
556                 *colon = '\0';
557             }
558             if (strcmp(user, scratch) != 0) {
559                 putline(ftemp, line);
560                 continue;
561             }
562             found++;
563             break;
564         }
565     }
566     if (found) {
567         fprintf(stderr, "Updating ");
568     }
569     else {
570         fprintf(stderr, "Adding ");
571     }
572     fprintf(stderr, "password for user %s\n", user);
573     /*
574      * Now add the user record we created.
575      */
576     putline(ftemp, record);
577     /*
578      * If we're updating an existing file, there may be additional
579      * records beyond the one we're updating, so copy them.
580      */
581     if (! newfile) {
582         copy_file(ftemp, fpw);
583         fclose(fpw);
584     }
585     /*
586      * The temporary file now contains the information that should be
587      * in the actual password file.  Close the open files, re-open them
588      * in the appropriate mode, and copy them file to the real one.
589      */
590     fclose(ftemp);
591     fpw = fopen(pwfilename, "w+");
592     ftemp = fopen(tempfilename, "r");
593     copy_file(fpw, ftemp);
594     fclose(fpw);
595     fclose(ftemp);
596     unlink(tempfilename);
597     return 0;
598 }