/* Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /****************************************************************************** ****************************************************************************** * NOTE! This program is not safe as a setuid executable! Do not make it * setuid! ****************************************************************************** *****************************************************************************/ /* * htpasswd.c: simple program for manipulating password file for * the Apache HTTP server * * Originally by Rob McCool * * Exit values: * 0: Success * 1: Failure; file access/permission problem * 2: Failure; command line syntax problem (usage message issued) * 3: Failure; password verification failure * 4: Failure; operation interrupted (such as with CTRL/C) * 5: Failure; buffer would overflow (username, filename, or computed * record too long) * 6: Failure; username contains illegal or reserved characters * 7: Failure; file is not a valid htpasswd file */ #include "passwd_common.h" #include "apr_signal.h" #include "apr_getopt.h" #if APR_HAVE_STDIO_H #include #endif #include "apr_md5.h" #include "apr_sha1.h" #if APR_HAVE_STDLIB_H #include #endif #if APR_HAVE_STRING_H #include #endif #if APR_HAVE_UNISTD_H #include #endif #ifdef WIN32 #include #define unlink _unlink #endif #define APHTP_NEWFILE 1 #define APHTP_NOFILE 2 #define APHTP_DELUSER 4 #define APHTP_VERIFY 8 apr_file_t *ftemp = NULL; static int mkrecord(struct passwd_ctx *ctx, char *user) { char hash_str[MAX_STRING_LEN]; int ret; ctx->out = hash_str; ctx->out_len = sizeof(hash_str); ret = mkhash(ctx); if (ret) return ret; ctx->out = apr_pstrcat(ctx->pool, user, ":", hash_str, NL, NULL); if (strlen(ctx->out) >= MAX_STRING_LEN) { ctx->errstr = "resultant record too long"; return ERR_OVERFLOW; } return 0; } static void usage(void) { apr_file_printf(errfile, "Usage:" NL "\thtpasswd [-cimBdpsDv] [-C cost] passwordfile username" NL "\thtpasswd -b[cmBdpsDv] [-C cost] passwordfile username password" NL NL "\thtpasswd -n[imBdps] [-C cost] username" NL "\thtpasswd -nb[mBdps] [-C cost] username password" NL " -c Create a new file." NL " -n Don't update file; display results on stdout." NL " -b Use the password from the command line rather than prompting " "for it." NL " -i Read password from stdin without verification (for script usage)." NL " -m Force MD5 encryption of the password (default)." NL " -B Force bcrypt encryption of the password (very secure)." NL " -C Set the computing time used for the bcrypt algorithm" NL " (higher is more secure but slower, default: %d, valid: 4 to 31)." NL " -d Force CRYPT encryption of the password (8 chars max, insecure)." NL " -s Force SHA encryption of the password (insecure)." NL " -p Do not encrypt the password (plaintext, insecure)." NL " -D Delete the specified user." NL " -v Verify password for the specified user." NL "On other systems than Windows and NetWare the '-p' flag will " "probably not work." NL "The SHA algorithm does not use a salt and is less secure than the " "MD5 algorithm." NL, BCRYPT_DEFAULT_COST ); exit(ERR_SYNTAX); } /* * Check to see if the specified file can be opened for the given * access. */ static int accessible(apr_pool_t *pool, char *fname, int mode) { apr_file_t *f = NULL; if (apr_file_open(&f, fname, mode, APR_OS_DEFAULT, pool) != APR_SUCCESS) { return 0; } apr_file_close(f); return 1; } /* * Return true if the named file exists, regardless of permissions. */ static int exists(char *fname, apr_pool_t *pool) { apr_finfo_t sbuf; apr_status_t check; check = apr_stat(&sbuf, fname, APR_FINFO_TYPE, pool); return ((check || sbuf.filetype != APR_REG) ? 0 : 1); } static void terminate(void) { apr_terminate(); #ifdef NETWARE pressanykey(); #endif } static void check_args(int argc, const char *const argv[], struct passwd_ctx *ctx, unsigned *mask, char **user, char **pwfilename) { const char *arg; int args_left = 2; int i, ret; apr_getopt_t *state; apr_status_t rv; char opt; const char *opt_arg; apr_pool_t *pool = ctx->pool; rv = apr_getopt_init(&state, pool, argc, argv); if (rv != APR_SUCCESS) exit(ERR_SYNTAX); while ((rv = apr_getopt(state, "cnmspdBbDiC:v", &opt, &opt_arg)) == APR_SUCCESS) { switch (opt) { case 'c': *mask |= APHTP_NEWFILE; break; case 'n': args_left--; *mask |= APHTP_NOFILE; break; case 'D': *mask |= APHTP_DELUSER; break; case 'v': *mask |= APHTP_VERIFY; break; default: ret = parse_common_options(ctx, opt, opt_arg); if (ret) { apr_file_printf(errfile, "%s: %s" NL, argv[0], ctx->errstr); exit(ret); } } } if (ctx->passwd_src == PW_ARG) args_left++; if (rv != APR_EOF) usage(); if ((*mask) & (*mask - 1)) { /* not a power of two, i.e. more than one flag specified */ apr_file_printf(errfile, "%s: only one of -c -n -v -D may be specified" NL, argv[0]); exit(ERR_SYNTAX); } if ((*mask & APHTP_VERIFY) && ctx->passwd_src == PW_PROMPT) ctx->passwd_src = PW_PROMPT_VERIFY; /* * Make sure we still have exactly the right number of arguments left * (the filename, the username, and possibly the password if -b was * specified). */ i = state->ind; if ((argc - i) != args_left) { usage(); } if (!(*mask & APHTP_NOFILE)) { if (strlen(argv[i]) > (APR_PATH_MAX - 1)) { apr_file_printf(errfile, "%s: filename too long" NL, argv[0]); exit(ERR_OVERFLOW); } *pwfilename = apr_pstrdup(pool, argv[i++]); } if (strlen(argv[i]) > (MAX_STRING_LEN - 1)) { apr_file_printf(errfile, "%s: username too long (> %d)" NL, argv[0], MAX_STRING_LEN - 1); exit(ERR_OVERFLOW); } *user = apr_pstrdup(pool, argv[i++]); if ((arg = strchr(*user, ':')) != NULL) { apr_file_printf(errfile, "%s: username contains illegal " "character '%c'" NL, argv[0], *arg); exit(ERR_BADUSER); } if (ctx->passwd_src == PW_ARG) { if (strlen(argv[i]) > (MAX_STRING_LEN - 1)) { apr_file_printf(errfile, "%s: password too long (> %d)" NL, argv[0], MAX_STRING_LEN); exit(ERR_OVERFLOW); } ctx->passwd = apr_pstrdup(pool, argv[i]); } } static int verify(struct passwd_ctx *ctx, const char *hash) { apr_status_t rv; int ret; if (ctx->passwd == NULL && (ret = get_password(ctx)) != 0) return ret; rv = apr_password_validate(ctx->passwd, hash); if (rv == APR_SUCCESS) return 0; if (APR_STATUS_IS_EMISMATCH(rv)) { ctx->errstr = "password verification failed"; return ERR_PWMISMATCH; } ctx->errstr = apr_psprintf(ctx->pool, "Could not verify password: %pm", &rv); return ERR_GENERAL; } /* * Let's do it. We end up doing a lot of file opening and closing, * but what do we care? This application isn't run constantly. */ int main(int argc, const char * const argv[]) { apr_file_t *fpw = NULL; char line[MAX_STRING_LEN]; char *pwfilename = NULL; char *user = NULL; char tn[] = "htpasswd.tmp.XXXXXX"; char *dirname; char *scratch, cp[MAX_STRING_LEN]; int found = 0; int i; unsigned mask = 0; apr_pool_t *pool; int existing_file = 0; struct passwd_ctx ctx = { 0 }; #if APR_CHARSET_EBCDIC apr_status_t rv; apr_xlate_t *to_ascii; #endif apr_app_initialize(&argc, &argv, NULL); atexit(terminate); apr_pool_create(&pool, NULL); apr_pool_abort_set(abort_on_oom, pool); apr_file_open_stderr(&errfile, pool); ctx.pool = pool; ctx.alg = ALG_APMD5; #if APR_CHARSET_EBCDIC rv = apr_xlate_open(&to_ascii, "ISO-8859-1", APR_DEFAULT_CHARSET, pool); if (rv) { apr_file_printf(errfile, "apr_xlate_open(to ASCII)->%d" NL, rv); exit(1); } rv = apr_SHA1InitEBCDIC(to_ascii); if (rv) { apr_file_printf(errfile, "apr_SHA1InitEBCDIC()->%d" NL, rv); exit(1); } rv = apr_MD5InitEBCDIC(to_ascii); if (rv) { apr_file_printf(errfile, "apr_MD5InitEBCDIC()->%d" NL, rv); exit(1); } #endif /*APR_CHARSET_EBCDIC*/ check_args(argc, argv, &ctx, &mask, &user, &pwfilename); /* * Only do the file checks if we're supposed to frob it. */ if (!(mask & APHTP_NOFILE)) { existing_file = exists(pwfilename, pool); if (existing_file) { /* * Check that this existing file is readable and writable. */ if (!accessible(pool, pwfilename, APR_FOPEN_READ|APR_FOPEN_WRITE)) { apr_file_printf(errfile, "%s: cannot open file %s for " "read/write access" NL, argv[0], pwfilename); exit(ERR_FILEPERM); } } else { /* * Error out if -c was omitted for this non-existant file. */ if (!(mask & APHTP_NEWFILE)) { apr_file_printf(errfile, "%s: cannot modify file %s; use '-c' to create it" NL, argv[0], pwfilename); exit(ERR_FILEPERM); } /* * As it doesn't exist yet, verify that we can create it. */ if (!accessible(pool, pwfilename, APR_FOPEN_WRITE|APR_FOPEN_CREATE)) { apr_file_printf(errfile, "%s: cannot create file %s" NL, argv[0], pwfilename); exit(ERR_FILEPERM); } } } /* * All the file access checks (if any) have been made. Time to go to work; * try to create the record for the username in question. If that * fails, there's no need to waste any time on file manipulations. * Any error message text is returned in the record buffer, since * the mkrecord() routine doesn't have access to argv[]. */ if ((mask & (APHTP_DELUSER|APHTP_VERIFY)) == 0) { i = mkrecord(&ctx, user); if (i != 0) { apr_file_printf(errfile, "%s: %s" NL, argv[0], ctx.errstr); exit(i); } if (mask & APHTP_NOFILE) { printf("%s" NL, ctx.out); exit(0); } } if ((mask & APHTP_VERIFY) == 0) { /* * We can access the files the right way, and we have a record * to add or update. Let's do it.. */ if (apr_temp_dir_get((const char**)&dirname, pool) != APR_SUCCESS) { apr_file_printf(errfile, "%s: could not determine temp dir" NL, argv[0]); exit(ERR_FILEPERM); } dirname = apr_psprintf(pool, "%s/%s", dirname, tn); if (apr_file_mktemp(&ftemp, dirname, 0, pool) != APR_SUCCESS) { apr_file_printf(errfile, "%s: unable to create temporary file %s" NL, argv[0], dirname); exit(ERR_FILEPERM); } } /* * If we're not creating a new file, copy records from the existing * one to the temporary file until we find the specified user. */ if (existing_file && !(mask & APHTP_NEWFILE)) { if (apr_file_open(&fpw, pwfilename, APR_READ | APR_BUFFERED, APR_OS_DEFAULT, pool) != APR_SUCCESS) { apr_file_printf(errfile, "%s: unable to read file %s" NL, argv[0], pwfilename); exit(ERR_FILEPERM); } while (apr_file_gets(line, sizeof(line), fpw) == APR_SUCCESS) { char *colon; strcpy(cp, line); scratch = cp; while (apr_isspace(*scratch)) { ++scratch; } if (!*scratch || (*scratch == '#')) { putline(ftemp, line); continue; } /* * See if this is our user. */ colon = strchr(scratch, ':'); if (colon != NULL) { *colon = '\0'; } else { /* * If we've not got a colon on the line, this could well * not be a valid htpasswd file. * We should bail at this point. */ apr_file_printf(errfile, "%s: The file %s does not appear " "to be a valid htpasswd file." NL, argv[0], pwfilename); apr_file_close(fpw); exit(ERR_INVALID); } if (strcmp(user, scratch) != 0) { putline(ftemp, line); continue; } else { /* We found the user we were looking for */ found++; if ((mask & APHTP_DELUSER)) { /* Delete entry from the file */ apr_file_printf(errfile, "Deleting "); } else if ((mask & APHTP_VERIFY)) { /* Verify */ char *hash = colon + 1; size_t len; len = strcspn(hash, "\r\n"); if (len == 0) { apr_file_printf(errfile, "Empty hash for user %s" NL, user); exit(ERR_INVALID); } hash[len] = '\0'; i = verify(&ctx, hash); if (i != 0) { apr_file_printf(errfile, "%s" NL, ctx.errstr); exit(i); } } else { /* Update entry */ apr_file_printf(errfile, "Updating "); putline(ftemp, ctx.out); } } } apr_file_close(fpw); } if (!found) { if (mask & APHTP_DELUSER) { apr_file_printf(errfile, "User %s not found" NL, user); exit(0); } else if (mask & APHTP_VERIFY) { apr_file_printf(errfile, "User %s not found" NL, user); exit(ERR_BADUSER); } else { apr_file_printf(errfile, "Adding "); putline(ftemp, ctx.out); } } if (mask & APHTP_VERIFY) { apr_file_printf(errfile, "Password for user %s correct." NL, user); exit(0); } apr_file_printf(errfile, "password for user %s" NL, user); /* The temporary file has all the data, just copy it to the new location. */ if (apr_file_copy(dirname, pwfilename, APR_OS_DEFAULT, pool) != APR_SUCCESS) { apr_file_printf(errfile, "%s: unable to update file %s" NL, argv[0], pwfilename); exit(ERR_FILEPERM); } apr_file_close(ftemp); return 0; }