httpd: implement POSTDATA read timeout, and -K KILLSECS CGI lifetime control
No binary (build failed)
gcc -Wp,-MD,applets/.usage.d -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer -Iinclude -Iinclude -o applets/usage applets/usage.c
(log yok)
| @@ -268,6 +268,7 @@ | |
| //usage: " [-c CONFFILE]" | |
| //usage: " [-p [IP:]PORT]" | |
| //usage: " [-M MAXCONN]" | |
| + | //usage: IF_FEATURE_HTTPD_CGI(" [-K KILLSEC]") |
| //usage: IF_FEATURE_HTTPD_SETUID(" [-u USER[:GRP]]") | |
| //usage: IF_FEATURE_HTTPD_BASIC_AUTH(" [-r REALM]") | |
| //usage: " [-h HOME]\n" | |
| @@ -279,6 +280,8 @@ | |
| //usage: "\n -v[v] Verbose" | |
| //usage: "\n -p [IP:]PORT Bind to IP:PORT (default *:"STR(CONFIG_FEATURE_HTTPD_PORT_DEFAULT)")" | |
| //usage: "\n -M NUM Pause if NUM connections are open (default 256)" | |
| + | //usage: IF_FEATURE_HTTPD_CGI( |
| + | //usage: "\n -K NUM Kill CGIs after NUM seconds") |
| //usage: IF_FEATURE_HTTPD_SETUID( | |
| //usage: "\n -u USER[:GRP] Set uid/gid after binding to port") | |
| //usage: IF_FEATURE_HTTPD_BASIC_AUTH( | |
| @@ -299,6 +302,10 @@ | |
| * (HEADER_READ_TIMEOUT) | |
| * - if client stops consuming its result ("stalled receiver" attack), | |
| * the server will drop the connection (DATA_WRITE_TIMEOUT) | |
| + | * - POSTDATA for POST method to CGI must arrive at least once |
| + | * per DATA_READ_TIMEOUT |
| + | * - killing CGIs which are obviously stuck if -K 60: |
| + | * "if CGI isn't done in 1 minute, it's fishy. SIGTERM+SIGKILL" |
| * | |
| * To limit the number of simultaneously running CGI children, | |
| * you may use the helper tool, httpd_ratelimit_cgi.c: | |
| @@ -309,15 +316,13 @@ | |
| * | |
| * TODO: | |
| * - do not allow all MAXCONN connections be taken up by the same remote IP. | |
| - | * - CGI I/O loop lacks full timeout protection (may be abused by slow POST client?), |
| - | * see cgi_io_loop_and_exit() |
| * - sanity: add a limit how big POSTDATA can be? (-P 1024: "megabyte+ of POSTDATA is insanity") | |
| * currently we only do: if (POST_length > INT_MAX) HTTP_BAD_REQUEST | |
| - | * - sanity: kill CGIs which are obviously stuck? (-K 60: "if CGI isn't done in 1 minute, it's fishy. SIGTERM+SIGKILL") |
| * - sanity: measure CGI memory consumption (how?), kill when way too big? | |
| */ | |
| #define HEADER_READ_TIMEOUT 30 | |
| #define DATA_WRITE_TIMEOUT 60 | |
| + | #define DATA_READ_TIMEOUT 60 |
| #include "libbb.h" | |
| @@ -483,7 +488,11 @@ struct globals { | |
| smallint flg_deny_all; | |
| #if ENABLE_FEATURE_HTTPD_GZIP | |
| /* client can handle gzip / we are going to send gzip */ | |
| + | smallint accept_gzip; |
| smallint content_gzip; | |
| + | #endif |
| + | #if ENABLE_FEATURE_HTTPD_CGI |
| + | smallint cgi_output; |
| #endif | |
| time_t last_mod; | |
| #if ENABLE_FEATURE_HTTPD_ETAG | |
| @@ -528,6 +537,13 @@ struct globals { | |
| #define sizeof_hdr_buf COMMON_BUFSIZE | |
| char *hdr_ptr; | |
| int hdr_cnt; | |
| + | #if ENABLE_FEATURE_HTTPD_CGI || ENABLE_FEATURE_HTTPD_PROXY |
| + | int POST_len; |
| + | #endif |
| + | #if ENABLE_FEATURE_HTTPD_CGI |
| + | unsigned cgi_kill_timeout; |
| + | pid_t cgi_pid; |
| + | #endif |
| #if ENABLE_FEATURE_HTTPD_ETAG | |
| char etag[sizeof("'%llx-%llx'") + 2 * sizeof(long long)*3]; | |
| #endif | |
| @@ -543,8 +559,10 @@ struct globals { | |
| #define verbose (G.verbose ) | |
| #define flg_deny_all (G.flg_deny_all ) | |
| #if ENABLE_FEATURE_HTTPD_GZIP | |
| + | # define accept_gzip (G.accept_gzip ) |
| # define content_gzip (G.content_gzip ) | |
| #else | |
| + | # define accept_gzip 0 |
| # define content_gzip 0 | |
| #endif | |
| #define bind_addr_or_port (G.bind_addr_or_port) | |
| @@ -1386,8 +1404,8 @@ static unsigned get_line(void) | |
| #if ENABLE_FEATURE_HTTPD_CGI || ENABLE_FEATURE_HTTPD_PROXY | |
| /* gcc 4.2.1 fares better with NOINLINE */ | |
| - | static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post_len) NORETURN; |
| - | static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post_len) |
| + | static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr) NORETURN; |
| + | static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr) |
| { | |
| enum { FROM_CGI = 1, TO_CGI = 2 }; /* indexes in pfd[] */ | |
| struct pollfd pfd[3]; | |
| @@ -1401,12 +1419,14 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post | |
| // is coming than we expected. We may give *some part* of that | |
| // extra data to CGI. | |
| - | //if (hdr_cnt > post_len) { |
| + | /* We often have the start of POSTDATA buffered in hdr_buf already |
| + | * by the header lines reading logic */ |
| + | //if (hdr_cnt > G.POST_len) { |
| // /* We got more POSTDATA from network than we expected */ | |
| - | // hdr_cnt = post_len; |
| + | // hdr_cnt = G.POST_len; |
| //} | |
| - | post_len -= hdr_cnt; |
| - | /* post_len - number of POST bytes not yet read from network */ |
| + | G.POST_len -= hdr_cnt; |
| + | //bb_error_msg("G.POST_len:%d", G.POST_len); |
| /* NB: breaking out of this loop jumps to log_and_exit() */ | |
| out_cnt = 0; | |
| @@ -1432,15 +1452,22 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post | |
| pfd[TO_CGI].revents = 0; /* needed! */ | |
| if (toCgi_wr && hdr_cnt <= 0) { | |
| - | if (post_len > 0) { |
| + | if (G.POST_len > 0) { |
| /* Expect more POST data from network */ | |
| pfd[0].fd = 0; | |
| + | /* Kill ourselves if no data arrives in N seconds */ |
| + | alarm(DATA_READ_TIMEOUT); |
| } else { | |
| - | /* post_len <= 0 && hdr_cnt <= 0: |
| + | /* G.POST_len <= 0 && hdr_cnt <= 0: |
| * no more POST data to CGI, | |
| * let CGI see EOF on CGI's stdin */ | |
| - | if (toCgi_wr != fromCgi_rd) |
| + | if (!ENABLE_FEATURE_HTTPD_PROXY || (toCgi_wr != fromCgi_rd)) { |
| close(toCgi_wr); | |
| + | alarm(G.cgi_kill_timeout); |
| + | } else { |
| + | /* proxying a socket, there is no CGI */ |
| + | alarm(0); |
| + | } |
| toCgi_wr = 0; | |
| } | |
| } | |
| @@ -1448,9 +1475,6 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post | |
| /* Now wait on the set of sockets */ | |
| /* Poll whether TO_CGI is ready to accept writes *only* if we have some POSTDATA to give to it */ | |
| count = safe_poll(pfd, hdr_cnt > 0 ? TO_CGI+1 : FROM_CGI+1, -1); | |
| - | //FIXME:do not allow the client to stall POSTDATA: |
| - | //the timeout must not be infinite, should be similar to DATA_WRITE_TIMEOUT |
| - | //("give some data at least once a minute", better "give >=N bytes once minute") |
| if (count <= 0) { | |
| #if 0 /* This doesn't work since we SIG_IGNed SIGCHLD (which means kernel auto-reaps our children) */ | |
| @@ -1483,25 +1507,25 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post | |
| hdr_cnt -= count; | |
| } else { | |
| /* EOF/broken pipe to CGI, stop piping POST data */ | |
| - | hdr_cnt = post_len = 0; |
| + | hdr_cnt = G.POST_len = 0; |
| } | |
| } | |
| - | |
| + | else /* After one poll(), we either write to CGI, or read from network. Never both */ |
| if (pfd[0].revents) { | |
| - | /* post_len > 0 && hdr_cnt == 0 here */ |
| + | /* G.POST_len > 0 && hdr_cnt == 0 here */ |
| /* We expect data, prev data portion is eaten by CGI | |
| * and there *is* POSTDATA to read from the peer | |
| */ | |
| - | //count = post_len > (int)sizeof_hdr_buf ? (int)sizeof_hdr_buf : post_len; |
| + | //count = G.POST_len > (int)sizeof_hdr_buf ? (int)sizeof_hdr_buf : G.POST_len; |
| //count = safe_read(STDIN_FILENO, hdr_buf, count); | |
| count = safe_read(STDIN_FILENO, hdr_buf, sizeof_hdr_buf); | |
| if (count > 0) { | |
| hdr_cnt = count; | |
| hdr_ptr = hdr_buf; | |
| - | post_len -= count; /* can go negative (peer wrote more than expected POSTDATA) */ |
| + | G.POST_len -= count; /* can go negative (peer wrote more than expected POSTDATA) */ |
| } else { | |
| /* no more POST data can be read */ | |
| - | post_len = 0; |
| + | G.POST_len = 0; |
| } | |
| } | |
| @@ -1546,6 +1570,13 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post | |
| memmove(iobuf + 1, iobuf, out_cnt); | |
| out_cnt += 1; | |
| memcpy(iobuf, HTTP_200, 9); | |
| + | if (verbose) { |
| + | char *end = iobuf + 9; |
| + | int cnt = out_cnt - 9; |
| + | while ((unsigned char)*end >= ' ' && (unsigned char)*end < 0x7f && cnt > 0) |
| + | end++, cnt--; |
| + | bb_error_msg("cgi response:'%.*s'", (int)(end - (iobuf + 9)), iobuf + 9); |
| + | } |
| } | |
| //NB: Apache has no such autodetection. It always adds its own HTTP/1.x header, | |
| //unless the CGI name starts with "nph-", in which case it passes its output verbatim to network. | |
| @@ -1556,10 +1587,12 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post | |
| /* no, send "HTTP/1.1 200 OK\r\n" ourself */ | |
| if (full_write(STDOUT_FILENO, HTTP_200, sizeof(HTTP_200)-1) != sizeof(HTTP_200)-1) | |
| break; | |
| + | if (verbose) |
| + | bb_error_msg("cgi response:%u", 200); |
| } | |
| /* Used to add "Content-type: text/plain\r\n\r\n" here if CGI didn't, | |
| * but it's wrong. Counter-example of valid CGI without Content-type: | |
| - | * echo -en "HTTP/1.1 302 Found\r\n" |
| + | * echo -en "Status: 302 Found\r\n" |
| * echo -en "Location: http://www.busybox.net\r\n" | |
| * echo -en "\r\n" | |
| */ | |
| @@ -1571,6 +1604,7 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post | |
| if (count <= 0) | |
| send_EOF_and_exit(); /* EOF (or error) */ | |
| } | |
| + | IF_FEATURE_HTTPD_CGI(G.cgi_output = 1;) |
| //FIXME: many (most?) servers translate bare "\n" to "\r\n", only in the headers, not body (the part after empty line) | |
| if (full_write(STDOUT_FILENO, iobuf, count) != count) | |
| break; | |
| @@ -1598,23 +1632,19 @@ static void setenv1(const char *name, const char *value) | |
| * Parameters: | |
| * const char *url The requested URL (with leading /). | |
| * const char *orig_uri The original URI before rewriting (if any) | |
| - | * int post_len Length of the POST body. |
| */ | |
| static void send_cgi_and_exit( | |
| const char *url, | |
| const char *orig_uri, | |
| - | const char *request, |
| - | int post_len) NORETURN; |
| + | const char *request) NORETURN; |
| static void send_cgi_and_exit( | |
| const char *url, | |
| const char *orig_uri, | |
| - | const char *request, |
| - | int post_len) |
| + | const char *request) |
| { | |
| struct fd_pair fromCgi; /* CGI -> httpd pipe */ | |
| struct fd_pair toCgi; /* httpd -> CGI pipe */ | |
| char *script, *last_slash; | |
| - | int pid; |
| /* Make a copy. NB: caller guarantees: | |
| * url[0] == '/', url[1] != '/' */ | |
| @@ -1687,8 +1717,8 @@ static void send_cgi_and_exit( | |
| #endif | |
| } | |
| } | |
| - | if (post_len) |
| - | putenv(xasprintf("CONTENT_LENGTH=%u", post_len)); |
| + | if (G.POST_len > 0) |
| + | putenv(xasprintf("CONTENT_LENGTH=%u", G.POST_len)); |
| #if ENABLE_FEATURE_HTTPD_BASIC_AUTH | |
| if (remoteuser) { | |
| setenv1("REMOTE_USER", remoteuser); | |
| @@ -1701,17 +1731,20 @@ static void send_cgi_and_exit( | |
| xpiped_pair(fromCgi); | |
| xpiped_pair(toCgi); | |
| - | pid = vfork(); |
| - | if (pid < 0) { |
| + | G.cgi_pid = vfork(); |
| + | if (G.cgi_pid < 0) { |
| if (VERBOSE_1) | |
| bb_simple_perror_msg("vfork"); | |
| log_and_exit(); | |
| } | |
| - | if (pid == 0) { |
| + | if (G.cgi_pid == 0) { |
| /* Child process */ | |
| char *argv[3]; | |
| + | /* -K SECS kills entire process group: set up one */ |
| + | bb_setpgrp(); |
| + | |
| /* NB: close _first_, then move fds! */ | |
| close(toCgi.wr); | |
| close(fromCgi.rd); | |
| @@ -1776,7 +1809,10 @@ static void send_cgi_and_exit( | |
| error_execing_cgi: | |
| /* send to stdout | |
| * (we are CGI here, our stdout is pumped to the net) */ | |
| - | send_headers_and_exit(HTTP_NOT_FOUND); |
| + | //send_headers_and_exit(HTTP_NOT_FOUND); |
| + | //^^^ WRONG, this logs "closed", then the parent logs it again |
| + | send_headers(HTTP_NOT_FOUND); |
| + | _exit_FAILURE(); |
| } /* end child */ | |
| /* Parent process */ | |
| @@ -1784,7 +1820,7 @@ static void send_cgi_and_exit( | |
| /* Pump data */ | |
| close(fromCgi.wr); | |
| close(toCgi.rd); | |
| - | cgi_io_loop_and_exit(fromCgi.rd, toCgi.wr, post_len); |
| + | cgi_io_loop_and_exit(fromCgi.rd, toCgi.wr); |
| } | |
| #endif /* FEATURE_HTTPD_CGI */ | |
| @@ -1802,7 +1838,8 @@ static NOINLINE void send_file_and_exit(const char *url, int what) | |
| int fd; | |
| ssize_t count; | |
| - | if (content_gzip) { |
| + | #if ENABLE_FEATURE_HTTPD_GZIP |
| + | if (accept_gzip) { |
| /* does <url>.gz exist? Then use it instead */ | |
| char *gzurl = xasprintf("%s.gz", url); | |
| fd = open(gzurl, O_RDONLY); | |
| @@ -1812,11 +1849,13 @@ static NOINLINE void send_file_and_exit(const char *url, int what) | |
| fstat(fd, &sb); | |
| file_size = sb.st_size; | |
| last_mod = sb.st_mtime; | |
| + | content_gzip = 1; |
| } else { | |
| - | IF_FEATURE_HTTPD_GZIP(content_gzip = 0;) |
| fd = open(url, O_RDONLY); | |
| } | |
| - | } else { |
| + | } else |
| + | #endif |
| + | { |
| fd = open(url, O_RDONLY); | |
| /* file_size and last_mod are already populated */ | |
| } | |
| @@ -2240,16 +2279,27 @@ static Htaccess_Proxy *find_proxy_entry(const char *url) | |
| /* | |
| * Handle timeouts | |
| */ | |
| - | static void send_REQUEST_TIMEOUT_and_exit(int sig) NORETURN; |
| - | static void send_REQUEST_TIMEOUT_and_exit(int sig UNUSED_PARAM) |
| + | static void sigalrm_handler(int sig) NORETURN; |
| + | static void sigalrm_handler(int sig UNUSED_PARAM) |
| { | |
| - | /* timed out reading headers */ |
| - | send_headers_and_exit(HTTP_REQUEST_TIMEOUT); |
| - | //If we'd use alarm() for write timeouts too: |
| - | // /* writing timed out: exit without writing anything */ |
| - | // if (VERBOSE_3) |
| - | // bb_simple_error_msg("write timeout"); |
| - | // _exit_SUCCESS(); |
| + | /* timed out reading headers, POSTDATA, or CGI runs too long */ |
| + | int response = HTTP_REQUEST_TIMEOUT; |
| + | #if ENABLE_FEATURE_HTTPD_CGI |
| + | if (G.cgi_pid > 0) { |
| + | if (kill(-G.cgi_pid, SIGTERM) == 0) { |
| + | bb_error_msg("kill cgi:%d", G.cgi_pid); |
| + | sleep1(); |
| + | kill(-G.cgi_pid, SIGKILL); |
| + | } |
| + | /* Browsers were seen retrying if got HTTP_REQUEST_TIMEOUT, |
| + | * we don't want that for the case of stuck CGI. */ |
| + | response = HTTP_INTERNAL_SERVER_ERROR; |
| + | } |
| + | #endif |
| + | IF_FEATURE_HTTPD_CGI(if (!G.cgi_output)) |
| + | send_headers_and_exit(response); |
| + | /* else: we already have some output, do not garble it with HTTP response */ |
| + | log_and_exit(); |
| } | |
| static void prepare_write_timeout(void) | |
| @@ -2286,7 +2336,6 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) | |
| static const char request_HEAD[] ALIGN1 = "HEAD"; | |
| #if ENABLE_FEATURE_HTTPD_CGI | |
| static const char request_POST[] ALIGN1 = "POST"; | |
| - | unsigned long POST_length; |
| enum CGI_type { | |
| CGI_NONE = 0, | |
| CGI_NORMAL, | |
| @@ -2397,7 +2446,8 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) | |
| /* The above also allows http2 which starts with a fixed | |
| * "PRI * HTTP/2.0" line | |
| */ | |
| - | cgi_io_loop_and_exit(proxy_fd, proxy_fd, /*max POST length:*/ INT_MAX); |
| + | G.POST_len = INT_MAX; /* hack */ |
| + | cgi_io_loop_and_exit(proxy_fd, proxy_fd); |
| } | |
| #endif | |
| /* We don't support http2 "*" URI, enforce "/URI" form */ | |
| @@ -2555,7 +2605,6 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) | |
| #if ENABLE_FEATURE_HTTPD_CGI | |
| total_headers_len = 0; | |
| - | POST_length = 0; |
| #endif | |
| /* Read until blank line */ | |
| @@ -2577,9 +2626,9 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) | |
| if (!tptr[0]) | |
| send_headers_and_exit(HTTP_BAD_REQUEST); | |
| /* not using strtoul: it ignores leading minus! */ | |
| - | POST_length = bb_strtou(tptr, NULL, 10); |
| - | /* length is "ulong", but we need to pass it to int later */ |
| - | if (errno || POST_length > INT_MAX) |
| + | G.POST_len = (int)bb_strtou(tptr, NULL, 10); |
| + | /* we need to pass it to int later */ |
| + | if (errno || G.POST_len < 0) |
| send_headers_and_exit(HTTP_BAD_REQUEST); | |
| continue; | |
| } | |
| @@ -2630,7 +2679,7 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) | |
| // || s[-1] == ',' | |
| // || s[-1] == ':' | |
| //) { | |
| - | content_gzip = 1; |
| + | accept_gzip = 1; |
| //} | |
| } | |
| continue; | |
| @@ -2698,7 +2747,7 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) | |
| send_cgi_and_exit( | |
| (cgi_type == CGI_INDEX) ? "/cgi-bin/index.cgi" | |
| /*CGI_NORMAL or CGI_INTERPRETER*/ : urlcopy, | |
| - | urlcopy, prequest, POST_length |
| + | urlcopy, prequest |
| ); | |
| } | |
| #endif | |
| @@ -2785,7 +2834,7 @@ static void mini_httpd(int server_socket) | |
| { | |
| int countdown; | |
| - | signal(SIGALRM, send_REQUEST_TIMEOUT_and_exit); |
| + | signal(SIGALRM, sigalrm_handler); |
| xmove_fd(server_socket, 0); | |
| /* NB: it's best to not use xfuncs in this loop before fork(). | |
| @@ -2843,7 +2892,7 @@ static void mini_httpd_nommu(int server_socket, int argc, char **argv) | |
| argv_copy[1] = (char*)"-i"; | |
| memcpy(&argv_copy[2], &argv[1], argc * sizeof(argv[0])); | |
| - | /*signal(SIGALRM, send_REQUEST_TIMEOUT_and_exit);*/ |
| + | /*signal(SIGALRM, sigalrm_handler);*/ |
| /* ^^^ WRONG. mini_httpd_inetd() does this */ | |
| xmove_fd(server_socket, 0); | |
| @@ -2895,7 +2944,7 @@ static void mini_httpd_inetd(void) | |
| { | |
| len_and_sockaddr fromAddr; | |
| - | signal(SIGALRM, send_REQUEST_TIMEOUT_and_exit); |
| + | signal(SIGALRM, sigalrm_handler); |
| memset(&fromAddr, 0, sizeof(fromAddr)); | |
| fromAddr.len = LSA_SIZEOF_SA; | |
| @@ -2923,6 +2972,7 @@ enum { | |
| IF_FEATURE_HTTPD_SETUID( u_opt_setuid ,) | |
| p_opt_port , | |
| M_opt_maxconn , | |
| + | K_opt_killcgi , |
| i_opt_inetd , | |
| f_opt_foreground, | |
| v_opt_verbose , | |
| @@ -2974,7 +3024,7 @@ int httpd_main(int argc UNUSED_PARAM, char **argv) | |
| IF_FEATURE_HTTPD_BASIC_AUTH("r:") | |
| IF_FEATURE_HTTPD_AUTH_MD5("m:") | |
| IF_FEATURE_HTTPD_SETUID("u:") | |
| - | "p:M:+ifv" |
| + | "p:M:+K:+ifv" |
| "\0" | |
| /* -v counts, -i implies -f */ | |
| "vv:if", | |
| @@ -2985,6 +3035,7 @@ int httpd_main(int argc UNUSED_PARAM, char **argv) | |
| IF_FEATURE_HTTPD_SETUID(, &s_ugid) | |
| , &bind_addr_or_port | |
| , &G.conn_limit | |
| + | , IF_FEATURE_HTTPD_CGI(&G.cgi_kill_timeout) IF_NOT_FEATURE_HTTPD_CGI(NULL) |
| , &verbose | |
| ); | |
| if (opt & OPT_DECODE_URL) { | |