| /* 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 <stdio.h> | |
| #endif | |
| #include "apr_md5.h" | |
| #include "apr_sha1.h" | |
| #if APR_HAVE_STDLIB_H | |
| #include <stdlib.h> | |
| #endif | |
| #if APR_HAVE_STRING_H | |
| #include <string.h> | |
| #endif | |
| #if APR_HAVE_UNISTD_H | |
| #include <unistd.h> | |
| #endif | |
| #ifdef WIN32 | |
| #include <conio.h> | |
| #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) { | |
| ctx->out = NULL; | |
| ctx->out_len = 0; | |
| return ret; | |
| } | |
| ctx->out = apr_pstrcat(ctx->pool, user, ":", hash_str, NL, NULL); | |
| ctx->out_len = strlen(ctx->out); | |
| if (ctx->out_len >= 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 [-cimB25dpsDv] [-C cost] [-r rounds] passwordfile username" NL | |
| "\thtpasswd -b[cmB25dpsDv] [-C cost] [-r rounds] passwordfile username password" NL | |
| NL | |
| "\thtpasswd -n[imB25dps] [-C cost] [-r rounds] username" NL | |
| "\thtpasswd -nb[mB25dps] [-C cost] [-r rounds] 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 hashing of the password (default)." NL | |
| " -2 Force SHA-256 hashing of the password (secure)." NL | |
| " -5 Force SHA-512 hashing of the password (secure)." NL | |
| " -B Force bcrypt hashing 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 17)." NL | |
| " -r Set the number of rounds used for the SHA-256, SHA-512 algorithms" NL | |
| " (higher is more secure but slower, default: 5000)." NL | |
| " -d Force CRYPT hashing of the password (8 chars max, insecure)." NL | |
| " -s Force SHA-1 hashing of the password (insecure)." NL | |
| " -p Do not hash 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-1 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, "cnmspdBbDi25C:r: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 && (mask & APHTP_VERIFY) == 0) { | |
| /* | |
| * 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 if (existing_file && (mask & APHTP_VERIFY) != 0) { | |
| /* | |
| * Check that this existing file is readable. | |
| */ | |
| if (!accessible(pool, pwfilename, APR_FOPEN_READ)) { | |
| apr_file_printf(errfile, "%s: cannot open file %s for " | |
| "read access" NL, argv[0], pwfilename); | |
| exit(ERR_FILEPERM); | |
| } | |
| } | |
| else { | |
| /* | |
| * Error out if -c was omitted for this non-existent 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; | |
| } |