1 /* $NetBSD: bozohttpd.c,v 1.148 2024/10/04 08:45:46 rillig Exp $ */ 2 3 /* $eterna: bozohttpd.c,v 1.178 2011/11/18 09:21:15 mrg Exp $ */ 4 5 /* 6 * Copyright (c) 1997-2024 Matthew R. Green 7 * All rights reserved. 8 * 9 * Redistribution and use in source and binary forms, with or without 10 * modification, are permitted provided that the following conditions 11 * are met: 12 * 1. Redistributions of source code must retain the above copyright 13 * notice, this list of conditions and the following disclaimer. 14 * 2. Redistributions in binary form must reproduce the above copyright 15 * notice, this list of conditions and the following disclaimer and 16 * dedication in the documentation and/or other materials provided 17 * with the distribution. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 20 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 21 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 22 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 23 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 26 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 28 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 29 * SUCH DAMAGE. 30 * 31 */ 32 33 /* this program is dedicated to the Great God of Processed Cheese */ 34 35 /* 36 * bozohttpd.c: minimal httpd; provides only these features: 37 * - HTTP/0.9 (by virtue of ..) 38 * - HTTP/1.0 39 * - HTTP/1.1 40 * - CGI/1.1 this will only be provided for "system" scripts 41 * - automatic "missing trailing slash" redirections 42 * - configurable translation of /~user/ to ~user/public_html, 43 * - access lists via libwrap via inetd/tcpd 44 * - virtual hosting 45 * - not that we do not even pretend to understand MIME, but 46 * rely only on the HTTP specification 47 * - ipv6 support 48 * - automatic `index.html' generation 49 * - configurable server name 50 * - directory index generation 51 * - daemon mode (lacks libwrap support) 52 * - .htpasswd support 53 */ 54 55 /* 56 * requirements for minimal http/1.1 (at least, as documented in 57 * RFC 2616 (HTTP/1.1): 58 * 59 * - 14.11: content-encoding handling. [1] 60 * 61 * - 14.13: content-length handling. this is only a SHOULD header 62 * thus we could just not send it ever. [1] 63 * 64 * - 14.17: content-type handling. [1] 65 * 66 * - 14.28: if-unmodified-since handling. if-modified-since is 67 * done since, shouldn't be too hard for this one. 68 * 69 * [1] need to revisit to ensure proper behaviour 70 * 71 * and the following is a list of features that we do not need 72 * to have due to other limits, or are too lazy. there are more 73 * of these than are listed, but these are of particular note, 74 * and could perhaps be implemented. 75 * 76 * - 3.5/3.6: content/transfer codings. probably can ignore 77 * this? we "SHOULD"n't. but 4.4 says we should ignore a 78 * `content-length' header upon receipt of a `transfer-encoding' 79 * header. 80 * 81 * - 5.1.1: request methods. only MUST support GET and HEAD, 82 * but there are new ones besides POST that are currently 83 * supported: OPTIONS PUT DELETE TRACE and CONNECT, plus 84 * extensions not yet known? 85 * 86 * - 10.1: we can ignore informational status codes 87 * 88 * - 10.3.3/10.3.4/10.3.8: just use '302' codes always. 89 * 90 * - 14.1/14.2/14.3/14.27: we do not support Accept: headers. 91 * just ignore them and send the request anyway. they are 92 * only SHOULD. 93 * 94 * - 14.5/14.16/14.35: only support simple ranges: %d- and %d-%d 95 * would be nice to support more. 96 * 97 * - 14.9: we aren't a cache. 98 * 99 * - 14.15: content-md5 would be nice. 100 * 101 * - 14.24/14.26/14.27: if-match, if-none-match, if-range. be 102 * nice to support this. 103 * 104 * - 14.44: Vary: seems unneeded. ignore it for now. 105 */ 106 107 #ifndef INDEX_HTML 108 #define INDEX_HTML "index.html" 109 #endif 110 #ifndef SERVER_SOFTWARE 111 #define SERVER_SOFTWARE "bozohttpd/20240428" 112 #endif 113 #ifndef PUBLIC_HTML 114 #define PUBLIC_HTML "public_html" 115 #endif 116 117 #ifndef USE_ARG 118 #define USE_ARG(x) /*LINTED*/(void)&(x) 119 #endif 120 121 /* 122 * And so it begins .. 123 */ 124 125 #include <sys/param.h> 126 #include <sys/socket.h> 127 #include <sys/time.h> 128 #include <sys/mman.h> 129 130 #include <arpa/inet.h> 131 132 #include <ctype.h> 133 #include <dirent.h> 134 #include <errno.h> 135 #include <fcntl.h> 136 #include <netdb.h> 137 #include <pwd.h> 138 #include <grp.h> 139 #include <stdarg.h> 140 #include <stdlib.h> 141 #include <stdint.h> 142 #include <strings.h> 143 #include <string.h> 144 #include <syslog.h> 145 #include <time.h> 146 #include <unistd.h> 147 148 #include "bozohttpd.h" 149 150 #ifndef SSL_TIMEOUT 151 #define SSL_TIMEOUT "30" /* ssl handshake: 30 seconds timeout */ 152 #endif 153 #ifndef INITIAL_TIMEOUT 154 #define INITIAL_TIMEOUT "30" /* wait for 30 seconds initially */ 155 #endif 156 #ifndef HEADER_WAIT_TIME 157 #define HEADER_WAIT_TIME "10" /* need more headers every 10 seconds */ 158 #endif 159 #ifndef TOTAL_MAX_REQ_TIME 160 #define TOTAL_MAX_REQ_TIME "600" /* must have total request in 600 */ 161 #endif /* seconds */ 162 163 /* if monotonic time is not available try real time. */ 164 #ifndef CLOCK_MONOTONIC 165 #define CLOCK_MONOTONIC CLOCK_REALTIME 166 #endif 167 168 /* variables and functions */ 169 #ifndef LOG_FTP 170 #define LOG_FTP LOG_DAEMON 171 #endif 172 173 /* 174 * List of special file that we should never serve. 175 */ 176 struct { 177 const char *file; 178 const char *name; 179 } specials[] = { 180 { REDIRECT_FILE, "rejected redirect request" }, 181 { ABSREDIRECT_FILE, "rejected absredirect request" }, 182 { REMAP_FILE, "rejected remap request" }, 183 { AUTH_FILE, "rejected authfile request" }, 184 { NULL, NULL }, 185 }; 186 187 volatile sig_atomic_t bozo_timeout_hit; 188 189 /* 190 * check there's enough space in the prefs and names arrays. 191 */ 192 static int 193 size_arrays(bozohttpd_t *httpd, bozoprefs_t *bozoprefs, size_t needed) 194 { 195 size_t len = sizeof(char *) * needed; 196 197 if (bozoprefs->size == 0) { 198 /* only get here first time around */ 199 bozoprefs->name = bozomalloc(httpd, len); 200 bozoprefs->value = bozomalloc(httpd, len); 201 } else if (bozoprefs->count == bozoprefs->size) { 202 /* only uses 'needed' when filled array */ 203 bozoprefs->name = bozorealloc(httpd, bozoprefs->name, len); 204 bozoprefs->value = bozorealloc(httpd, bozoprefs->value, len); 205 } 206 207 bozoprefs->size = needed; 208 return 1; 209 } 210 211 static ssize_t 212 findvar(bozoprefs_t *bozoprefs, const char *name) 213 { 214 size_t i; 215 216 for (i = 0; i < bozoprefs->count; i++) 217 if (strcmp(bozoprefs->name[i], name) == 0) 218 return (ssize_t)i; 219 return -1; 220 } 221 222 int 223 bozo_set_pref(bozohttpd_t *httpd, bozoprefs_t *bozoprefs, 224 const char *name, const char *value) 225 { 226 ssize_t i; 227 228 if ((i = findvar(bozoprefs, name)) < 0) { 229 /* add the element to the array */ 230 if (!size_arrays(httpd, bozoprefs, bozoprefs->size + 15)) 231 return 0; 232 i = bozoprefs->count++; 233 bozoprefs->name[i] = bozostrdup(httpd, NULL, name); 234 } else { 235 /* replace the element in the array */ 236 free(bozoprefs->value[i]); 237 } 238 bozoprefs->value[i] = bozostrdup(httpd, NULL, value); 239 return 1; 240 } 241 242 static void 243 bozo_clear_prefs(bozoprefs_t *prefs) 244 { 245 size_t i; 246 247 for (i = 0; i < prefs->count; i++) { 248 free(prefs->name[i]); 249 free(prefs->value[i]); 250 } 251 252 free(prefs->name); 253 free(prefs->value); 254 } 255 256 /* 257 * get a variable's value, or NULL 258 */ 259 char * 260 bozo_get_pref(bozoprefs_t *bozoprefs, const char *name) 261 { 262 ssize_t i; 263 264 i = findvar(bozoprefs, name); 265 return i < 0 ? NULL : bozoprefs->value[i]; 266 } 267 268 char * 269 bozo_http_date(char *date, size_t datelen) 270 { 271 struct tm *tm; 272 time_t now; 273 274 /* Sun, 06 Nov 1994 08:49:37 GMT */ 275 now = time(NULL); 276 tm = gmtime(&now); /* HTTP/1.1 spec rev 06 sez GMT only */ 277 strftime(date, datelen, "%a, %d %b %Y %H:%M:%S GMT", tm); 278 return date; 279 } 280 281 /* 282 * convert "in" into the three parts of a request (first line). 283 * we allocate into file and query, but return pointers into 284 * "in" for proto and method. 285 */ 286 static void 287 parse_request(bozohttpd_t *httpd, char *in, char **method, char **file, 288 char **query, char **proto) 289 { 290 ssize_t len; 291 char *val; 292 293 USE_ARG(httpd); 294 debug((httpd, DEBUG_EXPLODING, "parse in: %s", in)); 295 *method = *file = *query = *proto = NULL; 296 297 len = (ssize_t)strlen(in); 298 val = bozostrnsep(&in, " \t\n\r", &len); 299 if (len < 1 || val == NULL || in == NULL) 300 return; 301 *method = val; 302 303 while (*in == ' ' || *in == '\t') 304 in++; 305 val = bozostrnsep(&in, " \t\n\r", &len); 306 if (len < 1) { 307 if (len == 0) 308 *file = val; 309 else 310 *file = in; 311 } else { 312 *file = val; 313 314 *query = strchr(*file, '?'); 315 if (*query) 316 *(*query)++ = '\0'; 317 318 if (in) { 319 while (*in && (*in == ' ' || *in == '\t')) 320 in++; 321 if (*in) 322 *proto = in; 323 } 324 } 325 326 /* allocate private copies */ 327 *file = bozostrdup(httpd, NULL, *file); 328 if (*query) 329 *query = bozostrdup(httpd, NULL, *query); 330 331 debug((httpd, DEBUG_FAT, 332 "url: method: \"%s\" file: \"%s\" query: \"%s\" proto: \"%s\"", 333 *method, *file, *query ? *query : "", *proto ? *proto : "")); 334 } 335 336 /* 337 * cleanup a bozo_httpreq_t after use 338 */ 339 void 340 bozo_clean_request(bozo_httpreq_t *request) 341 { 342 struct bozoheaders *hdr, *ohdr = NULL; 343 344 if (request == NULL) 345 return; 346 347 /* If SSL enabled cleanup SSL structure. */ 348 bozo_ssl_destroy(request->hr_httpd); 349 350 /* clean up request */ 351 free(request->hr_remotehost); 352 free(request->hr_remoteaddr); 353 free(request->hr_serverport); 354 free(request->hr_virthostname); 355 free(request->hr_file_free); 356 /* XXX this is gross */ 357 if (request->hr_file_free != request->hr_oldfile) 358 free(request->hr_oldfile); 359 else 360 free(request->hr_file); 361 free(request->hr_query); 362 free(request->hr_host); 363 bozo_user_free(request->hr_user); 364 bozo_auth_cleanup(request); 365 for (hdr = SIMPLEQ_FIRST(&request->hr_headers); hdr; 366 hdr = SIMPLEQ_NEXT(hdr, h_next)) { 367 free(hdr->h_value); 368 free(hdr->h_header); 369 free(ohdr); 370 ohdr = hdr; 371 } 372 free(ohdr); 373 ohdr = NULL; 374 for (hdr = SIMPLEQ_FIRST(&request->hr_replheaders); hdr; 375 hdr = SIMPLEQ_NEXT(hdr, h_next)) { 376 free(hdr->h_value); 377 free(hdr->h_header); 378 free(ohdr); 379 ohdr = hdr; 380 } 381 free(ohdr); 382 383 free(request); 384 } 385 386 /* 387 * send a HTTP/1.1 408 response if we timeout. 388 */ 389 /* ARGSUSED */ 390 static void 391 alarmer(int sig) 392 { 393 USE_ARG(sig); 394 bozo_timeout_hit = 1; 395 } 396 397 398 /* 399 * set a timeout for "ssl", "initial", "header", or "request". 400 */ 401 int 402 bozo_set_timeout(bozohttpd_t *httpd, bozoprefs_t *prefs, 403 const char *target, const char *val) 404 { 405 const char **cur, *timeouts[] = { 406 "ssl timeout", 407 "initial timeout", 408 "header timeout", 409 "request timeout", 410 NULL, 411 }; 412 /* adjust minlen if more timeouts appear with conflicting names */ 413 const size_t minlen = 1; 414 size_t len = strlen(target); 415 416 for (cur = timeouts; len >= minlen && *cur; cur++) { 417 if (strncmp(target, *cur, len) == 0) { 418 bozo_set_pref(httpd, prefs, *cur, val); 419 return 0; 420 } 421 } 422 return 1; 423 } 424 425 /* 426 * a list of header quirks: currently, a list of headers that 427 * can't be folded into a single line. 428 */ 429 const char *header_quirks[] = { "WWW-Authenticate", NULL }; 430 431 /* 432 * add or merge this header (val: str) into the requests list 433 */ 434 static bozoheaders_t * 435 addmerge_header(bozo_httpreq_t *request, struct qheaders *headers, 436 const char *val, const char *str, ssize_t len) 437 { 438 struct bozohttpd_t *httpd = request->hr_httpd; 439 struct bozoheaders *hdr = NULL; 440 const char **quirk; 441 442 USE_ARG(len); 443 for (quirk = header_quirks; *quirk; quirk++) 444 if (strcasecmp(*quirk, val) == 0) 445 break; 446 447 if (*quirk == NULL) { 448 /* do we exist already? */ 449 SIMPLEQ_FOREACH(hdr, headers, h_next) { 450 if (strcasecmp(val, hdr->h_header) == 0) 451 break; 452 } 453 } 454 455 if (hdr) { 456 /* yup, merge it in */ 457 char *nval; 458 459 bozoasprintf(httpd, &nval, "%s, %s", hdr->h_value, str); 460 free(hdr->h_value); 461 hdr->h_value = nval; 462 } else { 463 /* nope, create a new one */ 464 465 hdr = bozomalloc(httpd, sizeof *hdr); 466 hdr->h_header = bozostrdup(httpd, request, val); 467 if (str && *str) 468 hdr->h_value = bozostrdup(httpd, request, str); 469 else 470 hdr->h_value = bozostrdup(httpd, request, " "); 471 472 SIMPLEQ_INSERT_TAIL(headers, hdr, h_next); 473 request->hr_nheaders++; 474 } 475 476 return hdr; 477 } 478 479 bozoheaders_t * 480 addmerge_reqheader(bozo_httpreq_t *request, const char *val, const char *str, 481 ssize_t len) 482 { 483 484 return addmerge_header(request, &request->hr_headers, val, str, len); 485 } 486 487 bozoheaders_t * 488 addmerge_replheader(bozo_httpreq_t *request, const char *val, const char *str, 489 ssize_t len) 490 { 491 492 return addmerge_header(request, &request->hr_replheaders, 493 val, str, len); 494 } 495 496 /* 497 * as the prototype string is not constant (eg, "HTTP/1.1" is equivalent 498 * to "HTTP/001.01"), we MUST parse this. 499 */ 500 static int 501 process_proto(bozo_httpreq_t *request, const char *proto) 502 { 503 struct bozohttpd_t *httpd = request->hr_httpd; 504 char majorstr[16], *minorstr; 505 int majorint, minorint; 506 507 if (proto == NULL) { 508 got_proto_09: 509 request->hr_proto = httpd->consts.http_09; 510 debug((httpd, DEBUG_FAT, "request %s is http/0.9", 511 request->hr_file)); 512 return 0; 513 } 514 515 if (strncasecmp(proto, "HTTP/", 5) != 0) 516 goto bad; 517 strncpy(majorstr, proto + 5, sizeof(majorstr)-1); 518 majorstr[sizeof(majorstr)-1] = 0; 519 minorstr = strchr(majorstr, '.'); 520 if (minorstr == NULL) 521 goto bad; 522 *minorstr++ = 0; 523 524 majorint = atoi(majorstr); 525 minorint = atoi(minorstr); 526 527 switch (majorint) { 528 case 0: 529 if (minorint != 9) 530 break; 531 goto got_proto_09; 532 case 1: 533 if (minorint == 0) 534 request->hr_proto = httpd->consts.http_10; 535 else if (minorint == 1) 536 request->hr_proto = httpd->consts.http_11; 537 else 538 break; 539 540 debug((httpd, DEBUG_FAT, "request %s is %s", 541 request->hr_file, request->hr_proto)); 542 SIMPLEQ_INIT(&request->hr_headers); 543 request->hr_nheaders = 0; 544 return 0; 545 } 546 bad: 547 return bozo_http_error(httpd, 404, NULL, "unknown prototype"); 548 } 549 550 /* 551 * process each type of HTTP method, setting this HTTP requests 552 * method type. 553 */ 554 static struct method_map { 555 const char *name; 556 int type; 557 } method_map[] = { 558 { "GET", HTTP_GET, }, 559 { "POST", HTTP_POST, }, 560 { "HEAD", HTTP_HEAD, }, 561 #if 0 /* other non-required http/1.1 methods */ 562 { "OPTIONS", HTTP_OPTIONS, }, 563 { "PUT", HTTP_PUT, }, 564 { "DELETE", HTTP_DELETE, }, 565 { "TRACE", HTTP_TRACE, }, 566 { "CONNECT", HTTP_CONNECT, }, 567 #endif 568 { NULL, 0, }, 569 }; 570 571 static int 572 process_method(bozo_httpreq_t *request, const char *method) 573 { 574 struct bozohttpd_t *httpd = request->hr_httpd; 575 struct method_map *mmp; 576 577 if (request->hr_proto == httpd->consts.http_11) 578 request->hr_allow = "GET, HEAD, POST"; 579 580 for (mmp = method_map; mmp->name; mmp++) 581 if (strcasecmp(method, mmp->name) == 0) { 582 request->hr_method = mmp->type; 583 request->hr_methodstr = mmp->name; 584 return 0; 585 } 586 587 return bozo_http_error(httpd, 404, request, "unknown method"); 588 } 589 590 /* check header byte count */ 591 static int 592 bozo_got_header_length(bozo_httpreq_t *request, size_t len) 593 { 594 595 if (len > BOZO_HEADERS_MAX_SIZE - request->hr_header_bytes) 596 return bozo_http_error(request->hr_httpd, 413, request, 597 "too many headers"); 598 599 request->hr_header_bytes += len; 600 601 return 0; 602 } 603 604 /* 605 * This function reads a http request from stdin, returning a pointer to a 606 * bozo_httpreq_t structure, describing the request. 607 */ 608 bozo_httpreq_t * 609 bozo_read_request(bozohttpd_t *httpd) 610 { 611 struct sigaction sa; 612 char *str, *val, *method, *file, *proto, *query; 613 char *host, *addr, *port; 614 char bufport[10]; 615 char hbuf[NI_MAXHOST], abuf[NI_MAXHOST]; 616 struct sockaddr_storage ss; 617 ssize_t len; 618 int line = 0; 619 socklen_t slen; 620 bozo_httpreq_t *request; 621 struct timespec ots, ts; 622 623 /* 624 * if we're in daemon mode, bozo_daemon_fork() will return here twice 625 * for each call. once in the child, returning 0, and once in the 626 * parent, returning 1 for each child. 627 */ 628 if (bozo_daemon_fork(httpd)) 629 return NULL; 630 631 request = bozomalloc(httpd, sizeof(*request)); 632 memset(request, 0, sizeof(*request)); 633 request->hr_httpd = httpd; 634 request->hr_allow = request->hr_host = NULL; 635 request->hr_content_type = request->hr_content_length = NULL; 636 request->hr_range = NULL; 637 request->hr_last_byte_pos = -1; 638 request->hr_if_modified_since = NULL; 639 request->hr_virthostname = NULL; 640 request->hr_file_free = NULL; 641 request->hr_file = NULL; 642 request->hr_oldfile = NULL; 643 SIMPLEQ_INIT(&request->hr_replheaders); 644 bozo_auth_init(request); 645 646 slen = sizeof(ss); 647 if (getpeername(0, (struct sockaddr *)(void *)&ss, &slen) < 0) 648 host = addr = NULL; 649 else { 650 if (getnameinfo((struct sockaddr *)(void *)&ss, slen, 651 abuf, sizeof abuf, NULL, 0, NI_NUMERICHOST) == 0) 652 addr = abuf; 653 else 654 addr = NULL; 655 if (httpd->numeric == 0 && 656 getnameinfo((struct sockaddr *)(void *)&ss, slen, 657 hbuf, sizeof hbuf, NULL, 0, 0) == 0) 658 host = hbuf; 659 else 660 host = NULL; 661 } 662 if (host != NULL) 663 request->hr_remotehost = bozostrdup(httpd, request, host); 664 if (addr != NULL) 665 request->hr_remoteaddr = bozostrdup(httpd, request, addr); 666 slen = sizeof(ss); 667 668 /* 669 * Override the bound port from the request value, so it works even 670 * if passed through a proxy that doesn't rewrite the port. 671 */ 672 port = NULL; 673 if (httpd->bindport) { 674 if (strcmp(httpd->bindport, BOZO_HTTP_PORT) != 0) 675 port = httpd->bindport; 676 } else if (getsockname(0, (struct sockaddr *)(void *)&ss, &slen) == 0 && 677 getnameinfo((struct sockaddr *)(void *)&ss, slen, NULL, 0, 678 bufport, sizeof bufport, NI_NUMERICSERV) == 0) 679 port = bufport; 680 if (port != NULL) 681 request->hr_serverport = bozostrdup(httpd, request, port); 682 683 /* 684 * setup a timer to make sure the request is not hung 685 */ 686 sa.sa_handler = alarmer; 687 sigemptyset(&sa.sa_mask); 688 sigaddset(&sa.sa_mask, SIGALRM); 689 sa.sa_flags = 0; 690 sigaction(SIGALRM, &sa, NULL); 691 692 if (clock_gettime(CLOCK_MONOTONIC, &ots) != 0) { 693 bozo_http_error(httpd, 500, NULL, "clock_gettime failed"); 694 goto cleanup; 695 } 696 697 /* 698 * now to try to setup SSL, and upon failure parent can signal the 699 * caller there was no request to process and it will wait for 700 * another. 701 */ 702 if (bozo_ssl_accept(httpd)) 703 return NULL; 704 705 alarm(httpd->initial_timeout); 706 while ((str = bozodgetln(httpd, STDIN_FILENO, &len, bozo_read)) != NULL) { 707 alarm(0); 708 709 if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) { 710 bozo_http_error(httpd, 500, NULL, "clock_gettime failed"); 711 goto cleanup; 712 } 713 /* 714 * don't timeout if old tv_sec is not more than current 715 * tv_sec, or if current tv_sec is less than the request 716 * timeout (these shouldn't happen, but the first could 717 * if monotonic time is not available.) 718 * 719 * the other timeout and header size checks should ensure 720 * that even if time it set backwards or forwards a very 721 * long way, timeout will eventually happen, even if this 722 * one fails. 723 */ 724 if (ts.tv_sec > ots.tv_sec && 725 ts.tv_sec > httpd->request_timeout && 726 ts.tv_sec - httpd->request_timeout > ots.tv_sec) 727 bozo_timeout_hit = 1; 728 729 if (bozo_timeout_hit) { 730 bozo_http_error(httpd, 408, NULL, "request timed out"); 731 goto cleanup; 732 } 733 line++; 734 735 if (line == 1) { 736 if (len < 1) { 737 bozo_http_error(httpd, 404, NULL, "null method"); 738 goto cleanup; 739 } 740 bozowarn(httpd, 741 "got request ``%s'' from host %s to port %s", 742 str, 743 host ? host : addr ? addr : "<local>", 744 port ? port : "<stdin>"); 745 746 /* we allocate return space in file and query only */ 747 parse_request(httpd, str, &method, &file, &query, &proto); 748 request->hr_file_free = request->hr_file = file; 749 request->hr_query = query; 750 if (method == NULL) { 751 bozo_http_error(httpd, 404, NULL, "null method"); 752 goto cleanup; 753 } 754 if (file == NULL) { 755 bozo_http_error(httpd, 404, NULL, "null file"); 756 goto cleanup; 757 } 758 759 /* 760 * note that we parse the proto first, so that we 761 * can more properly parse the method and the url. 762 */ 763 764 if (process_proto(request, proto) || 765 process_method(request, method)) { 766 goto cleanup; 767 } 768 769 debug((httpd, DEBUG_FAT, "got file \"%s\" query \"%s\"", 770 request->hr_file, 771 request->hr_query ? request->hr_query : "<none>")); 772 773 /* http/0.9 has no header processing */ 774 if (request->hr_proto == httpd->consts.http_09) 775 break; 776 } else { /* incoming headers */ 777 bozoheaders_t *hdr; 778 779 if (*str == '\0') 780 break; 781 782 val = bozostrnsep(&str, ":", &len); 783 debug((httpd, DEBUG_EXPLODING, "read_req2: after " 784 "bozostrnsep: str `%s' val `%s'", 785 str ? str : "<null>", val ? val : "<null>")); 786 if (val == NULL || len == -1) { 787 bozo_http_error(httpd, 404, request, "no header"); 788 goto cleanup; 789 } 790 if (str == NULL) { 791 bozo_http_error(httpd, 404, request, 792 "malformed header"); 793 goto cleanup; 794 } 795 while (*str == ' ' || *str == '\t') 796 len--, str++; 797 while (*val == ' ' || *val == '\t') 798 val++; 799 800 if (bozo_got_header_length(request, len)) 801 goto cleanup; 802 803 if (bozo_auth_check_headers(request, val, str, len)) 804 goto next_header; 805 806 hdr = addmerge_reqheader(request, val, str, len); 807 808 if (strcasecmp(hdr->h_header, "content-type") == 0) 809 request->hr_content_type = hdr->h_value; 810 else if (strcasecmp(hdr->h_header, "content-length") == 0) 811 request->hr_content_length = hdr->h_value; 812 else if (strcasecmp(hdr->h_header, "host") == 0) { 813 if (request->hr_host) { 814 /* RFC 7230 (HTTP/1.1): 5.4 */ 815 bozo_http_error(httpd, 400, request, 816 "Only allow one Host: header"); 817 goto cleanup; 818 } 819 request->hr_host = bozostrdup(httpd, request, 820 hdr->h_value); 821 } 822 /* RFC 2616 (HTTP/1.1): 14.20 */ 823 else if (strcasecmp(hdr->h_header, "expect") == 0) { 824 bozo_http_error(httpd, 417, request, 825 "we don't support Expect:"); 826 goto cleanup; 827 } 828 else if (strcasecmp(hdr->h_header, "referrer") == 0 || 829 strcasecmp(hdr->h_header, "referer") == 0) 830 request->hr_referrer = hdr->h_value; 831 else if (strcasecmp(hdr->h_header, "range") == 0) 832 request->hr_range = hdr->h_value; 833 else if (strcasecmp(hdr->h_header, 834 "if-modified-since") == 0) 835 request->hr_if_modified_since = hdr->h_value; 836 else if (strcasecmp(hdr->h_header, 837 "accept-encoding") == 0) 838 request->hr_accept_encoding = hdr->h_value; 839 840 debug((httpd, DEBUG_FAT, "adding header %s: %s", 841 hdr->h_header, hdr->h_value)); 842 } 843 next_header: 844 alarm(httpd->header_timeout); 845 } 846 if (str == NULL) { 847 bozo_http_error(httpd, 413, request, "request too large"); 848 goto cleanup; 849 } 850 851 /* now, clear it all out */ 852 alarm(0); 853 signal(SIGALRM, SIG_DFL); 854 855 /* RFC1945, 8.3 */ 856 if (request->hr_method == HTTP_POST && 857 request->hr_content_length == NULL) { 858 bozo_http_error(httpd, 400, request, "missing content length"); 859 goto cleanup; 860 } 861 862 /* RFC 2616 (HTTP/1.1), 14.23 & 19.6.1.1 */ 863 if (request->hr_proto == httpd->consts.http_11 && 864 /*(strncasecmp(request->hr_file, "http://", 7) != 0) &&*/ 865 request->hr_host == NULL) { 866 bozo_http_error(httpd, 400, request, "missing Host header"); 867 goto cleanup; 868 } 869 870 if (request->hr_range != NULL) { 871 debug((httpd, DEBUG_FAT, "hr_range: %s", request->hr_range)); 872 /* support only simple ranges %d- and %d-%d */ 873 if (strchr(request->hr_range, ',') == NULL) { 874 const char *rstart, *dash; 875 876 rstart = strchr(request->hr_range, '='); 877 if (rstart != NULL) { 878 rstart++; 879 dash = strchr(rstart, '-'); 880 if (dash != NULL && dash != rstart) { 881 dash++; 882 request->hr_have_range = 1; 883 request->hr_first_byte_pos = 884 strtoll(rstart, NULL, 10); 885 if (request->hr_first_byte_pos < 0) 886 request->hr_first_byte_pos = 0; 887 if (*dash != '\0') { 888 request->hr_last_byte_pos = 889 strtoll(dash, NULL, 10); 890 if (request->hr_last_byte_pos < 0) 891 request->hr_last_byte_pos = -1; 892 } 893 } 894 } 895 } 896 } 897 898 debug((httpd, DEBUG_FAT, "bozo_read_request returns url %s in request", 899 request->hr_file)); 900 return request; 901 902 cleanup: 903 bozo_clean_request(request); 904 905 return NULL; 906 } 907 908 static int 909 mmap_and_write_part(bozohttpd_t *httpd, int fd, off_t first_byte_pos, size_t sz) 910 { 911 size_t mappedsz, wroffset; 912 off_t mappedoffset; 913 char *addr; 914 void *mappedaddr; 915 916 /* 917 * we need to ensure that both the size *and* offset arguments to 918 * mmap() are page-aligned. our formala for this is: 919 * 920 * input offset: first_byte_pos 921 * input size: sz 922 * 923 * mapped offset = page align truncate (input offset) 924 * mapped size = 925 * page align extend (input offset - mapped offset + input size) 926 * write offset = input offset - mapped offset 927 * 928 * we use the write offset in all writes 929 */ 930 mappedoffset = first_byte_pos & ~((off_t)httpd->page_size - 1); 931 mappedsz = (size_t) 932 (first_byte_pos - mappedoffset + sz + httpd->page_size - 1) & 933 ~(httpd->page_size - 1); 934 wroffset = (size_t)(first_byte_pos - mappedoffset); 935 936 addr = mmap(0, mappedsz, PROT_READ, MAP_SHARED, fd, mappedoffset); 937 if (addr == MAP_FAILED) { 938 bozowarn(httpd, "mmap failed: %s", strerror(errno)); 939 return -1; 940 } 941 mappedaddr = addr; 942 943 #ifdef MADV_SEQUENTIAL 944 (void)madvise(addr, sz, MADV_SEQUENTIAL); 945 #endif 946 while (sz > BOZO_WRSZ) { 947 if (bozo_write(httpd, STDOUT_FILENO, addr + wroffset, 948 BOZO_WRSZ) != BOZO_WRSZ) { 949 bozowarn(httpd, "write failed: %s", strerror(errno)); 950 goto out; 951 } 952 debug((httpd, DEBUG_OBESE, "wrote %d bytes", BOZO_WRSZ)); 953 sz -= BOZO_WRSZ; 954 addr += BOZO_WRSZ; 955 } 956 if (sz && (size_t)bozo_write(httpd, STDOUT_FILENO, addr + wroffset, 957 sz) != sz) { 958 bozowarn(httpd, "final write failed: %s", strerror(errno)); 959 goto out; 960 } 961 debug((httpd, DEBUG_OBESE, "wrote %d bytes", (int)sz)); 962 out: 963 if (munmap(mappedaddr, mappedsz) < 0) { 964 bozowarn(httpd, "munmap failed"); 965 return -1; 966 } 967 968 return 0; 969 } 970 971 static int 972 parse_http_date(const char *val, time_t *timestamp) 973 { 974 char *remainder; 975 struct tm tm; 976 977 if ((remainder = strptime(val, "%a, %d %b %Y %T GMT", &tm)) == NULL && 978 (remainder = strptime(val, "%a, %d-%b-%y %T GMT", &tm)) == NULL && 979 (remainder = strptime(val, "%a %b %d %T %Y", &tm)) == NULL) 980 return 0; /* Invalid HTTP date format */ 981 982 if (*remainder) 983 return 0; /* No trailing garbage */ 984 985 *timestamp = timegm(&tm); 986 return 1; 987 } 988 989 /* 990 * given an url, encode it ala rfc 3986. ie, escape ? and friends. 991 * note that this function returns a static buffer, and thus needs 992 * to be updated for any sort of parallel processing. escape only 993 * chosen characters for absolute redirects 994 */ 995 char * 996 bozo_escape_rfc3986(bozohttpd_t *httpd, const char *url, int absolute) 997 { 998 static char *buf; 999 static size_t buflen = 0; 1000 size_t len; 1001 const char *s; 1002 char *d; 1003 1004 len = strlen(url); 1005 if (buflen < len * 3 + 1) { 1006 buflen = len * 3 + 1; 1007 buf = bozorealloc(httpd, buf, buflen); 1008 } 1009 1010 for (s = url, d = buf; *s;) { 1011 if (*s & 0x80) 1012 goto encode_it; 1013 switch (*s) { 1014 case ':': 1015 case '?': 1016 case '#': 1017 case '[': 1018 case ']': 1019 case '@': 1020 case '!': 1021 case '$': 1022 case '&': 1023 case '\'': 1024 case '(': 1025 case ')': 1026 case '*': 1027 case '+': 1028 case ',': 1029 case ';': 1030 case '=': 1031 case '%': 1032 case '"': 1033 if (absolute) 1034 goto leave_it; 1035 /*FALLTHROUGH*/ 1036 case '\n': 1037 case '\r': 1038 case ' ': 1039 encode_it: 1040 snprintf(d, 4, "%%%02X", (unsigned char)*s++); 1041 d += 3; 1042 break; 1043 default: 1044 leave_it: 1045 *d++ = *s++; 1046 break; 1047 } 1048 } 1049 *d = 0; 1050 1051 return buf; 1052 } 1053 1054 /* 1055 * do automatic redirection -- if there are query parameters or userdir for 1056 * the URL we will tack these on to the new (redirected) URL. 1057 */ 1058 static void 1059 handle_redirect(bozo_httpreq_t *request, const char *url, int absolute) 1060 { 1061 bozohttpd_t *httpd = request->hr_httpd; 1062 char *finalurl, *urlbuf; 1063 #ifndef NO_USER_SUPPORT 1064 char *userbuf; 1065 #endif /* !NO_USER_SUPPORT */ 1066 char portbuf[20]; 1067 const char *scheme, *query, *quest; 1068 const char *hostname = BOZOHOST(httpd, request); 1069 int absproto = 0; /* absolute redirect provides own schema */ 1070 1071 if (url == NULL) { 1072 bozoasprintf(httpd, &urlbuf, "/%s/", request->hr_file); 1073 url = urlbuf; 1074 } else 1075 urlbuf = NULL; 1076 1077 #ifndef NO_USER_SUPPORT 1078 if (request->hr_user && !absolute) { 1079 bozoasprintf(httpd, &userbuf, "/~%s%s", request->hr_user, url); 1080 url = userbuf; 1081 } else 1082 userbuf = NULL; 1083 #endif /* !NO_USER_SUPPORT */ 1084 1085 if (absolute) { 1086 const char *sep = NULL; 1087 const char *s; 1088 1089 /* 1090 * absolute redirect may specify own protocol i.e. to redirect 1091 * to another schema like https:// or ftp://. 1092 * Details: RFC 3986, section 3. 1093 */ 1094 1095 /* 1. check if url contains :// */ 1096 sep = strstr(url, "://"); 1097 1098 /* 1099 * RFC 3986, section 3.1: 1100 * scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) 1101 */ 1102 if (sep) { 1103 for (s = url; s != sep;) { 1104 if (!isalnum((unsigned char)*s) && 1105 *s != '+' && *s != '-' && *s != '.') 1106 break; 1107 if (++s == sep) { 1108 absproto = 1; 1109 } 1110 } 1111 } 1112 } 1113 1114 /* construct final redirection url */ 1115 1116 scheme = absproto ? "" : httpd->sslinfo ? "https://" : "http://"; 1117 1118 if (absolute) { 1119 hostname = ""; 1120 portbuf[0] = '\0'; 1121 } else { 1122 const char *defport = httpd->sslinfo ? BOZO_HTTPS_PORT : BOZO_HTTP_PORT; 1123 1124 if (request->hr_serverport && 1125 strcmp(request->hr_serverport, defport) != 0) 1126 snprintf(portbuf, sizeof(portbuf), ":%s", 1127 request->hr_serverport); 1128 else 1129 portbuf[0] = '\0'; 1130 } 1131 1132 url = bozo_escape_rfc3986(httpd, url, absolute); 1133 1134 if (request->hr_query && strlen(request->hr_query)) { 1135 query = request->hr_query; 1136 quest = "?"; 1137 } else { 1138 query = quest = ""; 1139 } 1140 1141 bozoasprintf(httpd, &finalurl, "%s%s%s%s%s%s", 1142 scheme, hostname, portbuf, url, quest, query); 1143 1144 bozowarn(httpd, "redirecting %s", finalurl); 1145 debug((httpd, DEBUG_FAT, "redirecting %s", finalurl)); 1146 1147 bozo_printf(httpd, "%s 301 Document Moved\r\n", request->hr_proto); 1148 if (request->hr_proto != httpd->consts.http_09) 1149 bozo_print_header(request, NULL, "text/html", NULL); 1150 if (request->hr_proto != httpd->consts.http_09) 1151 bozo_printf(httpd, "Location: %s\r\n", finalurl); 1152 bozo_printf(httpd, "\r\n"); 1153 if (request->hr_method == HTTP_HEAD) 1154 goto head; 1155 bozo_printf(httpd, "<html><head><title>Document Moved</title></head>\n"); 1156 bozo_printf(httpd, "<body><h1>Document Moved</h1>\n"); 1157 bozo_printf(httpd, "This document had moved <a href=\"%s\">here</a>\n", 1158 finalurl); 1159 bozo_printf(httpd, "</body></html>\n"); 1160 head: 1161 bozo_flush(httpd, stdout); 1162 free(urlbuf); 1163 free(finalurl); 1164 #ifndef NO_USER_SUPPORT 1165 free(userbuf); 1166 #endif /* !NO_USER_SUPPORT */ 1167 } 1168 1169 /* 1170 * Like strncmp(), but s_esc may contain characters escaped by \. 1171 * The len argument does not include the backslashes used for escaping, 1172 * that is: it gives the raw len, after unescaping the string. 1173 */ 1174 static int 1175 esccmp(const char *s_plain, const char *s_esc, size_t len) 1176 { 1177 bool esc = false; 1178 1179 while (len) { 1180 if (!esc && *s_esc == '\\') { 1181 esc = true; 1182 s_esc++; 1183 continue; 1184 } 1185 esc = false; 1186 if (*s_plain == 0 || *s_esc == 0 || *s_plain != *s_esc) 1187 return *s_esc - *s_plain; 1188 s_esc++; 1189 s_plain++; 1190 len--; 1191 } 1192 return 0; 1193 } 1194 1195 /* 1196 * Check if the request refers to a uri that is mapped via a .bzremap. 1197 * We have /requested/path:/re/mapped/to/this.html lines in there, 1198 * and the : separator may be use in the left hand side escaped with 1199 * \ to encode a path containig a : character. 1200 */ 1201 static void 1202 check_remap(bozo_httpreq_t *request) 1203 { 1204 bozohttpd_t *httpd = request->hr_httpd; 1205 char *file = request->hr_file, *newfile; 1206 void *fmap; 1207 const char *replace = NULL, *map_to = NULL, *p; 1208 struct stat st; 1209 int mapfile; 1210 size_t avail, len, rlen, reqlen, num_esc = 0; 1211 bool escaped = false; 1212 1213 mapfile = open(REMAP_FILE, O_RDONLY, 0); 1214 if (mapfile == -1) 1215 return; 1216 debug((httpd, DEBUG_FAT, "remap file found")); 1217 if (fstat(mapfile, &st) == -1) { 1218 bozowarn(httpd, "could not stat " REMAP_FILE ", errno: %d", 1219 errno); 1220 goto out; 1221 } 1222 1223 fmap = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, mapfile, 0); 1224 if (fmap == MAP_FAILED) { 1225 bozowarn(httpd, "could not mmap " REMAP_FILE ", error %d", 1226 errno); 1227 goto out; 1228 } 1229 reqlen = strlen(file); 1230 for (p = fmap, avail = st.st_size; avail; ) { 1231 /* 1232 * We have lines like: 1233 * /this/url:/replacement/that/url 1234 * If we find a matching left hand side, replace will point 1235 * to it and len will be its length. map_to will point to 1236 * the right hand side and rlen wil be its length. 1237 * If we have no match, both pointers will be NULL. 1238 */ 1239 1240 /* skip empty lines */ 1241 while ((*p == '\r' || *p == '\n') && avail) { 1242 p++; 1243 avail--; 1244 } 1245 replace = p; 1246 escaped = false; 1247 while (avail) { 1248 if (*p == '\r' || *p == '\n') 1249 break; 1250 if (!escaped && *p == ':') 1251 break; 1252 if (escaped) { 1253 escaped = false; 1254 num_esc++; 1255 } else if (*p == '\\') { 1256 escaped = true; 1257 } 1258 p++; 1259 avail--; 1260 } 1261 if (!avail || *p != ':') { 1262 replace = NULL; 1263 map_to = NULL; 1264 break; 1265 } 1266 len = p - replace - num_esc; 1267 /* 1268 * reqlen < len: the left hand side is too long, can't be a 1269 * match 1270 * reqlen == len: full string has to match 1271 * reqlen > len: make sure there is a path separator at 'len' 1272 * avail < 2: we are at eof, missing right hand side 1273 */ 1274 if (avail < 2 || reqlen < len || 1275 (reqlen == len && esccmp(file, replace, len) != 0) || 1276 (reqlen > len && (file[len] != '/' || 1277 esccmp(file, replace, len) != 0))) { 1278 1279 /* non-match, skip to end of line and continue */ 1280 while (*p != '\r' && *p != '\n' && avail) { 1281 p++; 1282 avail--; 1283 } 1284 replace = NULL; 1285 map_to = NULL; 1286 continue; 1287 } 1288 p++; 1289 avail--; 1290 1291 /* found a match, parse the target */ 1292 map_to = p; 1293 while (*p != '\r' && *p != '\n' && avail) { 1294 p++; 1295 avail--; 1296 } 1297 rlen = p - map_to; 1298 break; 1299 } 1300 1301 if (replace && map_to) { 1302 newfile = bozomalloc(httpd, strlen(file) + rlen - len + 1); 1303 memcpy(newfile, map_to, rlen); 1304 strcpy(newfile+rlen, file + len); 1305 debug((httpd, DEBUG_NORMAL, "remapping found '%s'", 1306 newfile)); 1307 free(request->hr_file_free); 1308 request->hr_file_free = request->hr_file = newfile; 1309 } 1310 1311 munmap(fmap, st.st_size); 1312 out: 1313 close(mapfile); 1314 } 1315 1316 /* 1317 * deal with virtual host names; we do this: 1318 * if we have a virtual path root (httpd->virtbase), and we are given a 1319 * virtual host spec (Host: ho.st or http://ho.st/), see if this 1320 * directory exists under httpd->virtbase. if it does, use this as the 1321 # new slashdir. 1322 */ 1323 static int 1324 check_virtual(bozo_httpreq_t *request) 1325 { 1326 bozohttpd_t *httpd = request->hr_httpd; 1327 char *file = request->hr_file, *s; 1328 size_t len; 1329 1330 /* 1331 * convert http://virtual.host/ to request->hr_host 1332 */ 1333 debug((httpd, DEBUG_OBESE, 1334 "checking for http:// virtual host in '%s'", file)); 1335 if (strncasecmp(file, "http://", 7) == 0) { 1336 /* we would do virtual hosting here? */ 1337 file += 7; 1338 /* RFC 2616 (HTTP/1.1), 5.2: URI takes precedence over Host: */ 1339 free(request->hr_host); 1340 request->hr_host = bozostrdup(httpd, request, file); 1341 if ((s = strchr(request->hr_host, '/')) != NULL) 1342 *s = '\0'; 1343 s = strchr(file, '/'); 1344 free(request->hr_file_free); 1345 request->hr_file_free = request->hr_file = 1346 bozostrdup(httpd, request, s ? s : "/"); 1347 debug((httpd, DEBUG_OBESE, "got host '%s' file is now '%s'", 1348 request->hr_host, request->hr_file)); 1349 } else if (!request->hr_host) 1350 goto use_slashdir; 1351 1352 /* 1353 * canonicalise hr_host - that is, remove any :80. 1354 */ 1355 len = strlen(request->hr_host); 1356 if (len > 3 && 1357 strcmp(request->hr_host + len - 3, ":" BOZO_HTTP_PORT) == 0) { 1358 request->hr_host[len - 3] = '\0'; 1359 len = strlen(request->hr_host); 1360 } 1361 1362 if (!httpd->virtbase) { 1363 /* 1364 * if we don't use vhost support, then set virthostname if 1365 * user supplied Host header. It will be used for possible 1366 * redirections 1367 */ 1368 if (request->hr_host) { 1369 s = strrchr(request->hr_host, ':'); 1370 if (s != NULL) 1371 /* 1372 * truncate Host: as we want to copy it 1373 * without port part 1374 */ 1375 *s = '\0'; 1376 request->hr_virthostname = bozostrdup(httpd, request, 1377 request->hr_host); 1378 if (s != NULL) 1379 /* fix Host: again, if we truncated it */ 1380 *s = ':'; 1381 } 1382 goto use_slashdir; 1383 } 1384 1385 /* 1386 * ok, we have a virtual host, use opendir(3) to find a case 1387 * insensitive match for the virtual host we are asked for. 1388 * note that if the virtual host is the same as the master, 1389 * we don't need to do anything special. 1390 */ 1391 debug((httpd, DEBUG_OBESE, 1392 "check_virtual: checking host `%s' under httpd->virtbase `%s' " 1393 "for file `%s'", 1394 request->hr_host, httpd->virtbase, request->hr_file)); 1395 if (strncasecmp(httpd->virthostname, request->hr_host, len) != 0) { 1396 s = NULL; 1397 DIR *dirp; 1398 struct dirent *d; 1399 1400 if ((dirp = opendir(httpd->virtbase)) != NULL) { 1401 while ((d = readdir(dirp)) != NULL) { 1402 if (strcmp(d->d_name, ".") == 0 || 1403 strcmp(d->d_name, "..") == 0) { 1404 continue; 1405 } 1406 debug((httpd, DEBUG_OBESE, "looking at dir '%s'", 1407 d->d_name)); 1408 if (strcmp(d->d_name, request->hr_host) == 0) { 1409 /* found it, punch it */ 1410 debug((httpd, DEBUG_OBESE, "found it punch it")); 1411 request->hr_virthostname = 1412 bozostrdup(httpd, request, d->d_name); 1413 bozoasprintf(httpd, &s, "%s/%s", 1414 httpd->virtbase, 1415 request->hr_virthostname); 1416 break; 1417 } 1418 } 1419 closedir(dirp); 1420 } 1421 else { 1422 debug((httpd, DEBUG_FAT, "opendir %s failed: %s", 1423 httpd->virtbase, strerror(errno))); 1424 } 1425 if (s == 0) { 1426 if (httpd->unknown_slash) 1427 goto use_slashdir; 1428 return bozo_http_error(httpd, 404, request, 1429 "unknown URL"); 1430 } 1431 } else 1432 use_slashdir: 1433 s = httpd->slashdir; 1434 1435 /* 1436 * ok, nailed the correct slashdir, chdir to it 1437 */ 1438 if (chdir(s) < 0) 1439 return bozo_http_error(httpd, 404, request, 1440 "can't chdir to slashdir"); 1441 1442 /* 1443 * is there a mapping for this request? 1444 */ 1445 check_remap(request); 1446 1447 return 0; 1448 } 1449 1450 /* 1451 * checks to see if this request has a valid .bzredirect file. returns 1452 * 0 when no redirection happened, or 1 when handle_redirect() has been 1453 * called, -1 on error. 1454 */ 1455 static int 1456 check_bzredirect(bozo_httpreq_t *request) 1457 { 1458 bozohttpd_t *httpd = request->hr_httpd; 1459 struct stat sb; 1460 char dir[MAXPATHLEN], redir[MAXPATHLEN], redirpath[MAXPATHLEN + 1], 1461 path[MAXPATHLEN + 1]; 1462 char *basename, *finalredir; 1463 int rv, absolute; 1464 1465 /* 1466 * if this pathname is really a directory, but doesn't end in /, 1467 * use it as the directory to look for the redir file. 1468 */ 1469 if ((size_t)snprintf(dir, sizeof(dir), "%s", request->hr_file + 1) >= 1470 sizeof(dir)) { 1471 bozo_http_error(httpd, 404, request, "file path too long"); 1472 return -1; 1473 } 1474 debug((httpd, DEBUG_FAT, "check_bzredirect: dir %s", dir)); 1475 basename = strrchr(dir, '/'); 1476 1477 if ((!basename || basename[1] != '\0') && 1478 lstat(dir, &sb) == 0 && S_ISDIR(sb.st_mode)) { 1479 strcpy(path, dir); 1480 basename = dir; 1481 } else if (basename == NULL) { 1482 strcpy(path, "."); 1483 strcpy(dir, ""); 1484 basename = request->hr_file + 1; 1485 } else { 1486 *basename++ = '\0'; 1487 strcpy(path, dir); 1488 } 1489 if (bozo_check_special_files(request, basename, true)) 1490 return -1; 1491 1492 debug((httpd, DEBUG_FAT, "check_bzredirect: path %s", path)); 1493 1494 if ((size_t)snprintf(redir, sizeof(redir), "%s/%s", path, 1495 REDIRECT_FILE) >= sizeof(redir)) { 1496 return bozo_http_error(httpd, 404, request, 1497 "redirectfile path too long"); 1498 } 1499 if (lstat(redir, &sb) == 0) { 1500 if (!S_ISLNK(sb.st_mode)) 1501 return 0; 1502 absolute = 0; 1503 } else { 1504 if ((size_t)snprintf(redir, sizeof(redir), "%s/%s", path, 1505 ABSREDIRECT_FILE) >= sizeof(redir)) { 1506 bozo_http_error(httpd, 404, request, 1507 "redirectfile path too long"); 1508 return -1; 1509 } 1510 if (lstat(redir, &sb) < 0 || !S_ISLNK(sb.st_mode)) 1511 return 0; 1512 absolute = 1; 1513 } 1514 debug((httpd, DEBUG_FAT, "check_bzredirect: calling readlink")); 1515 rv = readlink(redir, redirpath, sizeof redirpath - 1); 1516 if (rv == -1 || rv == 0) { 1517 debug((httpd, DEBUG_FAT, "readlink failed")); 1518 return 0; 1519 } 1520 redirpath[rv] = '\0'; 1521 debug((httpd, DEBUG_FAT, "readlink returned \"%s\"", redirpath)); 1522 1523 /* check if we need authentication */ 1524 snprintf(path, sizeof(path), "%s/", dir); 1525 if (bozo_auth_check(request, path)) 1526 return 1; 1527 1528 /* now we have the link pointer, redirect to the real place */ 1529 if (!absolute && redirpath[0] != '/') { 1530 if ((size_t)snprintf(finalredir = redir, sizeof(redir), "%s%s/%s", 1531 (strlen(dir) > 0 ? "/" : ""), dir, redirpath) >= sizeof(redir)) { 1532 bozo_http_error(httpd, 404, request, 1533 "redirect path too long"); 1534 return -1; 1535 } 1536 } else 1537 finalredir = redirpath; 1538 1539 debug((httpd, DEBUG_FAT, "check_bzredirect: new redir %s", finalredir)); 1540 handle_redirect(request, finalredir, absolute); 1541 return 1; 1542 } 1543 1544 /* this fixes the %HH hack that RFC2396 requires. */ 1545 int 1546 bozo_decode_url_percent(bozo_httpreq_t *request, char *str) 1547 { 1548 bozohttpd_t *httpd = request->hr_httpd; 1549 char *s, *t, buf[3]; 1550 char *end; /* if end is not-zero, we don't translate beyond that */ 1551 1552 end = str + strlen(str); 1553 1554 /* fast forward to the first % */ 1555 if ((s = strchr(str, '%')) == NULL) 1556 return 0; 1557 1558 t = s; 1559 do { 1560 if (end && s >= end) { 1561 debug((httpd, DEBUG_EXPLODING, 1562 "fu_%%: past end, filling out..")); 1563 while (*s) 1564 *t++ = *s++; 1565 break; 1566 } 1567 if (&s[2] < end) 1568 debug((httpd, DEBUG_EXPLODING, 1569 "fu_%%: got s == %%, s[1]s[2] == %c%c", 1570 s[1], s[2])); 1571 else 1572 debug((httpd, DEBUG_EXPLODING, 1573 "fu_%%: got s == %%, s[1] == %c s[2] is not set", 1574 s[1])); 1575 if (s[1] == '\0' || s[2] == '\0') 1576 return bozo_http_error(httpd, 400, request, 1577 "percent hack missing two chars afterwards"); 1578 if (s[1] == '0' && s[2] == '0') 1579 return bozo_http_error(httpd, 404, request, 1580 "percent hack was %00"); 1581 if (s[1] == '2' && (s[2] == 'f' || s[2] == 'F')) 1582 return bozo_http_error(httpd, 404, request, 1583 "percent hack was %2f (/)"); 1584 1585 buf[0] = *++s; 1586 buf[1] = *++s; 1587 buf[2] = '\0'; 1588 s++; 1589 *t = (char)strtol(buf, NULL, 16); 1590 debug((httpd, DEBUG_EXPLODING, 1591 "fu_%%: strtol put '%02x' into *t", *t)); 1592 if (*t++ == '\0') 1593 return bozo_http_error(httpd, 400, request, 1594 "percent hack got a 0 back"); 1595 1596 while (*s && *s != '%') { 1597 if (end && s >= end) 1598 break; 1599 *t++ = *s++; 1600 } 1601 } while (*s); 1602 *t = '\0'; 1603 1604 debug((httpd, DEBUG_FAT, "bozo_decode_url_percent returns `%s'", 1605 request->hr_file)); 1606 1607 return 0; 1608 } 1609 1610 /* 1611 * transform_request does this: 1612 * - ``expand'' %20 crapola 1613 * - punt if it doesn't start with / 1614 * - look for "http://myname/" and deal with it. 1615 * - maybe call bozo_process_cgi() 1616 * - check for ~user and call bozo_user_transform() if so 1617 * - if the length > 1, check for trailing slash. if so, 1618 * add the index.html file 1619 * - if the length is 1, return the index.html file 1620 * - disallow anything ending up with a file starting 1621 * at "/" or having ".." in it. 1622 * - anything else is a really weird internal error 1623 * - returns malloced file to serve, if unhandled 1624 */ 1625 static int 1626 transform_request(bozo_httpreq_t *request, int *isindex) 1627 { 1628 bozohttpd_t *httpd = request->hr_httpd; 1629 char *file, *newfile = NULL; 1630 size_t len; 1631 1632 file = NULL; 1633 *isindex = 0; 1634 debug((httpd, DEBUG_FAT, "tf_req: file %s", request->hr_file)); 1635 1636 if (bozo_decode_url_percent(request, request->hr_file) || 1637 check_virtual(request)) 1638 goto bad_done; 1639 1640 file = request->hr_file; 1641 1642 if (file[0] != '/') { 1643 bozo_http_error(httpd, 404, request, "unknown URL"); 1644 goto bad_done; 1645 } 1646 1647 /* omit additional slashes at the beginning */ 1648 while (file[1] == '/') 1649 file++; 1650 1651 /* fix file provided by user as it's used in other handlers */ 1652 request->hr_file = file; 1653 1654 len = strlen(file); 1655 1656 #ifndef NO_USER_SUPPORT 1657 /* first of all expand user path */ 1658 if (len > 1 && httpd->enable_users && file[1] == '~') { 1659 if (file[2] == '\0') { 1660 bozo_http_error(httpd, 404, request, 1661 "missing username"); 1662 goto bad_done; 1663 } 1664 if (strchr(file + 2, '/') == NULL) { 1665 char *userredirecturl; 1666 1667 bozoasprintf(httpd, &userredirecturl, "%s/", file); 1668 handle_redirect(request, userredirecturl, 0); 1669 free(userredirecturl); 1670 return 0; 1671 } 1672 debug((httpd, DEBUG_FAT, "calling bozo_user_transform")); 1673 1674 if (!bozo_user_transform(request)) 1675 return 0; 1676 1677 file = request->hr_file; 1678 len = strlen(file); 1679 } 1680 #endif /* NO_USER_SUPPORT */ 1681 1682 1683 switch (check_bzredirect(request)) { 1684 case -1: 1685 goto bad_done; 1686 case 0: 1687 break; 1688 default: 1689 return 0; 1690 } 1691 1692 if (len > 1) { 1693 debug((httpd, DEBUG_FAT, "file[len-1] == %c", file[len-1])); 1694 if (file[len-1] == '/') { /* append index.html */ 1695 *isindex = 1; 1696 debug((httpd, DEBUG_FAT, "appending index.html")); 1697 newfile = bozomalloc(httpd, 1698 len + strlen(httpd->index_html) + 1); 1699 strcpy(newfile, file + 1); 1700 strcat(newfile, httpd->index_html); 1701 } else 1702 newfile = bozostrdup(httpd, request, file + 1); 1703 } else if (len == 1) { 1704 debug((httpd, DEBUG_EXPLODING, "tf_req: len == 1")); 1705 newfile = bozostrdup(httpd, request, httpd->index_html); 1706 *isindex = 1; 1707 } else { /* len == 0 ? */ 1708 bozo_http_error(httpd, 500, request, "request->hr_file is nul"); 1709 goto bad_done; 1710 } 1711 1712 if (newfile == NULL) { 1713 bozo_http_error(httpd, 500, request, "internal failure"); 1714 goto bad_done; 1715 } 1716 1717 /* 1718 * stop traversing outside our domain 1719 * 1720 * XXX true security only comes from our parent using chroot(2) 1721 * before execve(2)'ing us. or our own built in chroot(2) support. 1722 */ 1723 1724 debug((httpd, DEBUG_FAT, "newfile: %s", newfile)); 1725 1726 if (*newfile == '/' || strcmp(newfile, "..") == 0 || 1727 strstr(newfile, "/..") || strstr(newfile, "../")) { 1728 bozo_http_error(httpd, 403, request, "illegal request"); 1729 goto bad_done; 1730 } 1731 1732 if (bozo_auth_check(request, newfile)) 1733 goto bad_done; 1734 1735 if (strlen(newfile)) { 1736 request->hr_oldfile = request->hr_file_free; 1737 request->hr_file = newfile; 1738 } 1739 1740 if (bozo_process_cgi(request) || 1741 bozo_process_lua(request)) 1742 return 0; 1743 1744 debug((httpd, DEBUG_FAT, "transform_request set: %s", newfile)); 1745 return 1; 1746 1747 bad_done: 1748 debug((httpd, DEBUG_FAT, "transform_request returning: 0")); 1749 free(newfile); 1750 return 0; 1751 } 1752 1753 /* 1754 * can_gzip checks if the request supports and prefers gzip encoding. 1755 * 1756 * XXX: we do not consider the associated q with gzip in making our 1757 * decision which is broken. 1758 */ 1759 1760 static int 1761 can_gzip(bozo_httpreq_t *request) 1762 { 1763 const char *pos; 1764 const char *tmp; 1765 size_t len; 1766 1767 /* First we decide if the request can be gzipped at all. */ 1768 1769 /* not if we already are encoded... */ 1770 tmp = bozo_content_encoding(request, request->hr_file); 1771 if (tmp && *tmp) 1772 return 0; 1773 1774 /* not if we are not asking for the whole file... */ 1775 if (request->hr_last_byte_pos != -1 || request->hr_have_range) 1776 return 0; 1777 1778 /* Then we determine if gzip is on the cards. */ 1779 1780 for (pos = request->hr_accept_encoding; pos && *pos; pos += len) { 1781 while (*pos == ' ') 1782 pos++; 1783 1784 len = strcspn(pos, ";,"); 1785 1786 if ((len == 4 && strncasecmp("gzip", pos, 4) == 0) || 1787 (len == 6 && strncasecmp("x-gzip", pos, 6) == 0)) 1788 return 1; 1789 1790 if (pos[len] == ';') 1791 len += strcspn(&pos[len], ","); 1792 1793 if (pos[len]) 1794 len++; 1795 } 1796 1797 return 0; 1798 } 1799 1800 /* 1801 * bozo_process_request does the following: 1802 * - check the request is valid 1803 * - process cgi-bin if necessary 1804 * - transform a filename if necesarry 1805 * - return the HTTP request 1806 */ 1807 void 1808 bozo_process_request(bozo_httpreq_t *request) 1809 { 1810 bozohttpd_t *httpd = request->hr_httpd; 1811 struct stat sb; 1812 time_t timestamp; 1813 char *file; 1814 const char *type, *encoding; 1815 int fd, isindex; 1816 1817 /* 1818 * note that transform_request chdir()'s if required. also note 1819 * that cgi is handed here. if transform_request() returns 0 1820 * then the request has been handled already. 1821 */ 1822 if (transform_request(request, &isindex) == 0) 1823 return; 1824 1825 fd = -1; 1826 encoding = NULL; 1827 if (can_gzip(request)) { 1828 bozoasprintf(httpd, &file, "%s.gz", request->hr_file); 1829 fd = open(file, O_RDONLY); 1830 if (fd >= 0) 1831 encoding = "gzip"; 1832 free(file); 1833 } 1834 1835 file = request->hr_file; 1836 1837 if (fd < 0) 1838 fd = open(file, O_RDONLY); 1839 1840 if (fd < 0) { 1841 debug((httpd, DEBUG_FAT, "open failed: %s", strerror(errno))); 1842 switch (errno) { 1843 case EPERM: 1844 case EACCES: 1845 bozo_http_error(httpd, 403, request, 1846 "no permission to open file"); 1847 break; 1848 case ENAMETOOLONG: 1849 /*FALLTHROUGH*/ 1850 case ENOENT: 1851 if (!bozo_dir_index(request, file, isindex)) 1852 bozo_http_error(httpd, 404, request, "no file"); 1853 break; 1854 default: 1855 bozo_http_error(httpd, 500, request, "open file"); 1856 } 1857 goto cleanup_nofd; 1858 } 1859 if (fstat(fd, &sb) < 0) { 1860 bozo_http_error(httpd, 500, request, "can't fstat"); 1861 goto cleanup; 1862 } 1863 if (S_ISDIR(sb.st_mode)) { 1864 handle_redirect(request, NULL, 0); 1865 goto cleanup; 1866 } 1867 1868 if (request->hr_if_modified_since && 1869 parse_http_date(request->hr_if_modified_since, ×tamp) && 1870 timestamp >= sb.st_mtime) { 1871 /* XXX ignore subsecond of timestamp */ 1872 bozo_printf(httpd, "%s 304 Not Modified\r\n", 1873 request->hr_proto); 1874 bozo_printf(httpd, "\r\n"); 1875 bozo_flush(httpd, stdout); 1876 goto cleanup; 1877 } 1878 1879 /* validate requested range */ 1880 if (request->hr_last_byte_pos == -1 || 1881 request->hr_last_byte_pos >= sb.st_size) 1882 request->hr_last_byte_pos = sb.st_size - 1; 1883 if (request->hr_have_range && 1884 request->hr_first_byte_pos > request->hr_last_byte_pos) { 1885 request->hr_have_range = 0; /* punt */ 1886 request->hr_first_byte_pos = 0; 1887 request->hr_last_byte_pos = sb.st_size - 1; 1888 } 1889 debug((httpd, DEBUG_FAT, "have_range %d first_pos %lld last_pos %lld", 1890 request->hr_have_range, 1891 (long long)request->hr_first_byte_pos, 1892 (long long)request->hr_last_byte_pos)); 1893 if (request->hr_have_range) 1894 bozo_printf(httpd, "%s 206 Partial Content\r\n", 1895 request->hr_proto); 1896 else 1897 bozo_printf(httpd, "%s 200 OK\r\n", request->hr_proto); 1898 1899 if (request->hr_proto != httpd->consts.http_09) { 1900 type = bozo_content_type(request, file); 1901 if (!encoding) 1902 encoding = bozo_content_encoding(request, file); 1903 1904 bozo_print_header(request, &sb, type, encoding); 1905 bozo_printf(httpd, "\r\n"); 1906 } 1907 bozo_flush(httpd, stdout); 1908 1909 if (request->hr_method != HTTP_HEAD) { 1910 off_t szleft, cur_byte_pos; 1911 1912 szleft = 1913 request->hr_last_byte_pos - request->hr_first_byte_pos + 1; 1914 cur_byte_pos = request->hr_first_byte_pos; 1915 1916 retry: 1917 while (szleft) { 1918 size_t sz; 1919 1920 if ((off_t)httpd->mmapsz < szleft) 1921 sz = httpd->mmapsz; 1922 else 1923 sz = (size_t)szleft; 1924 if (mmap_and_write_part(httpd, fd, cur_byte_pos, sz)) { 1925 if (errno == ENOMEM) { 1926 httpd->mmapsz /= 2; 1927 if (httpd->mmapsz >= httpd->page_size) 1928 goto retry; 1929 } 1930 goto cleanup; 1931 } 1932 cur_byte_pos += sz; 1933 szleft -= sz; 1934 } 1935 } 1936 cleanup: 1937 close(fd); 1938 cleanup_nofd: 1939 /* If SSL enabled send close_notify. */ 1940 bozo_ssl_shutdown(request->hr_httpd); 1941 close(STDIN_FILENO); 1942 close(STDOUT_FILENO); 1943 /*close(STDERR_FILENO);*/ 1944 } 1945 1946 /* make sure we're not trying to access special files */ 1947 int 1948 bozo_check_special_files(bozo_httpreq_t *request, const char *name, bool doerror) 1949 { 1950 bozohttpd_t *httpd = request->hr_httpd; 1951 size_t i; 1952 int error = 0; 1953 1954 for (i = 0; specials[i].file; i++) { 1955 if (strcmp(name, specials[i].file) == 0) { 1956 if (doerror) { 1957 error = bozo_http_error(httpd, 403, request, 1958 specials[i].name); 1959 } else { 1960 error = -1; 1961 } 1962 } 1963 } 1964 1965 return error; 1966 } 1967 1968 /* generic header printing routine */ 1969 void 1970 bozo_print_header(bozo_httpreq_t *request, 1971 struct stat *sbp, const char *type, const char *encoding) 1972 { 1973 bozohttpd_t *httpd = request->hr_httpd; 1974 off_t len; 1975 char date[40]; 1976 bozoheaders_t *hdr; 1977 1978 SIMPLEQ_FOREACH(hdr, &request->hr_replheaders, h_next) { 1979 bozo_printf(httpd, "%s: %s\r\n", hdr->h_header, 1980 hdr->h_value); 1981 } 1982 1983 bozo_printf(httpd, "Date: %s\r\n", bozo_http_date(date, sizeof(date))); 1984 bozo_printf(httpd, "Server: %s\r\n", httpd->server_software); 1985 bozo_printf(httpd, "Accept-Ranges: bytes\r\n"); 1986 if (sbp) { 1987 char filedate[40]; 1988 struct tm *tm; 1989 1990 tm = gmtime(&sbp->st_mtime); 1991 strftime(filedate, sizeof filedate, 1992 "%a, %d %b %Y %H:%M:%S GMT", tm); 1993 bozo_printf(httpd, "Last-Modified: %s\r\n", filedate); 1994 } 1995 if (type && *type) 1996 bozo_printf(httpd, "Content-Type: %s\r\n", type); 1997 if (encoding && *encoding) 1998 bozo_printf(httpd, "Content-Encoding: %s\r\n", encoding); 1999 if (sbp) { 2000 if (request->hr_have_range) { 2001 len = request->hr_last_byte_pos - 2002 request->hr_first_byte_pos +1; 2003 bozo_printf(httpd, 2004 "Content-Range: bytes %qd-%qd/%qd\r\n", 2005 (long long) request->hr_first_byte_pos, 2006 (long long) request->hr_last_byte_pos, 2007 (long long) sbp->st_size); 2008 } else 2009 len = sbp->st_size; 2010 bozo_printf(httpd, "Content-Length: %qd\r\n", (long long)len); 2011 } 2012 if (request->hr_proto == httpd->consts.http_11) 2013 bozo_printf(httpd, "Connection: close\r\n"); 2014 bozo_flush(httpd, stdout); 2015 } 2016 2017 #ifndef NO_DEBUG 2018 void 2019 debug__(bozohttpd_t *httpd, int level, const char *fmt, ...) 2020 { 2021 va_list ap; 2022 int savederrno; 2023 2024 /* only log if the level is low enough */ 2025 if (httpd->debug < level) 2026 return; 2027 2028 savederrno = errno; 2029 va_start(ap, fmt); 2030 if (!httpd->nolog) { 2031 if (httpd->logstderr) { 2032 vfprintf(stderr, fmt, ap); 2033 fputs("\n", stderr); 2034 } else 2035 vsyslog(LOG_DEBUG, fmt, ap); 2036 } 2037 va_end(ap); 2038 errno = savederrno; 2039 } 2040 #endif /* NO_DEBUG */ 2041 2042 /* these are like warn() and err(), except for syslog not stderr */ 2043 void 2044 bozowarn(bozohttpd_t *httpd, const char *fmt, ...) 2045 { 2046 va_list ap; 2047 2048 va_start(ap, fmt); 2049 if (!httpd->nolog) { 2050 if (httpd->logstderr || isatty(STDERR_FILENO)) { 2051 //fputs("warning: ", stderr); 2052 vfprintf(stderr, fmt, ap); 2053 fputs("\n", stderr); 2054 } else 2055 vsyslog(LOG_INFO, fmt, ap); 2056 } 2057 va_end(ap); 2058 } 2059 2060 void 2061 bozoerr(bozohttpd_t *httpd, int code, const char *fmt, ...) 2062 { 2063 va_list ap; 2064 2065 va_start(ap, fmt); 2066 if (!httpd->nolog) { 2067 if (httpd->logstderr || isatty(STDERR_FILENO)) { 2068 //fputs("error: ", stderr); 2069 vfprintf(stderr, fmt, ap); 2070 fputs("\n", stderr); 2071 } else 2072 vsyslog(LOG_ERR, fmt, ap); 2073 } 2074 va_end(ap); 2075 exit(code); 2076 } 2077 2078 void 2079 bozoasprintf(bozohttpd_t *httpd, char **str, const char *fmt, ...) 2080 { 2081 va_list ap; 2082 int e; 2083 2084 va_start(ap, fmt); 2085 e = vasprintf(str, fmt, ap); 2086 va_end(ap); 2087 2088 if (e < 0) 2089 bozoerr(httpd, EXIT_FAILURE, "asprintf"); 2090 } 2091 2092 /* 2093 * this escapes HTML tags. returns allocated escaped 2094 * string if needed, or NULL on allocation failure or 2095 * lack of escape need. 2096 * call with NULL httpd in error paths, to avoid recursive 2097 * malloc failure. call with valid httpd in normal paths 2098 * to get automatic allocation failure handling. 2099 */ 2100 char * 2101 bozo_escape_html(bozohttpd_t *httpd, const char *url) 2102 { 2103 int i, j; 2104 char *tmp; 2105 size_t len; 2106 2107 for (i = 0, j = 0; url[i]; i++) { 2108 switch (url[i]) { 2109 case '<': 2110 case '>': 2111 j += 4; 2112 break; 2113 case '&': 2114 j += 5; 2115 break; 2116 case '"': 2117 j += 6; 2118 break; 2119 } 2120 } 2121 2122 if (j == 0) 2123 return NULL; 2124 2125 /* 2126 * we need to handle being called from different 2127 * pathnames. 2128 */ 2129 len = strlen(url) + j; 2130 if (httpd) 2131 tmp = bozomalloc(httpd, len); 2132 else if ((tmp = malloc(len)) == 0) 2133 return NULL; 2134 2135 for (i = 0, j = 0; url[i]; i++) { 2136 switch (url[i]) { 2137 case '<': 2138 memcpy(tmp + j, "<", 4); 2139 j += 4; 2140 break; 2141 case '>': 2142 memcpy(tmp + j, ">", 4); 2143 j += 4; 2144 break; 2145 case '&': 2146 memcpy(tmp + j, "&", 5); 2147 j += 5; 2148 break; 2149 case '"': 2150 memcpy(tmp + j, """, 6); 2151 j += 6; 2152 break; 2153 default: 2154 tmp[j++] = url[i]; 2155 } 2156 } 2157 tmp[j] = 0; 2158 2159 return tmp; 2160 } 2161 2162 /* short map between error code, and short/long messages */ 2163 static struct errors_map { 2164 int code; /* HTTP return code */ 2165 const char *shortmsg; /* short version of message */ 2166 const char *longmsg; /* long version of message */ 2167 } errors_map[] = { 2168 { 200, "200 OK", "The request was valid", }, 2169 { 400, "400 Bad Request", "The request was not valid", }, 2170 { 401, "401 Unauthorized", "No authorization", }, 2171 { 403, "403 Forbidden", "Access to this item has been denied",}, 2172 { 404, "404 Not Found", "This item has not been found", }, 2173 { 408, "408 Request Timeout", "This request took too long", }, 2174 { 413, "413 Payload Too Large", "Use smaller requests", }, 2175 { 417, "417 Expectation Failed","Expectations not available", }, 2176 { 420, "420 Enhance Your Calm","Chill, Winston", }, 2177 { 500, "500 Internal Error", "An error occurred on the server", }, 2178 { 501, "501 Not Implemented", "This request is not available", }, 2179 { 0, NULL, NULL, }, 2180 }; 2181 2182 static const char *help = "DANGER! WILL ROBINSON! DANGER!"; 2183 2184 static const char * 2185 http_errors_short(int code) 2186 { 2187 struct errors_map *ep; 2188 2189 for (ep = errors_map; ep->code; ep++) 2190 if (ep->code == code) 2191 return (ep->shortmsg); 2192 return (help); 2193 } 2194 2195 static const char * 2196 http_errors_long(int code) 2197 { 2198 struct errors_map *ep; 2199 2200 for (ep = errors_map; ep->code; ep++) 2201 if (ep->code == code) 2202 return (ep->longmsg); 2203 return (help); 2204 } 2205 2206 #ifndef NO_BLOCKLIST_SUPPORT 2207 static struct blocklist *blstate; 2208 2209 void 2210 pfilter_notify(const int what, const int code) 2211 { 2212 2213 if (blstate == NULL) 2214 blstate = blocklist_open(); 2215 2216 if (blstate == NULL) 2217 return; 2218 2219 (void)blocklist_r(blstate, what, 0, http_errors_short(code)); 2220 } 2221 #endif /* !NO_BLOCKLIST_SUPPORT */ 2222 2223 /* the follow functions and variables are used in handling HTTP errors */ 2224 int 2225 bozo_http_error(bozohttpd_t *httpd, int code, bozo_httpreq_t *request, 2226 const char *msg) 2227 { 2228 char portbuf[20]; 2229 const char *header = http_errors_short(code); 2230 const char *reason = http_errors_long(code); 2231 const char *proto = (request && request->hr_proto) ? 2232 request->hr_proto : httpd->consts.http_11; 2233 int size; 2234 bozoheaders_t *hdr; 2235 2236 USE_ARG(msg); 2237 2238 debug((httpd, DEBUG_FAT, "bozo_http_error %d: %s", code, msg)); 2239 if (header == NULL || reason == NULL) { 2240 bozoerr(httpd, 1, 2241 "bozo_http_error() failed (short = %p, long = %p)", 2242 header, reason); 2243 return code; 2244 } 2245 2246 if (request && request->hr_serverport && 2247 strcmp(request->hr_serverport, BOZO_HTTP_PORT) != 0) 2248 snprintf(portbuf, sizeof(portbuf), ":%s", 2249 request->hr_serverport); 2250 else 2251 portbuf[0] = '\0'; 2252 2253 if (request && request->hr_file) { 2254 char *file = NULL, *user = NULL; 2255 int file_alloc = 0; 2256 const char *hostname = BOZOHOST(httpd, request); 2257 2258 /* bozo_escape_html() failure here is just too bad. */ 2259 file = bozo_escape_html(NULL, request->hr_file); 2260 if (file == NULL) 2261 file = request->hr_file; 2262 else 2263 file_alloc = 1; 2264 2265 #ifndef NO_USER_SUPPORT 2266 if (request->hr_user != NULL) { 2267 char *user_escaped; 2268 2269 user_escaped = bozo_escape_html(NULL, request->hr_user); 2270 if (user_escaped == NULL) 2271 user_escaped = request->hr_user; 2272 /* expand username to ~user/ */ 2273 bozoasprintf(httpd, &user, "~%s/", user_escaped); 2274 if (user_escaped != request->hr_user) 2275 free(user_escaped); 2276 } 2277 #endif /* !NO_USER_SUPPORT */ 2278 2279 size = snprintf(httpd->errorbuf, BOZO_MINBUFSIZE, 2280 "<html><head><title>%s</title></head>\n" 2281 "<body><h1>%s</h1>\n" 2282 "%s%s: <pre>%s</pre>\n" 2283 "<hr><address><a href=\"//%s%s/\">%s%s</a></address>\n" 2284 "</body></html>\n", 2285 header, header, 2286 user ? user : "", file, 2287 reason, hostname, portbuf, hostname, portbuf); 2288 free(user); 2289 if (size >= (int)BOZO_MINBUFSIZE) { 2290 bozowarn(httpd, 2291 "bozo_http_error buffer too small, truncated"); 2292 size = (int)BOZO_MINBUFSIZE; 2293 } 2294 2295 if (file_alloc) 2296 free(file); 2297 } else 2298 size = 0; 2299 2300 bozo_printf(httpd, "%s %s\r\n", proto, header); 2301 2302 if (request) { 2303 bozo_auth_check_401(request, code); 2304 SIMPLEQ_FOREACH(hdr, &request->hr_replheaders, h_next) { 2305 bozo_printf(httpd, "%s: %s\r\n", hdr->h_header, 2306 hdr->h_value); 2307 } 2308 } 2309 2310 bozo_printf(httpd, "Content-Type: text/html\r\n"); 2311 bozo_printf(httpd, "Content-Length: %d\r\n", size); 2312 bozo_printf(httpd, "Server: %s\r\n", httpd->server_software); 2313 if (request && request->hr_allow) 2314 bozo_printf(httpd, "Allow: %s\r\n", request->hr_allow); 2315 /* RFC 7231 (HTTP/1.1) 6.5.7 */ 2316 if (code == 408 && request && 2317 request->hr_proto == httpd->consts.http_11) 2318 bozo_printf(httpd, "Connection: close\r\n"); 2319 bozo_printf(httpd, "\r\n"); 2320 /* According to the RFC 2616 sec. 9.4 HEAD method MUST NOT return a 2321 * message-body in the response */ 2322 if (size && request && request->hr_method != HTTP_HEAD) 2323 bozo_printf(httpd, "%s", httpd->errorbuf); 2324 bozo_flush(httpd, stdout); 2325 2326 #ifndef NO_BLOCKLIST_SUPPORT 2327 switch(code) { 2328 2329 case 401: 2330 pfilter_notify(BLOCKLIST_AUTH_FAIL, code); 2331 break; 2332 2333 case 403: 2334 pfilter_notify(BLOCKLIST_ABUSIVE_BEHAVIOR, code); 2335 break; 2336 } 2337 #endif /* !NO_BLOCKLIST_SUPPORT */ 2338 2339 return code; 2340 } 2341 2342 /* Below are various modified libc functions */ 2343 2344 /* 2345 * returns -1 in lenp if the string ran out before finding a delimiter, 2346 * but is otherwise the same as strsep. Note that the length must be 2347 * correctly passed in. 2348 */ 2349 char * 2350 bozostrnsep(char **strp, const char *delim, ssize_t *lenp) 2351 { 2352 char *s; 2353 const char *spanp; 2354 int c, sc; 2355 char *tok; 2356 2357 if ((s = *strp) == NULL) 2358 return (NULL); 2359 for (tok = s;;) { 2360 if (lenp && --(*lenp) == -1) 2361 return (NULL); 2362 c = *s++; 2363 spanp = delim; 2364 do { 2365 if ((sc = *spanp++) == c) { 2366 if (c == 0) 2367 s = NULL; 2368 else 2369 s[-1] = '\0'; 2370 *strp = s; 2371 return (tok); 2372 } 2373 } while (sc != 0); 2374 } 2375 /* NOTREACHED */ 2376 } 2377 2378 /* 2379 * inspired by fgetln(3), but works for fd's. should work identically 2380 * except it, however, does *not* return the newline, and it does nul 2381 * terminate the string. 2382 * 2383 * returns NULL if the line grows too large. empty lines will be 2384 * returned with *lenp set to 0. 2385 */ 2386 char * 2387 bozodgetln(bozohttpd_t *httpd, int fd, ssize_t *lenp, 2388 ssize_t (*readfn)(bozohttpd_t *, int, void *, size_t)) 2389 { 2390 ssize_t len; 2391 int got_cr = 0; 2392 char c, *nbuffer; 2393 2394 /* initialise */ 2395 if (httpd->getln_buflen == 0) { 2396 /* should be plenty for most requests */ 2397 httpd->getln_buflen = 128; 2398 httpd->getln_buffer = 2399 bozomalloc(httpd, (size_t)httpd->getln_buflen); 2400 } 2401 len = 0; 2402 2403 /* 2404 * we *have* to read one byte at a time, to not break cgi 2405 * programs (for we pass stdin off to them). could fix this 2406 * by becoming a fd-passing program instead of just exec'ing 2407 * the program 2408 * 2409 * the above is no longer true, we are the fd-passing 2410 * program already. 2411 */ 2412 for (; readfn(httpd, fd, &c, 1) == 1; ) { 2413 debug((httpd, DEBUG_EXPLODING, "bozodgetln read %c", c)); 2414 2415 if (httpd->getln_buflen > BOZO_HEADERS_MAX_SIZE) 2416 return NULL; 2417 2418 if (len >= httpd->getln_buflen - 1) { 2419 httpd->getln_buflen *= 2; 2420 debug((httpd, DEBUG_EXPLODING, "bozodgetln: " 2421 "reallocating buffer to buflen %zu", 2422 httpd->getln_buflen)); 2423 nbuffer = bozorealloc(httpd, httpd->getln_buffer, 2424 (size_t)httpd->getln_buflen); 2425 httpd->getln_buffer = nbuffer; 2426 } 2427 2428 httpd->getln_buffer[len++] = c; 2429 if (c == '\r') { 2430 got_cr = 1; 2431 continue; 2432 } else if (c == '\n') { 2433 /* 2434 * HTTP/1.1 spec says to ignore CR and treat 2435 * LF as the real line terminator. even though 2436 * the same spec defines CRLF as the line 2437 * terminator, it is recommended in section 19.3 2438 * to do the LF trick for tolerance. 2439 */ 2440 if (got_cr) 2441 len -= 2; 2442 else 2443 len -= 1; 2444 break; 2445 } 2446 2447 } 2448 httpd->getln_buffer[len] = '\0'; 2449 debug((httpd, DEBUG_OBESE, "bozodgetln returns: '%s' with len %zd", 2450 httpd->getln_buffer, len)); 2451 *lenp = len; 2452 return httpd->getln_buffer; 2453 } 2454 2455 /* 2456 * allocation frontends with error handling. 2457 * 2458 * note that these may access members of the httpd and/or request. 2459 */ 2460 void * 2461 bozorealloc(bozohttpd_t *httpd, void *ptr, size_t size) 2462 { 2463 void *p; 2464 2465 p = realloc(ptr, size); 2466 if (p) 2467 return p; 2468 2469 bozo_http_error(httpd, 500, NULL, "memory allocation failure"); 2470 exit(EXIT_FAILURE); 2471 } 2472 2473 void * 2474 bozomalloc(bozohttpd_t *httpd, size_t size) 2475 { 2476 void *p; 2477 2478 p = malloc(size); 2479 if (p) 2480 return p; 2481 2482 bozo_http_error(httpd, 500, NULL, "memory allocation failure"); 2483 exit(EXIT_FAILURE); 2484 } 2485 2486 char * 2487 bozostrdup(bozohttpd_t *httpd, bozo_httpreq_t *request, const char *str) 2488 { 2489 char *p; 2490 2491 p = strdup(str); 2492 if (p) 2493 return p; 2494 2495 if (!request) 2496 bozoerr(httpd, EXIT_FAILURE, "strdup"); 2497 2498 bozo_http_error(httpd, 500, request, "memory allocation failure"); 2499 exit(EXIT_FAILURE); 2500 } 2501 2502 /* set default values in bozohttpd_t struct */ 2503 int 2504 bozo_init_httpd(bozohttpd_t *httpd) 2505 { 2506 /* make sure everything is clean */ 2507 (void) memset(httpd, 0x0, sizeof(*httpd)); 2508 2509 /* constants */ 2510 httpd->consts.http_09 = "HTTP/0.9"; 2511 httpd->consts.http_10 = "HTTP/1.0"; 2512 httpd->consts.http_11 = "HTTP/1.1"; 2513 httpd->consts.text_plain = "text/plain"; 2514 2515 /* mmap region size */ 2516 httpd->mmapsz = BOZO_MMAPSZ; 2517 2518 /* error buffer for bozo_http_error() */ 2519 if ((httpd->errorbuf = malloc(BOZO_MINBUFSIZE)) == NULL) { 2520 fprintf(stderr, 2521 "bozohttpd: memory_allocation failure\n"); 2522 return 0; 2523 } 2524 #ifndef NO_LUA_SUPPORT 2525 SIMPLEQ_INIT(&httpd->lua_states); 2526 #endif 2527 return 1; 2528 } 2529 2530 /* set default values in bozoprefs_t struct */ 2531 int 2532 bozo_init_prefs(bozohttpd_t *httpd, bozoprefs_t *prefs) 2533 { 2534 int rv = 1; 2535 2536 /* make sure everything is clean */ 2537 (void) memset(prefs, 0x0, sizeof(*prefs)); 2538 2539 /* set up default values */ 2540 if (!bozo_set_pref(httpd, prefs, "server software", SERVER_SOFTWARE)) 2541 rv = 0; 2542 if (!bozo_set_pref(httpd, prefs, "index.html", INDEX_HTML)) 2543 rv = 0; 2544 if (!bozo_set_pref(httpd, prefs, "public_html", PUBLIC_HTML)) 2545 rv = 0; 2546 if (!bozo_set_pref(httpd, prefs, "ssl timeout", SSL_TIMEOUT)) 2547 rv = 0; 2548 if (!bozo_set_pref(httpd, prefs, "initial timeout", INITIAL_TIMEOUT)) 2549 rv = 0; 2550 if (!bozo_set_pref(httpd, prefs, "header timeout", HEADER_WAIT_TIME)) 2551 rv = 0; 2552 if (!bozo_set_pref(httpd, prefs, "request timeout", TOTAL_MAX_REQ_TIME)) 2553 rv = 0; 2554 2555 return rv; 2556 } 2557 2558 /* set default values */ 2559 int 2560 bozo_set_defaults(bozohttpd_t *httpd, bozoprefs_t *prefs) 2561 { 2562 return bozo_init_httpd(httpd) && bozo_init_prefs(httpd, prefs); 2563 } 2564 2565 /* set the virtual host name, port and root */ 2566 int 2567 bozo_setup(bozohttpd_t *httpd, bozoprefs_t *prefs, const char *vhost, 2568 const char *root) 2569 { 2570 struct passwd *pw; 2571 extern char **environ; 2572 static char *cleanenv[1] = { NULL }; 2573 uid_t uid; 2574 int uidset = 0; 2575 char *chrootdir; 2576 char *username; 2577 char *portnum; 2578 char *cp; 2579 int dirtyenv; 2580 2581 dirtyenv = 0; 2582 2583 if (vhost == NULL) { 2584 httpd->virthostname = bozomalloc(httpd, MAXHOSTNAMELEN+1); 2585 if (gethostname(httpd->virthostname, MAXHOSTNAMELEN+1) < 0) 2586 bozoerr(httpd, 1, "gethostname"); 2587 httpd->virthostname[MAXHOSTNAMELEN] = '\0'; 2588 } else { 2589 httpd->virthostname = bozostrdup(httpd, NULL, vhost); 2590 } 2591 httpd->slashdir = bozostrdup(httpd, NULL, root); 2592 if ((portnum = bozo_get_pref(prefs, "port number")) != NULL) { 2593 httpd->bindport = bozostrdup(httpd, NULL, portnum); 2594 } 2595 2596 /* go over preferences now */ 2597 if ((cp = bozo_get_pref(prefs, "numeric")) != NULL && 2598 strcmp(cp, "true") == 0) { 2599 httpd->numeric = 1; 2600 } 2601 if ((cp = bozo_get_pref(prefs, "log to stderr")) != NULL && 2602 strcmp(cp, "true") == 0) { 2603 httpd->logstderr = 1; 2604 } 2605 if ((cp = bozo_get_pref(prefs, "no log")) != NULL && 2606 strcmp(cp, "true") == 0) { 2607 httpd->nolog = 1; 2608 } 2609 if ((cp = bozo_get_pref(prefs, "bind address")) != NULL) { 2610 httpd->bindaddress = bozostrdup(httpd, NULL, cp); 2611 } 2612 if ((cp = bozo_get_pref(prefs, "background")) != NULL) { 2613 httpd->background = atoi(cp); 2614 } 2615 if ((cp = bozo_get_pref(prefs, "foreground")) != NULL && 2616 strcmp(cp, "true") == 0) { 2617 httpd->foreground = 1; 2618 } 2619 if ((cp = bozo_get_pref(prefs, "pid file")) != NULL) { 2620 httpd->pidfile = bozostrdup(httpd, NULL, cp); 2621 } 2622 if ((cp = bozo_get_pref(prefs, "unknown slash")) != NULL && 2623 strcmp(cp, "true") == 0) { 2624 httpd->unknown_slash = 1; 2625 } 2626 if ((cp = bozo_get_pref(prefs, "virtual base")) != NULL) { 2627 httpd->virtbase = bozostrdup(httpd, NULL, cp); 2628 } 2629 if ((cp = bozo_get_pref(prefs, "enable users")) != NULL && 2630 strcmp(cp, "true") == 0) { 2631 httpd->enable_users = 1; 2632 } 2633 if ((cp = bozo_get_pref(prefs, "enable user cgibin")) != NULL && 2634 strcmp(cp, "true") == 0) { 2635 httpd->enable_cgi_users = 1; 2636 } 2637 if ((cp = bozo_get_pref(prefs, "dirty environment")) != NULL && 2638 strcmp(cp, "true") == 0) { 2639 dirtyenv = 1; 2640 } 2641 if ((cp = bozo_get_pref(prefs, "hide dots")) != NULL && 2642 strcmp(cp, "true") == 0) { 2643 httpd->hide_dots = 1; 2644 } 2645 if ((cp = bozo_get_pref(prefs, "directory indexing")) != NULL && 2646 strcmp(cp, "true") == 0) { 2647 httpd->dir_indexing = 1; 2648 } 2649 if ((cp = bozo_get_pref(prefs, "directory index readme")) != NULL) { 2650 httpd->dir_readme = bozostrdup(httpd, NULL, cp); 2651 } 2652 if ((cp = bozo_get_pref(prefs, "public_html")) != NULL) { 2653 httpd->public_html = bozostrdup(httpd, NULL, cp); 2654 } 2655 if ((cp = bozo_get_pref(prefs, "ssl timeout")) != NULL) { 2656 httpd->ssl_timeout = atoi(cp); 2657 } 2658 if ((cp = bozo_get_pref(prefs, "initial timeout")) != NULL) { 2659 httpd->initial_timeout = atoi(cp); 2660 } 2661 if ((cp = bozo_get_pref(prefs, "header timeout")) != NULL) { 2662 httpd->header_timeout = atoi(cp); 2663 } 2664 if ((cp = bozo_get_pref(prefs, "request timeout")) != NULL) { 2665 httpd->request_timeout = atoi(cp); 2666 } 2667 httpd->server_software = 2668 bozostrdup(httpd, NULL, bozo_get_pref(prefs, "server software")); 2669 httpd->index_html = 2670 bozostrdup(httpd, NULL, bozo_get_pref(prefs, "index.html")); 2671 2672 /* 2673 * initialise ssl and daemon mode if necessary. 2674 */ 2675 bozo_ssl_init(httpd); 2676 bozo_daemon_init(httpd); 2677 2678 username = bozo_get_pref(prefs, "username"); 2679 if (username != NULL) { 2680 if ((pw = getpwnam(username)) == NULL) 2681 bozoerr(httpd, 1, "getpwnam(%s): %s", username, 2682 strerror(errno)); 2683 if (initgroups(pw->pw_name, pw->pw_gid) == -1) 2684 bozoerr(httpd, 1, "initgroups: %s", strerror(errno)); 2685 if (setgid(pw->pw_gid) == -1) 2686 bozoerr(httpd, 1, "setgid(%u): %s", pw->pw_gid, 2687 strerror(errno)); 2688 uid = pw->pw_uid; 2689 uidset = 1; 2690 } 2691 /* 2692 * handle chroot. 2693 */ 2694 if ((chrootdir = bozo_get_pref(prefs, "chroot dir")) != NULL) { 2695 httpd->rootdir = bozostrdup(httpd, NULL, chrootdir); 2696 if (chdir(httpd->rootdir) == -1) 2697 bozoerr(httpd, 1, "chdir(%s): %s", httpd->rootdir, 2698 strerror(errno)); 2699 if (chroot(httpd->rootdir) == -1) 2700 bozoerr(httpd, 1, "chroot(%s): %s", httpd->rootdir, 2701 strerror(errno)); 2702 } 2703 2704 if (uidset && setuid(uid) == -1) 2705 bozoerr(httpd, 1, "setuid(%d): %s", uid, strerror(errno)); 2706 2707 /* 2708 * prevent info leakage between different compartments. 2709 * some PATH values in the environment would be invalided 2710 * by chroot. cross-user settings might result in undesirable 2711 * effects. 2712 */ 2713 if ((chrootdir != NULL || username != NULL) && !dirtyenv) 2714 environ = cleanenv; 2715 2716 #ifdef _SC_PAGESIZE 2717 httpd->page_size = (long)sysconf(_SC_PAGESIZE); 2718 #else 2719 httpd->page_size = 4096; 2720 #endif 2721 debug((httpd, DEBUG_OBESE, "myname is %s, slashdir is %s", 2722 httpd->virthostname, httpd->slashdir)); 2723 2724 return 1; 2725 } 2726 2727 void 2728 bozo_cleanup(bozohttpd_t *httpd, bozoprefs_t *prefs) 2729 { 2730 bozo_clear_prefs(prefs); 2731 2732 free(httpd->virthostname); 2733 free(httpd->errorbuf); 2734 free(httpd->getln_buffer); 2735 free(httpd->slashdir); 2736 free(httpd->bindport); 2737 free(httpd->pidfile); 2738 free(httpd->cgibin); 2739 free(httpd->virtbase); 2740 free(httpd->dynamic_content_map); 2741 #define bozo_unconst(x) ((void *)(uintptr_t)x) 2742 free(bozo_unconst(httpd->server_software)); 2743 free(bozo_unconst(httpd->index_html)); 2744 free(bozo_unconst(httpd->dir_readme)); 2745 free(bozo_unconst(httpd->public_html)); 2746 #undef bozo_unconst 2747 } 2748 2749 int 2750 bozo_get_version(char *buf, size_t size) 2751 { 2752 return snprintf(buf, size, "%s", SERVER_SOFTWARE); 2753 } 2754