1 /* $NetBSD: cgi-bozo.c,v 1.27 2015/05/02 11:35:48 mrg Exp $ */ 2 3 /* $eterna: cgi-bozo.c,v 1.40 2011/11/18 09:21:15 mrg Exp $ */ 4 5 /* 6 * Copyright (c) 1997-2015 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 code implements CGI/1.2 for bozohttpd */ 34 35 #ifndef NO_CGIBIN_SUPPORT 36 37 #include <sys/param.h> 38 #include <sys/socket.h> 39 40 #include <ctype.h> 41 #include <errno.h> 42 #include <paths.h> 43 #include <signal.h> 44 #include <stdlib.h> 45 #include <string.h> 46 #include <syslog.h> 47 #include <unistd.h> 48 49 #include <netinet/in.h> 50 51 #include "bozohttpd.h" 52 53 #define CGIBIN_PREFIX "cgi-bin/" 54 #define CGIBIN_PREFIX_LEN (sizeof(CGIBIN_PREFIX)-1) 55 56 #ifndef USE_ARG 57 #define USE_ARG(x) /*LINTED*/(void)&(x) 58 #endif 59 60 /* 61 * given the file name, return a CGI interpreter 62 */ 63 static const char * 64 content_cgihandler(bozohttpd_t *httpd, bozo_httpreq_t *request, 65 const char *file) 66 { 67 bozo_content_map_t *map; 68 69 USE_ARG(request); 70 debug((httpd, DEBUG_FAT, "content_cgihandler: trying file %s", file)); 71 map = bozo_match_content_map(httpd, file, 0); 72 if (map) 73 return map->cgihandler; 74 return NULL; 75 } 76 77 static int 78 parse_header(bozohttpd_t *httpd, const char *str, ssize_t len, char **hdr_str, 79 char **hdr_val) 80 { 81 char *name, *value; 82 83 /* if the string passed is zero-length bail out */ 84 if (*str == '\0') 85 return -1; 86 87 value = bozostrdup(httpd, str); 88 89 /* locate the ':' separator in the header/value */ 90 name = bozostrnsep(&value, ":", &len); 91 92 if (NULL == name || -1 == len) { 93 free(name); 94 return -1; 95 } 96 97 /* skip leading space/tab */ 98 while (*value == ' ' || *value == '\t') 99 len--, value++; 100 101 *hdr_str = name; 102 *hdr_val = value; 103 104 return 0; 105 } 106 107 /* 108 * handle parsing a CGI header output, transposing a Status: header 109 * into the HTTP reply (ie, instead of "200 OK"). 110 */ 111 static void 112 finish_cgi_output(bozohttpd_t *httpd, bozo_httpreq_t *request, int in, int nph) 113 { 114 char buf[BOZO_WRSZ]; 115 char *str; 116 ssize_t len; 117 ssize_t rbytes; 118 SIMPLEQ_HEAD(, bozoheaders) headers; 119 bozoheaders_t *hdr, *nhdr; 120 int write_header, nheaders = 0; 121 122 /* much of this code is like bozo_read_request()'s header loop. */ 123 SIMPLEQ_INIT(&headers); 124 write_header = nph == 0; 125 /* was read(2) here - XXX - agc */ 126 while (nph == 0 && 127 (str = bozodgetln(httpd, in, &len, bozo_read)) != NULL) { 128 char *hdr_name, *hdr_value; 129 130 if (parse_header(httpd, str, len, &hdr_name, &hdr_value)) 131 break; 132 133 /* 134 * The CGI 1.{1,2} spec both say that if the cgi program 135 * returns a `Status:' header field then the server MUST 136 * return it in the response. If the cgi program does 137 * not return any `Status:' header then the server should 138 * respond with 200 OK. 139 * XXX The CGI 1.1 and 1.2 specification differ slightly on 140 * this in that v1.2 says that the script MUST NOT return a 141 * `Status:' header if it is returning a `Location:' header. 142 * For compatibility we are going with the CGI 1.1 behavior. 143 */ 144 if (strcasecmp(hdr_name, "status") == 0) { 145 debug((httpd, DEBUG_OBESE, 146 "bozo_process_cgi: writing HTTP header " 147 "from status %s ..", hdr_value)); 148 bozo_printf(httpd, "%s %s\r\n", request->hr_proto, 149 hdr_value); 150 bozo_flush(httpd, stdout); 151 write_header = 0; 152 free(hdr_name); 153 break; 154 } 155 156 hdr = bozomalloc(httpd, sizeof *hdr); 157 hdr->h_header = hdr_name; 158 hdr->h_value = hdr_value; 159 SIMPLEQ_INSERT_TAIL(&headers, hdr, h_next); 160 nheaders++; 161 } 162 163 if (write_header) { 164 debug((httpd, DEBUG_OBESE, 165 "bozo_process_cgi: writing HTTP header ..")); 166 bozo_printf(httpd, 167 "%s 200 OK\r\n", request->hr_proto); 168 bozo_flush(httpd, stdout); 169 } 170 171 if (nheaders) { 172 debug((httpd, DEBUG_OBESE, 173 "bozo_process_cgi: writing delayed HTTP headers ..")); 174 SIMPLEQ_FOREACH_SAFE(hdr, &headers, h_next, nhdr) { 175 bozo_printf(httpd, "%s: %s\r\n", hdr->h_header, 176 hdr->h_value); 177 free(hdr->h_header); 178 free(hdr); 179 } 180 bozo_printf(httpd, "\r\n"); 181 bozo_flush(httpd, stdout); 182 } 183 184 /* XXX we should have some goo that times us out 185 */ 186 while ((rbytes = read(in, buf, sizeof buf)) > 0) { 187 ssize_t wbytes; 188 char *bp = buf; 189 190 while (rbytes) { 191 wbytes = bozo_write(httpd, STDOUT_FILENO, buf, 192 (size_t)rbytes); 193 if (wbytes > 0) { 194 rbytes -= wbytes; 195 bp += wbytes; 196 } else 197 bozo_err(httpd, 1, 198 "cgi output write failed: %s", 199 strerror(errno)); 200 } 201 } 202 } 203 204 static void 205 append_index_html(bozohttpd_t *httpd, char **url) 206 { 207 *url = bozorealloc(httpd, *url, 208 strlen(*url) + strlen(httpd->index_html) + 1); 209 strcat(*url, httpd->index_html); 210 debug((httpd, DEBUG_NORMAL, 211 "append_index_html: url adjusted to `%s'", *url)); 212 } 213 214 void 215 bozo_cgi_setbin(bozohttpd_t *httpd, const char *path) 216 { 217 httpd->cgibin = strdup(path); 218 debug((httpd, DEBUG_OBESE, "cgibin (cgi-bin directory) is %s", 219 httpd->cgibin)); 220 } 221 222 /* help build up the environ pointer */ 223 void 224 bozo_setenv(bozohttpd_t *httpd, const char *env, const char *val, 225 char **envp) 226 { 227 char *s1 = bozomalloc(httpd, strlen(env) + strlen(val) + 2); 228 229 strcpy(s1, env); 230 strcat(s1, "="); 231 strcat(s1, val); 232 debug((httpd, DEBUG_OBESE, "bozo_setenv: %s", s1)); 233 *envp = s1; 234 } 235 236 /* 237 * Checks if the request has asked for a cgi-bin. Should only be called if 238 * cgibin is set. If it starts CGIBIN_PREFIX or has a ncontent handler, 239 * process the cgi, otherwise just return. Returns 0 if it did not handle 240 * the request. 241 */ 242 int 243 bozo_process_cgi(bozo_httpreq_t *request) 244 { 245 bozohttpd_t *httpd = request->hr_httpd; 246 char buf[BOZO_WRSZ]; 247 char date[40]; 248 bozoheaders_t *headp; 249 const char *type, *clen, *info, *cgihandler; 250 char *query, *s, *t, *path, *env, *file, *url; 251 char command[MAXPATHLEN]; 252 char **envp, **curenvp, *argv[4]; 253 char *uri; 254 size_t len; 255 ssize_t rbytes; 256 pid_t pid; 257 int envpsize, ix, nph; 258 int sv[2]; 259 260 if (!httpd->cgibin && !httpd->process_cgi) 261 return 0; 262 263 if (request->hr_oldfile && strcmp(request->hr_oldfile, "/") != 0) 264 uri = request->hr_oldfile; 265 else 266 uri = request->hr_file; 267 268 if (uri[0] == '/') 269 file = bozostrdup(httpd, uri); 270 else 271 asprintf(&file, "/%s", uri); 272 if (file == NULL) 273 return 0; 274 275 if (request->hr_query && strlen(request->hr_query)) 276 query = bozostrdup(httpd, request->hr_query); 277 else 278 query = NULL; 279 280 asprintf(&url, "%s%s%s", file, query ? "?" : "", query ? query : ""); 281 if (url == NULL) 282 goto out; 283 debug((httpd, DEBUG_NORMAL, "bozo_process_cgi: url `%s'", url)); 284 285 path = NULL; 286 envp = NULL; 287 cgihandler = NULL; 288 info = NULL; 289 290 len = strlen(url); 291 292 if (bozo_auth_check(request, url + 1)) 293 goto out; 294 295 if (!httpd->cgibin || 296 strncmp(url + 1, CGIBIN_PREFIX, CGIBIN_PREFIX_LEN) != 0) { 297 cgihandler = content_cgihandler(httpd, request, file + 1); 298 if (cgihandler == NULL) { 299 debug((httpd, DEBUG_FAT, 300 "bozo_process_cgi: no handler, returning")); 301 goto out; 302 } 303 if (len == 0 || file[len - 1] == '/') 304 append_index_html(httpd, &file); 305 debug((httpd, DEBUG_NORMAL, "bozo_process_cgi: cgihandler `%s'", 306 cgihandler)); 307 } else if (len - 1 == CGIBIN_PREFIX_LEN) /* url is "/cgi-bin/" */ 308 append_index_html(httpd, &file); 309 310 ix = 0; 311 if (cgihandler) { 312 snprintf(command, sizeof(command), "%s", file + 1); 313 path = bozostrdup(httpd, cgihandler); 314 argv[ix++] = path; 315 /* argv[] = [ path, command, query, NULL ] */ 316 } else { 317 snprintf(command, sizeof(command), "%s", 318 file + CGIBIN_PREFIX_LEN + 1); 319 if ((s = strchr(command, '/')) != NULL) { 320 info = bozostrdup(httpd, s); 321 *s = '\0'; 322 } 323 path = bozomalloc(httpd, 324 strlen(httpd->cgibin) + 1 + strlen(command) + 1); 325 strcpy(path, httpd->cgibin); 326 strcat(path, "/"); 327 strcat(path, command); 328 /* argv[] = [ command, query, NULL ] */ 329 } 330 argv[ix++] = command; 331 argv[ix++] = query; 332 argv[ix++] = NULL; 333 334 nph = strncmp(command, "nph-", 4) == 0; 335 336 type = request->hr_content_type; 337 clen = request->hr_content_length; 338 339 envpsize = 13 + request->hr_nheaders + 340 (info && *info ? 1 : 0) + 341 (query && *query ? 1 : 0) + 342 (type && *type ? 1 : 0) + 343 (clen && *clen ? 1 : 0) + 344 (request->hr_remotehost && *request->hr_remotehost ? 1 : 0) + 345 (request->hr_remoteaddr && *request->hr_remoteaddr ? 1 : 0) + 346 bozo_auth_cgi_count(request) + 347 (request->hr_serverport && *request->hr_serverport ? 1 : 0); 348 349 debug((httpd, DEBUG_FAT, 350 "bozo_process_cgi: path `%s', cmd `%s', info `%s', " 351 "query `%s', nph `%d', envpsize `%d'", 352 path, command, strornull(info), 353 strornull(query), nph, envpsize)); 354 355 envp = bozomalloc(httpd, sizeof(*envp) * envpsize); 356 for (ix = 0; ix < envpsize; ix++) 357 envp[ix] = NULL; 358 curenvp = envp; 359 360 SIMPLEQ_FOREACH(headp, &request->hr_headers, h_next) { 361 const char *s2; 362 env = bozomalloc(httpd, 6 + strlen(headp->h_header) + 1 + 363 strlen(headp->h_value)); 364 365 t = env; 366 strcpy(t, "HTTP_"); 367 t += strlen(t); 368 for (s2 = headp->h_header; *s2; t++, s2++) 369 if (islower((u_int)*s2)) 370 *t = toupper((u_int)*s2); 371 else if (*s2 == '-') 372 *t = '_'; 373 else 374 *t = *s2; 375 *t = '\0'; 376 debug((httpd, DEBUG_OBESE, "setting header %s as %s = %s", 377 headp->h_header, env, headp->h_value)); 378 bozo_setenv(httpd, env, headp->h_value, curenvp++); 379 free(env); 380 } 381 382 #ifndef _PATH_DEFPATH 383 #define _PATH_DEFPATH "/usr/bin:/bin" 384 #endif 385 386 bozo_setenv(httpd, "PATH", _PATH_DEFPATH, curenvp++); 387 bozo_setenv(httpd, "IFS", " \t\n", curenvp++); 388 bozo_setenv(httpd, "SERVER_NAME", BOZOHOST(httpd,request), curenvp++); 389 bozo_setenv(httpd, "GATEWAY_INTERFACE", "CGI/1.1", curenvp++); 390 bozo_setenv(httpd, "SERVER_PROTOCOL", request->hr_proto, curenvp++); 391 bozo_setenv(httpd, "REQUEST_METHOD", request->hr_methodstr, curenvp++); 392 bozo_setenv(httpd, "SCRIPT_NAME", file, curenvp++); 393 bozo_setenv(httpd, "SCRIPT_FILENAME", file + 1, curenvp++); 394 bozo_setenv(httpd, "SERVER_SOFTWARE", httpd->server_software, 395 curenvp++); 396 bozo_setenv(httpd, "REQUEST_URI", uri, curenvp++); 397 bozo_setenv(httpd, "DATE_GMT", bozo_http_date(date, sizeof(date)), 398 curenvp++); 399 if (query && *query) 400 bozo_setenv(httpd, "QUERY_STRING", query, curenvp++); 401 if (info && *info) 402 bozo_setenv(httpd, "PATH_INFO", info, curenvp++); 403 if (type && *type) 404 bozo_setenv(httpd, "CONTENT_TYPE", type, curenvp++); 405 if (clen && *clen) 406 bozo_setenv(httpd, "CONTENT_LENGTH", clen, curenvp++); 407 if (request->hr_serverport && *request->hr_serverport) 408 bozo_setenv(httpd, "SERVER_PORT", request->hr_serverport, 409 curenvp++); 410 if (request->hr_remotehost && *request->hr_remotehost) 411 bozo_setenv(httpd, "REMOTE_HOST", request->hr_remotehost, 412 curenvp++); 413 if (request->hr_remoteaddr && *request->hr_remoteaddr) 414 bozo_setenv(httpd, "REMOTE_ADDR", request->hr_remoteaddr, 415 curenvp++); 416 /* 417 * XXX Apache does this when invoking content handlers, and PHP 418 * XXX 5.3 requires it as a "security" measure. 419 */ 420 if (cgihandler) 421 bozo_setenv(httpd, "REDIRECT_STATUS", "200", curenvp++); 422 bozo_auth_cgi_setenv(request, &curenvp); 423 424 free(file); 425 free(url); 426 427 debug((httpd, DEBUG_FAT, "bozo_process_cgi: going exec %s, %s %s %s", 428 path, argv[0], strornull(argv[1]), strornull(argv[2]))); 429 430 if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, sv) == -1) 431 bozo_err(httpd, 1, "child socketpair failed: %s", 432 strerror(errno)); 433 434 /* 435 * We create 2 procs: one to become the CGI, one read from 436 * the CGI and output to the network, and this parent will 437 * continue reading from the network and writing to the 438 * CGI procsss. 439 */ 440 switch (fork()) { 441 case -1: /* eep, failure */ 442 bozo_err(httpd, 1, "child fork failed: %s", strerror(errno)); 443 /*NOTREACHED*/ 444 case 0: 445 close(sv[0]); 446 dup2(sv[1], STDIN_FILENO); 447 dup2(sv[1], STDOUT_FILENO); 448 close(2); 449 close(sv[1]); 450 closelog(); 451 bozo_daemon_closefds(httpd); 452 453 if (-1 == execve(path, argv, envp)) 454 bozo_err(httpd, 1, "child exec failed: %s: %s", 455 path, strerror(errno)); 456 /* NOT REACHED */ 457 bozo_err(httpd, 1, "child execve returned?!"); 458 } 459 460 close(sv[1]); 461 462 /* parent: read from stdin (bozo_read()) write to sv[0] */ 463 /* child: read from sv[0] (bozo_write()) write to stdout */ 464 pid = fork(); 465 if (pid == -1) 466 bozo_err(httpd, 1, "io child fork failed: %s", strerror(errno)); 467 else if (pid == 0) { 468 /* child reader/writer */ 469 close(STDIN_FILENO); 470 finish_cgi_output(httpd, request, sv[0], nph); 471 /* if we're done output, our parent is useless... */ 472 kill(getppid(), SIGKILL); 473 debug((httpd, DEBUG_FAT, "done processing cgi output")); 474 _exit(0); 475 } 476 close(STDOUT_FILENO); 477 478 /* XXX we should have some goo that times us out 479 */ 480 while ((rbytes = bozo_read(httpd, STDIN_FILENO, buf, sizeof buf)) > 0) { 481 ssize_t wbytes; 482 char *bp = buf; 483 484 while (rbytes) { 485 wbytes = write(sv[0], buf, (size_t)rbytes); 486 if (wbytes > 0) { 487 rbytes -= wbytes; 488 bp += wbytes; 489 } else 490 bozo_err(httpd, 1, "write failed: %s", 491 strerror(errno)); 492 } 493 } 494 debug((httpd, DEBUG_FAT, "done processing cgi input")); 495 exit(0); 496 497 out: 498 free(query); 499 free(file); 500 free(url); 501 return 0; 502 } 503 504 #ifndef NO_DYNAMIC_CONTENT 505 /* cgi maps are simple ".postfix /path/to/prog" */ 506 void 507 bozo_add_content_map_cgi(bozohttpd_t *httpd, const char *arg, const char *cgihandler) 508 { 509 bozo_content_map_t *map; 510 511 debug((httpd, DEBUG_NORMAL, "bozo_add_content_map_cgi: name %s cgi %s", 512 arg, cgihandler)); 513 514 httpd->process_cgi = 1; 515 516 map = bozo_get_content_map(httpd, arg); 517 map->name = arg; 518 map->type = map->encoding = map->encoding11 = NULL; 519 map->cgihandler = cgihandler; 520 } 521 #endif /* NO_DYNAMIC_CONTENT */ 522 523 #endif /* NO_CGIBIN_SUPPORT */ 524