1 /* $NetBSD: cgi-bozo.c,v 1.19 2010/12/14 13:27:39 tls Exp $ */ 2 3 /* $eterna: cgi-bozo.c,v 1.38 2010/09/20 22:25:00 mrg Exp $ */ 4 5 /* 6 * Copyright (c) 1997-2010 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, *command, *file, *url; 251 char **envp, **curenvp, *argv[4]; 252 char *uri; 253 size_t len; 254 ssize_t rbytes; 255 pid_t pid; 256 int envpsize, ix, nph; 257 int sv[2]; 258 259 if (!httpd->cgibin && !httpd->process_cgi) 260 return 0; 261 262 uri = request->hr_oldfile ? request->hr_oldfile : request->hr_file; 263 if (uri[0] == '/') 264 file = bozostrdup(httpd, uri); 265 else 266 asprintf(&file, "/%s", uri); 267 if (file == NULL) 268 return 0; 269 270 if (request->hr_query && strlen(request->hr_query)) 271 query = bozostrdup(httpd, request->hr_query); 272 else 273 query = NULL; 274 275 asprintf(&url, "%s%s%s", file, query ? "?" : "", query ? query : ""); 276 if (url == NULL) 277 goto out; 278 debug((httpd, DEBUG_NORMAL, "bozo_process_cgi: url `%s'", url)); 279 280 path = NULL; 281 envp = NULL; 282 cgihandler = NULL; 283 command = NULL; 284 info = NULL; 285 286 len = strlen(url); 287 288 if (bozo_auth_check(request, url + 1)) 289 goto out; 290 291 if (!httpd->cgibin || 292 strncmp(url + 1, CGIBIN_PREFIX, CGIBIN_PREFIX_LEN) != 0) { 293 cgihandler = content_cgihandler(httpd, request, file + 1); 294 if (cgihandler == NULL) { 295 debug((httpd, DEBUG_FAT, 296 "bozo_process_cgi: no handler, returning")); 297 goto out; 298 } 299 if (len == 0 || file[len - 1] == '/') 300 append_index_html(httpd, &file); 301 debug((httpd, DEBUG_NORMAL, "bozo_process_cgi: cgihandler `%s'", 302 cgihandler)); 303 } else if (len - 1 == CGIBIN_PREFIX_LEN) /* url is "/cgi-bin/" */ 304 append_index_html(httpd, &file); 305 306 ix = 0; 307 if (cgihandler) { 308 command = file + 1; 309 path = bozostrdup(httpd, cgihandler); 310 argv[ix++] = path; 311 /* argv[] = [ path, command, query, NULL ] */ 312 } else { 313 command = file + CGIBIN_PREFIX_LEN + 1; 314 if ((s = strchr(command, '/')) != NULL) { 315 info = bozostrdup(httpd, s); 316 *s = '\0'; 317 } 318 path = bozomalloc(httpd, 319 strlen(httpd->cgibin) + 1 + strlen(command) + 1); 320 strcpy(path, httpd->cgibin); 321 strcat(path, "/"); 322 strcat(path, command); 323 /* argv[] = [ command, query, NULL ] */ 324 } 325 argv[ix++] = command; 326 argv[ix++] = query; 327 argv[ix++] = NULL; 328 329 nph = strncmp(command, "nph-", 4) == 0; 330 331 type = request->hr_content_type; 332 clen = request->hr_content_length; 333 334 envpsize = 13 + request->hr_nheaders + 335 (info && *info ? 1 : 0) + 336 (query && *query ? 1 : 0) + 337 (type && *type ? 1 : 0) + 338 (clen && *clen ? 1 : 0) + 339 (request->hr_remotehost && *request->hr_remotehost ? 1 : 0) + 340 (request->hr_remoteaddr && *request->hr_remoteaddr ? 1 : 0) + 341 bozo_auth_cgi_count(request) + 342 (request->hr_serverport && *request->hr_serverport ? 1 : 0); 343 344 debug((httpd, DEBUG_FAT, 345 "bozo_process_cgi: path `%s', cmd `%s', info `%s', " 346 "query `%s', nph `%d', envpsize `%d'", 347 path, command, strornull(info), 348 strornull(query), nph, envpsize)); 349 350 envp = bozomalloc(httpd, sizeof(*envp) * envpsize); 351 for (ix = 0; ix < envpsize; ix++) 352 envp[ix] = NULL; 353 curenvp = envp; 354 355 SIMPLEQ_FOREACH(headp, &request->hr_headers, h_next) { 356 const char *s2; 357 env = bozomalloc(httpd, 6 + strlen(headp->h_header) + 1 + 358 strlen(headp->h_value)); 359 360 t = env; 361 strcpy(t, "HTTP_"); 362 t += strlen(t); 363 for (s2 = headp->h_header; *s2; t++, s2++) 364 if (islower((u_int)*s2)) 365 *t = toupper((u_int)*s2); 366 else if (*s2 == '-') 367 *t = '_'; 368 else 369 *t = *s2; 370 *t = '\0'; 371 debug((httpd, DEBUG_OBESE, "setting header %s as %s = %s", 372 headp->h_header, env, headp->h_value)); 373 bozo_setenv(httpd, env, headp->h_value, curenvp++); 374 free(env); 375 } 376 377 #ifndef _PATH_DEFPATH 378 #define _PATH_DEFPATH "/usr/bin:/bin" 379 #endif 380 381 bozo_setenv(httpd, "PATH", _PATH_DEFPATH, curenvp++); 382 bozo_setenv(httpd, "IFS", " \t\n", curenvp++); 383 bozo_setenv(httpd, "SERVER_NAME", httpd->virthostname, curenvp++); 384 bozo_setenv(httpd, "GATEWAY_INTERFACE", "CGI/1.1", curenvp++); 385 bozo_setenv(httpd, "SERVER_PROTOCOL", request->hr_proto, curenvp++); 386 bozo_setenv(httpd, "REQUEST_METHOD", request->hr_methodstr, curenvp++); 387 bozo_setenv(httpd, "SCRIPT_NAME", file, curenvp++); 388 bozo_setenv(httpd, "SCRIPT_FILENAME", file + 1, curenvp++); 389 bozo_setenv(httpd, "SERVER_SOFTWARE", httpd->server_software, 390 curenvp++); 391 bozo_setenv(httpd, "REQUEST_URI", uri, curenvp++); 392 bozo_setenv(httpd, "DATE_GMT", bozo_http_date(date, sizeof(date)), 393 curenvp++); 394 if (query && *query) 395 bozo_setenv(httpd, "QUERY_STRING", query, curenvp++); 396 if (info && *info) 397 bozo_setenv(httpd, "PATH_INFO", info, curenvp++); 398 if (type && *type) 399 bozo_setenv(httpd, "CONTENT_TYPE", type, curenvp++); 400 if (clen && *clen) 401 bozo_setenv(httpd, "CONTENT_LENGTH", clen, curenvp++); 402 if (request->hr_serverport && *request->hr_serverport) 403 bozo_setenv(httpd, "SERVER_PORT", request->hr_serverport, 404 curenvp++); 405 if (request->hr_remotehost && *request->hr_remotehost) 406 bozo_setenv(httpd, "REMOTE_HOST", request->hr_remotehost, 407 curenvp++); 408 if (request->hr_remoteaddr && *request->hr_remoteaddr) 409 bozo_setenv(httpd, "REMOTE_ADDR", request->hr_remoteaddr, 410 curenvp++); 411 /* 412 * XXX Apache does this when invoking content handlers, and PHP 413 * XXX 5.3 requires it as a "security" measure. 414 */ 415 if (cgihandler) 416 bozo_setenv(httpd, "REDIRECT_STATUS", "200", curenvp++); 417 bozo_auth_cgi_setenv(request, &curenvp); 418 419 free(file); 420 free(url); 421 422 debug((httpd, DEBUG_FAT, "bozo_process_cgi: going exec %s, %s %s %s", 423 path, argv[0], strornull(argv[1]), strornull(argv[2]))); 424 425 if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, sv) == -1) 426 bozo_err(httpd, 1, "child socketpair failed: %s", 427 strerror(errno)); 428 429 /* 430 * We create 2 procs: one to become the CGI, one read from 431 * the CGI and output to the network, and this parent will 432 * continue reading from the network and writing to the 433 * CGI procsss. 434 */ 435 switch (fork()) { 436 case -1: /* eep, failure */ 437 bozo_err(httpd, 1, "child fork failed: %s", strerror(errno)); 438 /*NOTREACHED*/ 439 case 0: 440 close(sv[0]); 441 dup2(sv[1], STDIN_FILENO); 442 dup2(sv[1], STDOUT_FILENO); 443 close(2); 444 close(sv[1]); 445 closelog(); 446 bozo_daemon_closefds(httpd); 447 448 if (-1 == execve(path, argv, envp)) 449 bozo_err(httpd, 1, "child exec failed: %s: %s", 450 path, strerror(errno)); 451 /* NOT REACHED */ 452 bozo_err(httpd, 1, "child execve returned?!"); 453 } 454 455 close(sv[1]); 456 457 /* parent: read from stdin (bozo_read()) write to sv[0] */ 458 /* child: read from sv[0] (bozo_write()) write to stdout */ 459 pid = fork(); 460 if (pid == -1) 461 bozo_err(httpd, 1, "io child fork failed: %s", strerror(errno)); 462 else if (pid == 0) { 463 /* child reader/writer */ 464 close(STDIN_FILENO); 465 finish_cgi_output(httpd, request, sv[0], nph); 466 /* if we're done output, our parent is useless... */ 467 kill(getppid(), SIGKILL); 468 debug((httpd, DEBUG_FAT, "done processing cgi output")); 469 _exit(0); 470 } 471 close(STDOUT_FILENO); 472 473 /* XXX we should have some goo that times us out 474 */ 475 while ((rbytes = bozo_read(httpd, STDIN_FILENO, buf, sizeof buf)) > 0) { 476 ssize_t wbytes; 477 char *bp = buf; 478 479 while (rbytes) { 480 wbytes = write(sv[0], buf, (size_t)rbytes); 481 if (wbytes > 0) { 482 rbytes -= wbytes; 483 bp += wbytes; 484 } else 485 bozo_err(httpd, 1, "write failed: %s", 486 strerror(errno)); 487 } 488 } 489 debug((httpd, DEBUG_FAT, "done processing cgi input")); 490 exit(0); 491 492 out: 493 if (query) 494 free(query); 495 if (file) 496 free(file); 497 if (url) 498 free(url); 499 return 0; 500 } 501 502 #ifndef NO_DYNAMIC_CONTENT 503 /* cgi maps are simple ".postfix /path/to/prog" */ 504 void 505 bozo_add_content_map_cgi(bozohttpd_t *httpd, const char *arg, const char *cgihandler) 506 { 507 bozo_content_map_t *map; 508 509 debug((httpd, DEBUG_NORMAL, "bozo_add_content_map_cgi: name %s cgi %s", 510 arg, cgihandler)); 511 512 httpd->process_cgi = 1; 513 514 map = bozo_get_content_map(httpd, arg); 515 map->name = arg; 516 map->namelen = strlen(map->name); 517 map->type = map->encoding = map->encoding11 = NULL; 518 map->cgihandler = cgihandler; 519 } 520 #endif /* NO_DYNAMIC_CONTENT */ 521 522 #endif /* NO_CGIBIN_SUPPORT */ 523