From 32a471e4db43b5c1115084d02d8cf54db2098a72 Mon Sep 17 00:00:00 2001 From: Denis Vlasenko Date: Sun, 23 Sep 2007 13:56:57 +0000 Subject: httpd: simplify CGI i/o loop. -200 bytes. --- networking/httpd.c | 351 ++++++++++++++++++++++++++++------------------------- 1 file changed, 183 insertions(+), 168 deletions(-) diff --git a/networking/httpd.c b/networking/httpd.c index 057416040..33f083189 100644 --- a/networking/httpd.c +++ b/networking/httpd.c @@ -104,12 +104,15 @@ //#define DEBUG 1 #define DEBUG 0 +#define IOBUF_SIZE 8192 /* IO buffer */ + /* amount of buffering in a pipe */ #ifndef PIPE_BUF # define PIPE_BUF 4096 #endif - -#define IOBUF_SIZE 8192 /* IO buffer */ +#if PIPE_BUF >= IOBUF_SIZE +# error "PIPE_BUF >= IOBUF_SIZE" +#endif #define HEADER_READ_TIMEOUT 60 @@ -1029,6 +1032,173 @@ static int get_line(void) } #if ENABLE_FEATURE_HTTPD_CGI + +/* 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) ATTRIBUTE_NORETURN; +static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post_len) +{ + enum { FROM_CGI = 1, TO_CGI = 2 }; /* indexes in pfd[] */ + struct pollfd pfd[3]; + int out_cnt; /* we buffer a bit of initial CGI output */ + int count; + + /* iobuf is used for CGI -> network data, + * hdr_buf is for network -> CGI data (POSTDATA) */ + + /* If CGI dies, we still want to correctly finish reading its output + * and send it to the peer. So please no SIGPIPEs! */ + signal(SIGPIPE, SIG_IGN); + + /* NB: breaking out of this loop jumps to log_and_exit() */ + out_cnt = 0; + while (1) { + memset(pfd, 0, sizeof(pfd)); + + pfd[FROM_CGI].fd = fromCgi_rd; + pfd[FROM_CGI].events = POLLIN; + + if (toCgi_wr) { + pfd[TO_CGI].fd = toCgi_wr; + if (hdr_cnt > 0) { + pfd[TO_CGI].events = POLLOUT; + } else if (post_len > 0) { + pfd[0].events = POLLIN; + } else { + /* post_len <= 0 && hdr_cnt <= 0: + * no more POST data to CGI, + * let CGI see EOF on CGI's stdin */ + close(toCgi_wr); + toCgi_wr = 0; + } + } + + /* Now wait on the set of sockets */ + count = poll(pfd, 3, -1); + if (count <= 0) { +#if 0 + if (errno == EINTR) + continue; +#endif +#if 0 + if (waitpid(pid, &status, WNOHANG) <= 0) { + /* Weird. CGI didn't exit and no fd's + * are ready, yet poll returned?! */ + continue; + } + if (DEBUG && WIFEXITED(status)) + bb_error_msg("CGI exited, status=%d", WEXITSTATUS(status)); + if (DEBUG && WIFSIGNALED(status)) + bb_error_msg("CGI killed, signal=%d", WTERMSIG(status)); +#endif + break; + } + + if (pfd[TO_CGI].revents) { + /* hdr_cnt > 0 here due to the way pfd[TO_CGI].events set */ + /* Have data from peer and can write to CGI */ + count = safe_write(toCgi_wr, hdr_ptr, hdr_cnt); + /* Doesn't happen, we dont use nonblocking IO here + *if (count < 0 && errno == EAGAIN) { + * ... + *} else */ + if (count > 0) { + hdr_ptr += count; + hdr_cnt -= count; + } else { + /* EOF/broken pipe to CGI, stop piping POST data */ + hdr_cnt = post_len = 0; + } + } + + if (pfd[0].revents) { + /* post_len > 0 && hdr_cnt == 0 here */ + /* We expect data, prev data portion is eaten by CGI + * and there *is* data to read from the peer + * (POSTDATA) */ + //count = post_len > (int)sizeof(hdr_buf) ? (int)sizeof(hdr_buf) : post_len; + //count = safe_read(0, hdr_buf, count); + count = safe_read(0, hdr_buf, sizeof(hdr_buf)); + if (count > 0) { + hdr_cnt = count; + hdr_ptr = hdr_buf; + post_len -= count; + } else { + /* no more POST data can be read */ + post_len = 0; + } + } + + if (pfd[FROM_CGI].revents) { + /* There is something to read from CGI */ + char *rbuf = iobuf; + + /* Are we still buffering CGI output? */ + if (out_cnt >= 0) { + /* HTTP_200[] has single "\r\n" at the end. + * According to http://hoohoo.ncsa.uiuc.edu/cgi/out.html, + * CGI scripts MUST send their own header terminated by + * empty line, then data. That's why we have only one + * pair here. We will output "200 OK" line + * if needed, but CGI still has to provide blank line + * between header and body */ + + /* Must use safe_read, not full_read, because + * CGI may output a few first bytes and then wait + * for POSTDATA without closing stdout. + * With full_read we may wait here forever. */ + count = safe_read(fromCgi_rd, rbuf + out_cnt, PIPE_BUF - 8); + if (count <= 0) { + /* eof (or error) and there was no "HTTP", + * so write it, then write received data */ + if (out_cnt) { + full_write(1, HTTP_200, sizeof(HTTP_200)-1); + full_write(1, rbuf, out_cnt); + } + break; /* CGI stdout is closed, exiting */ + } + out_cnt += count; + count = 0; + /* "Status" header format is: "Status: 302 Redirected\r\n" */ + if (out_cnt >= 8 && memcmp(rbuf, "Status: ", 8) == 0) { + /* send "HTTP/1.0 " */ + if (full_write(1, HTTP_200, 9) != 9) + break; + rbuf += 8; /* skip "Status: " */ + count = out_cnt - 8; + out_cnt = -1; /* buffering off */ + } else if (out_cnt >= 4) { + /* Did CGI add "HTTP"? */ + if (memcmp(rbuf, HTTP_200, 4) != 0) { + /* there is no "HTTP", do it ourself */ + if (full_write(1, HTTP_200, sizeof(HTTP_200)-1) != sizeof(HTTP_200)-1) + break; + } + /* Commented out: + if (!strstr(rbuf, "ontent-")) { + full_write(s, "Content-type: text/plain\r\n\r\n", 28); + } + * Counter-example of valid CGI without Content-type: + * echo -en "HTTP/1.0 302 Found\r\n" + * echo -en "Location: http://www.busybox.net\r\n" + * echo -en "\r\n" + */ + count = out_cnt; + out_cnt = -1; /* buffering off */ + } + } else { + count = safe_read(fromCgi_rd, rbuf, PIPE_BUF); + if (count <= 0) + break; /* eof (or error) */ + } + if (full_write(1, rbuf, count) != count) + break; + if (DEBUG) + fprintf(stderr, "cgi read %d bytes: '%.*s'\n", count, count, rbuf); + } /* if (pfd[FROM_CGI].revents) */ + } /* while (1) */ + log_and_exit(); +} + static void setenv1(const char *name, const char *value) { setenv(name, value ? value : "", 1); @@ -1038,25 +1208,25 @@ static void setenv1(const char *name, const char *value) * Spawn CGI script, forward CGI's stdin/out <=> network * * Environment variables are set up and the script is invoked with pipes - * for stdin/stdout. If a post is being done the script is fed the POST + * for stdin/stdout. If a POST is being done the script is fed the POST * data in addition to setting the QUERY_STRING variable (for GETs or POSTs). * * Parameters: * const char *url The requested URL (with leading /). - * int bodyLen Length of the post body. + * int post_len Length of the POST body. * const char *cookie For set HTTP_COOKIE. * const char *content_type For set CONTENT_TYPE. */ static void send_cgi_and_exit( const char *url, const char *request, - int bodyLen, + int post_len, const char *cookie, const char *content_type) ATTRIBUTE_NORETURN; static void send_cgi_and_exit( const char *url, const char *request, - int bodyLen, + int post_len, const char *cookie, const char *content_type) { @@ -1065,9 +1235,7 @@ static void send_cgi_and_exit( char *fullpath; char *script; char *purl; - int buf_count; - int status; - int pid = 0; + int pid; /* * We are mucking with environment _first_ and then vfork/exec, @@ -1138,8 +1306,8 @@ static void send_cgi_and_exit( } } setenv1("HTTP_USER_AGENT", user_agent); - if (bodyLen) - putenv(xasprintf("CONTENT_LENGTH=%d", bodyLen)); + if (post_len) + putenv(xasprintf("CONTENT_LENGTH=%d", post_len)); if (cookie) setenv1("HTTP_COOKIE", cookie); if (content_type) @@ -1215,168 +1383,15 @@ static void send_cgi_and_exit( /* Parent process */ - /* First, restore variables possibly changed by child */ + /* Restore variables possibly changed by child */ xfunc_error_retval = 0; - /* Prepare for pumping data. - * iobuf is used for CGI -> network data, - * hdr_buf is for network -> CGI data (POSTDATA) */ - buf_count = 0; + /* Pump data */ close(fromCgi.wr); close(toCgi.rd); - - /* If CGI dies, we still want to correctly finish reading its output - * and send it to the peer. So please no SIGPIPEs! */ - signal(SIGPIPE, SIG_IGN); - - /* This loop still looks messy. What is an exit criteria? - * "CGI's output closed"? Or "CGI has exited"? - * What to do if CGI has closed both input and output, but - * didn't exit? etc... */ - - /* NB: breaking out of this loop jumps to log_and_exit() */ - while (1) { - fd_set readSet; - fd_set writeSet; - int nfound; - int count; - - FD_ZERO(&readSet); - FD_ZERO(&writeSet); - FD_SET(fromCgi.rd, &readSet); - if (bodyLen > 0 || hdr_cnt > 0) { - FD_SET(toCgi.wr, &writeSet); - nfound = toCgi.wr > fromCgi.rd ? toCgi.wr : fromCgi.rd; - if (hdr_cnt <= 0) - FD_SET(0, &readSet); - /* Now wait on the set of sockets! */ - nfound = select(nfound + 1, &readSet, &writeSet, NULL, NULL); - } else { - if (!bodyLen) { - close(toCgi.wr); /* no more POST data to CGI */ - bodyLen = -1; - } - nfound = select(fromCgi.rd + 1, &readSet, NULL, NULL, NULL); - } - - if (nfound <= 0) { - if (waitpid(pid, &status, WNOHANG) <= 0) { - /* Weird. CGI didn't exit and no fd's - * are ready, yet select returned?! */ - continue; - } - close(fromCgi.rd); - if (DEBUG && WIFEXITED(status)) - bb_error_msg("CGI exited, status=%d", WEXITSTATUS(status)); - if (DEBUG && WIFSIGNALED(status)) - bb_error_msg("CGI killed, signal=%d", WTERMSIG(status)); - break; - } - - if (hdr_cnt > 0 && FD_ISSET(toCgi.wr, &writeSet)) { - /* Have data from peer and can write to CGI */ - count = safe_write(toCgi.wr, hdr_ptr, hdr_cnt); - /* Doesn't happen, we dont use nonblocking IO here - *if (count < 0 && errno == EAGAIN) { - * ... - *} else */ - if (count > 0) { - hdr_ptr += count; - hdr_cnt -= count; - } else { - hdr_cnt = bodyLen = 0; /* EOF/broken pipe to CGI */ - } - } else if (bodyLen > 0 && hdr_cnt == 0 - && FD_ISSET(0, &readSet) - ) { - /* We expect data, prev data portion is eaten by CGI - * and there *is* data to read from the peer - * (POSTDATA?) */ - count = bodyLen > (int)sizeof(hdr_buf) ? (int)sizeof(hdr_buf) : bodyLen; - count = safe_read(0, hdr_buf, count); - if (count > 0) { - hdr_cnt = count; - hdr_ptr = hdr_buf; - bodyLen -= count; - } else { - bodyLen = 0; /* closed */ - } - } - -#define PIPESIZE PIPE_BUF -#if PIPESIZE >= IOBUF_SIZE -# error "PIPESIZE >= IOBUF_SIZE" -#endif - if (FD_ISSET(fromCgi.rd, &readSet)) { - /* There is something to read from CGI */ - char *rbuf = iobuf; - - /* Are we still buffering CGI output? */ - if (buf_count >= 0) { - /* HTTP_200[] has single "\r\n" at the end. - * According to http://hoohoo.ncsa.uiuc.edu/cgi/out.html, - * CGI scripts MUST send their own header terminated by - * empty line, then data. That's why we have only one - * pair here. We will output "200 OK" line - * if needed, but CGI still has to provide blank line - * between header and body */ - - /* Must use safe_read, not full_read, because - * CGI may output a few first bytes and then wait - * for POSTDATA without closing stdout. - * With full_read we may wait here forever. */ - count = safe_read(fromCgi.rd, rbuf + buf_count, PIPESIZE - 8); - if (count <= 0) { - /* eof (or error) and there was no "HTTP", - * so write it, then write received data */ - if (buf_count) { - full_write(1, HTTP_200, sizeof(HTTP_200)-1); - full_write(1, rbuf, buf_count); - } - break; /* CGI stdout is closed, exiting */ - } - buf_count += count; - count = 0; - /* "Status" header format is: "Status: 302 Redirected\r\n" */ - if (buf_count >= 8 && memcmp(rbuf, "Status: ", 8) == 0) { - /* send "HTTP/1.0 " */ - if (full_write(1, HTTP_200, 9) != 9) - break; - rbuf += 8; /* skip "Status: " */ - count = buf_count - 8; - buf_count = -1; /* buffering off */ - } else if (buf_count >= 4) { - /* Did CGI add "HTTP"? */ - if (memcmp(rbuf, HTTP_200, 4) != 0) { - /* there is no "HTTP", do it ourself */ - if (full_write(1, HTTP_200, sizeof(HTTP_200)-1) != sizeof(HTTP_200)-1) - break; - } - /* Commented out: - if (!strstr(rbuf, "ontent-")) { - full_write(s, "Content-type: text/plain\r\n\r\n", 28); - } - * Counter-example of valid CGI without Content-type: - * echo -en "HTTP/1.0 302 Found\r\n" - * echo -en "Location: http://www.busybox.net\r\n" - * echo -en "\r\n" - */ - count = buf_count; - buf_count = -1; /* buffering off */ - } - } else { - count = safe_read(fromCgi.rd, rbuf, PIPESIZE); - if (count <= 0) - break; /* eof (or error) */ - } - if (full_write(1, rbuf, count) != count) - break; - if (DEBUG) - fprintf(stderr, "cgi read %d bytes: '%.*s'\n", count, count, rbuf); - } /* if (FD_ISSET(fromCgi.rd)) */ - } /* while (1) */ - log_and_exit(); + cgi_io_loop_and_exit(fromCgi.rd, toCgi.wr, post_len); } + #endif /* FEATURE_HTTPD_CGI */ /* -- cgit v1.2.3