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 "apr.h" 42#include "apr_lib.h" 43#include "apr_strings.h" 44#include "apr_errno.h" 45#include "apr_file_io.h" 46#include "apr_general.h" 47#include "apr_signal.h" 48 49#if APR_HAVE_STDIO_H 50#include <stdio.h> 51#endif 52 53#include "apr_md5.h" 54#include "apr_sha1.h" 55#include <time.h> 56 57#if APR_HAVE_CRYPT_H 58#include <crypt.h> 59#endif 60#if APR_HAVE_STDLIB_H 61#include <stdlib.h> 62#endif 63#if APR_HAVE_STRING_H 64#include <string.h> 65#endif 66#if APR_HAVE_UNISTD_H 67#include <unistd.h> 68#endif 69 70#ifdef WIN32 71#include <conio.h> 72#define unlink _unlink 73#endif 74 75#if !APR_CHARSET_EBCDIC 76#define LF 10 77#define CR 13 78#else /*APR_CHARSET_EBCDIC*/ 79#define LF '\n' 80#define CR '\r' 81#endif /*APR_CHARSET_EBCDIC*/ 82 83#define MAX_STRING_LEN 256 84#define ALG_PLAIN 0 85#define ALG_CRYPT 1 86#define ALG_APMD5 2 87#define ALG_APSHA 3 88 89#define ERR_FILEPERM 1 90#define ERR_SYNTAX 2 91#define ERR_PWMISMATCH 3 92#define ERR_INTERRUPTED 4 93#define ERR_OVERFLOW 5 94#define ERR_BADUSER 6 95#define ERR_INVALID 7 96 97#define APHTP_NEWFILE 1 98#define APHTP_NOFILE 2 99#define APHTP_NONINTERACTIVE 4 100#define APHTP_DELUSER 8 101 102apr_file_t *errfile; 103apr_file_t *ftemp = NULL; 104 105#define NL APR_EOL_STR 106 107static void to64(char *s, unsigned long v, int n) 108{ 109 static unsigned char itoa64[] = /* 0 ... 63 => ASCII - 64 */ 110 "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 111 112 while (--n >= 0) { 113 *s++ = itoa64[v&0x3f]; 114 v >>= 6; 115 } 116} 117 118static void generate_salt(char *s, size_t size) 119{ 120 static unsigned char tbl[] = 121 "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 122 size_t i; 123 for (i = 0; i < size; ++i) { 124 int idx = (int) (64.0 * rand() / (RAND_MAX + 1.0)); 125 s[i] = tbl[idx]; 126 } 127} 128 129static apr_status_t seed_rand(void) 130{ 131 int seed = 0; 132 apr_status_t rv; 133 rv = apr_generate_random_bytes((unsigned char*) &seed, sizeof(seed)); 134 if (rv) { 135 apr_file_printf(errfile, "Unable to generate random bytes: %pm" NL, &rv); 136 return rv; 137 } 138 srand(seed); 139 return rv; 140} 141 142static void putline(apr_file_t *f, const char *l) 143{ 144 apr_status_t rc; 145 rc = apr_file_puts(l, f); 146 if (rc != APR_SUCCESS) { 147 char errstr[MAX_STRING_LEN]; 148 apr_strerror(rc, errstr, MAX_STRING_LEN); 149 apr_file_printf(errfile, "Error writing temp file: %s" NL, errstr); 150 apr_file_close(f); 151 exit(ERR_FILEPERM); 152 } 153} 154 155/* 156 * Make a password record from the given information. A zero return 157 * indicates success; failure means that the output buffer contains an 158 * error message instead. 159 */ 160static int mkrecord(char *user, char *record, apr_size_t rlen, char *passwd, 161 int alg) 162{ 163 char *pw; 164 char cpw[120]; 165 char pwin[MAX_STRING_LEN]; 166 char pwv[MAX_STRING_LEN]; 167 char salt[9]; 168 apr_size_t bufsize; 169#if (!(defined(WIN32) || defined(NETWARE))) 170 char *cbuf; 171#endif 172 173 if (passwd != NULL) { 174 pw = passwd; 175 } 176 else { 177 bufsize = sizeof(pwin); 178 if (apr_password_get("New password: ", pwin, &bufsize) != 0) { 179 apr_snprintf(record, (rlen - 1), "password too long (>%" 180 APR_SIZE_T_FMT ")", sizeof(pwin) - 1); 181 return ERR_OVERFLOW; 182 } 183 bufsize = sizeof(pwv); 184 apr_password_get("Re-type new password: ", pwv, &bufsize); 185 if (strcmp(pwin, pwv) != 0) { 186 apr_cpystrn(record, "password verification error", (rlen - 1)); 187 return ERR_PWMISMATCH; 188 } 189 pw = pwin; 190 memset(pwv, '\0', sizeof(pwin)); 191 } 192 switch (alg) { 193 194 case ALG_APSHA: 195 /* XXX cpw >= 28 + strlen(sha1) chars - fixed len SHA */ 196 apr_sha1_base64(pw,strlen(pw),cpw); 197 break; 198 199 case ALG_APMD5: 200 if (seed_rand()) { 201 break; 202 } 203 generate_salt(&salt[0], 8); 204 salt[8] = '\0'; 205 206 apr_md5_encode((const char *)pw, (const char *)salt, 207 cpw, sizeof(cpw)); 208 break; 209 210 case ALG_PLAIN: 211 /* XXX this len limitation is not in sync with any HTTPd len. */ 212 apr_cpystrn(cpw,pw,sizeof(cpw)); 213 break; 214 215#if (!(defined(WIN32) || defined(NETWARE))) 216 case ALG_CRYPT: 217 default: 218 if (seed_rand()) { 219 break; 220 } 221 to64(&salt[0], rand(), 8); 222 salt[8] = '\0'; 223 224 cbuf = crypt(pw, salt); 225 if (cbuf == NULL) { 226 char errbuf[128]; 227 228 apr_snprintf(record, rlen-1, "crypt() failed: %s", 229 apr_strerror(errno, errbuf, sizeof errbuf)); 230 return ERR_PWMISMATCH; 231 } 232 233 apr_cpystrn(cpw, cbuf, sizeof(cpw) - 1); 234 if (strlen(pw) > 8) { 235 char *truncpw = strdup(pw); 236 truncpw[8] = '\0'; 237 if (!strcmp(cpw, crypt(truncpw, salt))) { 238 apr_file_printf(errfile, "Warning: Password truncated to 8 characters " 239 "by CRYPT algorithm." NL); 240 } 241 free(truncpw); 242 } 243 break; 244#endif 245 } 246 memset(pw, '\0', strlen(pw)); 247 248 /* 249 * Check to see if the buffer is large enough to hold the username, 250 * hash, and delimiters. 251 */ 252 if ((strlen(user) + 1 + strlen(cpw)) > (rlen - 1)) { 253 apr_cpystrn(record, "resultant record too long", (rlen - 1)); 254 return ERR_OVERFLOW; 255 } 256 strcpy(record, user); 257 strcat(record, ":"); 258 strcat(record, cpw); 259 strcat(record, "\n"); 260 return 0; 261} 262 263static void usage(void) 264{ 265 apr_file_printf(errfile, "Usage:" NL); 266 apr_file_printf(errfile, "\thtpasswd [-cmdpsD] passwordfile username" NL); 267 apr_file_printf(errfile, "\thtpasswd -b[cmdpsD] passwordfile username " 268 "password" NL NL); 269 apr_file_printf(errfile, "\thtpasswd -n[mdps] username" NL); 270 apr_file_printf(errfile, "\thtpasswd -nb[mdps] username password" NL); 271 apr_file_printf(errfile, " -c Create a new file." NL); 272 apr_file_printf(errfile, " -n Don't update file; display results on " 273 "stdout." NL); 274 apr_file_printf(errfile, " -m Force MD5 encryption of the password" 275 " (default)" 276 "." NL); 277 apr_file_printf(errfile, " -d Force CRYPT encryption of the password" 278 "." NL); 279 apr_file_printf(errfile, " -p Do not encrypt the password (plaintext)." NL); 280 apr_file_printf(errfile, " -s Force SHA encryption of the password." NL); 281 apr_file_printf(errfile, " -b Use the password from the command line " 282 "rather than prompting for it." NL); 283 apr_file_printf(errfile, " -D Delete the specified user." NL); 284 apr_file_printf(errfile, 285 "On other systems than Windows, NetWare and TPF the '-p' flag will " 286 "probably not work." NL); 287 apr_file_printf(errfile, 288 "The SHA algorithm does not use a salt and is less secure than " 289 "the MD5 algorithm." NL); 290 exit(ERR_SYNTAX); 291} 292 293/* 294 * Check to see if the specified file can be opened for the given 295 * access. 296 */ 297static int accessible(apr_pool_t *pool, char *fname, int mode) 298{ 299 apr_file_t *f = NULL; 300 301 if (apr_file_open(&f, fname, mode, APR_OS_DEFAULT, pool) != APR_SUCCESS) { 302 return 0; 303 } 304 apr_file_close(f); 305 return 1; 306} 307 308/* 309 * Return true if the named file exists, regardless of permissions. 310 */ 311static int exists(char *fname, apr_pool_t *pool) 312{ 313 apr_finfo_t sbuf; 314 apr_status_t check; 315 316 check = apr_stat(&sbuf, fname, APR_FINFO_TYPE, pool); 317 return ((check || sbuf.filetype != APR_REG) ? 0 : 1); 318} 319 320static void terminate(void) 321{ 322 apr_terminate(); 323#ifdef NETWARE 324 pressanykey(); 325#endif 326} 327 328static void check_args(apr_pool_t *pool, int argc, const char *const argv[], 329 int *alg, int *mask, char **user, char **pwfilename, 330 char **password) 331{ 332 const char *arg; 333 int args_left = 2; 334 int i; 335 336 /* 337 * Preliminary check to make sure they provided at least 338 * three arguments, we'll do better argument checking as 339 * we parse the command line. 340 */ 341 if (argc < 3) { 342 usage(); 343 } 344 345 /* 346 * Go through the argument list and pick out any options. They 347 * have to precede any other arguments. 348 */ 349 for (i = 1; i < argc; i++) { 350 arg = argv[i]; 351 if (*arg != '-') { 352 break; 353 } 354 while (*++arg != '\0') { 355 if (*arg == 'c') { 356 *mask |= APHTP_NEWFILE; 357 } 358 else if (*arg == 'n') { 359 *mask |= APHTP_NOFILE; 360 args_left--; 361 } 362 else if (*arg == 'm') { 363 *alg = ALG_APMD5; 364 } 365 else if (*arg == 's') { 366 *alg = ALG_APSHA; 367 } 368 else if (*arg == 'p') { 369 *alg = ALG_PLAIN; 370 } 371 else if (*arg == 'd') { 372 *alg = ALG_CRYPT; 373 } 374 else if (*arg == 'b') { 375 *mask |= APHTP_NONINTERACTIVE; 376 args_left++; 377 } 378 else if (*arg == 'D') { 379 *mask |= APHTP_DELUSER; 380 } 381 else { 382 usage(); 383 } 384 } 385 } 386 387 if ((*mask & APHTP_NEWFILE) && (*mask & APHTP_NOFILE)) { 388 apr_file_printf(errfile, "%s: -c and -n options conflict" NL, argv[0]); 389 exit(ERR_SYNTAX); 390 } 391 if ((*mask & APHTP_NEWFILE) && (*mask & APHTP_DELUSER)) { 392 apr_file_printf(errfile, "%s: -c and -D options conflict" NL, argv[0]); 393 exit(ERR_SYNTAX); 394 } 395 if ((*mask & APHTP_NOFILE) && (*mask & APHTP_DELUSER)) { 396 apr_file_printf(errfile, "%s: -n and -D options conflict" NL, argv[0]); 397 exit(ERR_SYNTAX); 398 } 399 /* 400 * Make sure we still have exactly the right number of arguments left 401 * (the filename, the username, and possibly the password if -b was 402 * specified). 403 */ 404 if ((argc - i) != args_left) { 405 usage(); 406 } 407 408 if (*mask & APHTP_NOFILE) { 409 i--; 410 } 411 else { 412 if (strlen(argv[i]) > (APR_PATH_MAX - 1)) { 413 apr_file_printf(errfile, "%s: filename too long" NL, argv[0]); 414 exit(ERR_OVERFLOW); 415 } 416 *pwfilename = apr_pstrdup(pool, argv[i]); 417 if (strlen(argv[i + 1]) > (MAX_STRING_LEN - 1)) { 418 apr_file_printf(errfile, "%s: username too long (> %d)" NL, 419 argv[0], MAX_STRING_LEN - 1); 420 exit(ERR_OVERFLOW); 421 } 422 } 423 *user = apr_pstrdup(pool, argv[i + 1]); 424 if ((arg = strchr(*user, ':')) != NULL) { 425 apr_file_printf(errfile, "%s: username contains illegal " 426 "character '%c'" NL, argv[0], *arg); 427 exit(ERR_BADUSER); 428 } 429 if (*mask & APHTP_NONINTERACTIVE) { 430 if (strlen(argv[i + 2]) > (MAX_STRING_LEN - 1)) { 431 apr_file_printf(errfile, "%s: password too long (> %d)" NL, 432 argv[0], MAX_STRING_LEN); 433 exit(ERR_OVERFLOW); 434 } 435 *password = apr_pstrdup(pool, argv[i + 2]); 436 } 437} 438 439/* 440 * Let's do it. We end up doing a lot of file opening and closing, 441 * but what do we care? This application isn't run constantly. 442 */ 443int main(int argc, const char * const argv[]) 444{ 445 apr_file_t *fpw = NULL; 446 char record[MAX_STRING_LEN]; 447 char line[MAX_STRING_LEN]; 448 char *password = NULL; 449 char *pwfilename = NULL; 450 char *user = NULL; 451 char tn[] = "htpasswd.tmp.XXXXXX"; 452 char *dirname; 453 char *scratch, cp[MAX_STRING_LEN]; 454 int found = 0; 455 int i; 456 int alg = ALG_APMD5; 457 int mask = 0; 458 apr_pool_t *pool; 459 int existing_file = 0; 460#if APR_CHARSET_EBCDIC 461 apr_status_t rv; 462 apr_xlate_t *to_ascii; 463#endif 464 465 apr_app_initialize(&argc, &argv, NULL); 466 atexit(terminate); 467 apr_pool_create(&pool, NULL); 468 apr_file_open_stderr(&errfile, pool); 469 470#if APR_CHARSET_EBCDIC 471 rv = apr_xlate_open(&to_ascii, "ISO-8859-1", APR_DEFAULT_CHARSET, pool); 472 if (rv) { 473 apr_file_printf(errfile, "apr_xlate_open(to ASCII)->%d" NL, rv); 474 exit(1); 475 } 476 rv = apr_SHA1InitEBCDIC(to_ascii); 477 if (rv) { 478 apr_file_printf(errfile, "apr_SHA1InitEBCDIC()->%d" NL, rv); 479 exit(1); 480 } 481 rv = apr_MD5InitEBCDIC(to_ascii); 482 if (rv) { 483 apr_file_printf(errfile, "apr_MD5InitEBCDIC()->%d" NL, rv); 484 exit(1); 485 } 486#endif /*APR_CHARSET_EBCDIC*/ 487 488 check_args(pool, argc, argv, &alg, &mask, &user, &pwfilename, &password); 489 490 491#if defined(WIN32) || defined(NETWARE) 492 if (alg == ALG_CRYPT) { 493 alg = ALG_APMD5; 494 apr_file_printf(errfile, "Automatically using MD5 format." NL); 495 } 496#endif 497 498#if (!(defined(WIN32) || defined(TPF) || defined(NETWARE))) 499 if (alg == ALG_PLAIN) { 500 apr_file_printf(errfile,"Warning: storing passwords as plain text " 501 "might just not work on this platform." NL); 502 } 503#endif 504 505 /* 506 * Only do the file checks if we're supposed to frob it. 507 */ 508 if (!(mask & APHTP_NOFILE)) { 509 existing_file = exists(pwfilename, pool); 510 if (existing_file) { 511 /* 512 * Check that this existing file is readable and writable. 513 */ 514 if (!accessible(pool, pwfilename, APR_READ | APR_APPEND)) { 515 apr_file_printf(errfile, "%s: cannot open file %s for " 516 "read/write access" NL, argv[0], pwfilename); 517 exit(ERR_FILEPERM); 518 } 519 } 520 else { 521 /* 522 * Error out if -c was omitted for this non-existant file. 523 */ 524 if (!(mask & APHTP_NEWFILE)) { 525 apr_file_printf(errfile, 526 "%s: cannot modify file %s; use '-c' to create it" NL, 527 argv[0], pwfilename); 528 exit(ERR_FILEPERM); 529 } 530 /* 531 * As it doesn't exist yet, verify that we can create it. 532 */ 533 if (!accessible(pool, pwfilename, APR_CREATE | APR_WRITE)) { 534 apr_file_printf(errfile, "%s: cannot create file %s" NL, 535 argv[0], pwfilename); 536 exit(ERR_FILEPERM); 537 } 538 } 539 } 540 541 /* 542 * All the file access checks (if any) have been made. Time to go to work; 543 * try to create the record for the username in question. If that 544 * fails, there's no need to waste any time on file manipulations. 545 * Any error message text is returned in the record buffer, since 546 * the mkrecord() routine doesn't have access to argv[]. 547 */ 548 if (!(mask & APHTP_DELUSER)) { 549 i = mkrecord(user, record, sizeof(record) - 1, 550 password, alg); 551 if (i != 0) { 552 apr_file_printf(errfile, "%s: %s" NL, argv[0], record); 553 exit(i); 554 } 555 if (mask & APHTP_NOFILE) { 556 printf("%s" NL, record); 557 exit(0); 558 } 559 } 560 561 /* 562 * We can access the files the right way, and we have a record 563 * to add or update. Let's do it.. 564 */ 565 if (apr_temp_dir_get((const char**)&dirname, pool) != APR_SUCCESS) { 566 apr_file_printf(errfile, "%s: could not determine temp dir" NL, 567 argv[0]); 568 exit(ERR_FILEPERM); 569 } 570 dirname = apr_psprintf(pool, "%s/%s", dirname, tn); 571 572 if (apr_file_mktemp(&ftemp, dirname, 0, pool) != APR_SUCCESS) { 573 apr_file_printf(errfile, "%s: unable to create temporary file %s" NL, 574 argv[0], dirname); 575 exit(ERR_FILEPERM); 576 } 577 578 /* 579 * If we're not creating a new file, copy records from the existing 580 * one to the temporary file until we find the specified user. 581 */ 582 if (existing_file && !(mask & APHTP_NEWFILE)) { 583 if (apr_file_open(&fpw, pwfilename, APR_READ | APR_BUFFERED, 584 APR_OS_DEFAULT, pool) != APR_SUCCESS) { 585 apr_file_printf(errfile, "%s: unable to read file %s" NL, 586 argv[0], pwfilename); 587 exit(ERR_FILEPERM); 588 } 589 while (apr_file_gets(line, sizeof(line), fpw) == APR_SUCCESS) { 590 char *colon; 591 592 strcpy(cp, line); 593 scratch = cp; 594 while (apr_isspace(*scratch)) { 595 ++scratch; 596 } 597 598 if (!*scratch || (*scratch == '#')) { 599 putline(ftemp, line); 600 continue; 601 } 602 /* 603 * See if this is our user. 604 */ 605 colon = strchr(scratch, ':'); 606 if (colon != NULL) { 607 *colon = '\0'; 608 } 609 else { 610 /* 611 * If we've not got a colon on the line, this could well 612 * not be a valid htpasswd file. 613 * We should bail at this point. 614 */ 615 apr_file_printf(errfile, "%s: The file %s does not appear " 616 "to be a valid htpasswd file." NL, 617 argv[0], pwfilename); 618 apr_file_close(fpw); 619 exit(ERR_INVALID); 620 } 621 if (strcmp(user, scratch) != 0) { 622 putline(ftemp, line); 623 continue; 624 } 625 else { 626 if (!(mask & APHTP_DELUSER)) { 627 /* We found the user we were looking for. 628 * Add him to the file. 629 */ 630 apr_file_printf(errfile, "Updating "); 631 putline(ftemp, record); 632 found++; 633 } 634 else { 635 /* We found the user we were looking for. 636 * Delete them from the file. 637 */ 638 apr_file_printf(errfile, "Deleting "); 639 found++; 640 } 641 } 642 } 643 apr_file_close(fpw); 644 } 645 if (!found && !(mask & APHTP_DELUSER)) { 646 apr_file_printf(errfile, "Adding "); 647 putline(ftemp, record); 648 } 649 else if (!found && (mask & APHTP_DELUSER)) { 650 apr_file_printf(errfile, "User %s not found" NL, user); 651 exit(0); 652 } 653 apr_file_printf(errfile, "password for user %s" NL, user); 654 655 /* The temporary file has all the data, just copy it to the new location. 656 */ 657 if (apr_file_copy(dirname, pwfilename, APR_FILE_SOURCE_PERMS, pool) != 658 APR_SUCCESS) { 659 apr_file_printf(errfile, "%s: unable to update file %s" NL, 660 argv[0], pwfilename); 661 exit(ERR_FILEPERM); 662 } 663 apr_file_close(ftemp); 664 return 0; 665} 666