From 38721685af680f8ffd8b5b70ed74a0c305519eac Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Wed, 21 Jan 2026 17:49:40 +0100 Subject: httpd: expand logging: now can see what CGIs are started function old new delta send_cgi_and_exit 711 784 +73 vmstat_main 657 708 +51 .rodata 106746 106778 +32 send_file_and_exit 877 887 +10 ------------------------------------------------------------------------------ (add/remove: 0/0 grow/shrink: 4/0 up/down: 166/0) Total: 166 bytes Signed-off-by: Denys Vlasenko --- networking/httpd.c | 71 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/networking/httpd.c b/networking/httpd.c index e1a447fa1..e96916480 100644 --- a/networking/httpd.c +++ b/networking/httpd.c @@ -556,6 +556,10 @@ enum { file_size = -1; \ } while (0) +#define VERBOSE_1 (verbose) +#define VERBOSE_2 (verbose > 1) +/* TODO: make conditional on FEATURE_HTTPD_MAXVERBOSE */ +#define VERBOSE_3 (verbose > 2) #define STRNCASECMP(a, str) strncasecmp((a), (str), sizeof(str)-1) @@ -1064,7 +1068,7 @@ static void log_and_exit(void) continue; */ - if (verbose > 2) + if (VERBOSE_3) bb_simple_error_msg("closed"); _exit(xfunc_error_retval); } @@ -1280,8 +1284,8 @@ static void send_headers(unsigned responseNum) fprintf(stderr, "headers: '%s'\n", iobuf); } if (full_write(STDOUT_FILENO, iobuf, len) != len) { - if (verbose > 1) - bb_simple_perror_msg("error"); + if (VERBOSE_1) + bb_simple_perror_msg("write error"); log_and_exit(); } } @@ -1401,16 +1405,19 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post /* Now wait on the set of sockets */ count = safe_poll(pfd, hdr_cnt > 0 ? TO_CGI+1 : FROM_CGI+1, -1); if (count <= 0) { -#if 0 - if (safe_waitpid(pid, &status, WNOHANG) <= 0) { - /* Weird. CGI didn't exit and no fd's - * are ready, yet poll returned?! */ - continue; +#if 0 /* This doesn't work since we SIG_IGNed SIGCHLD (which means kernel auto-reaps our children) */ + if (VERBOSE_3) { + int status; + if (safe_waitpid(-1, &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=%u", WEXITSTATUS(status)); + if (DEBUG && WIFSIGNALED(status)) + bb_error_msg("CGI killed, signal=%u", WTERMSIG(status)); } - if (DEBUG && WIFEXITED(status)) - bb_error_msg("CGI exited, status=%u", WEXITSTATUS(status)); - if (DEBUG && WIFSIGNALED(status)) - bb_error_msg("CGI killed, signal=%u", WTERMSIG(status)); #endif break; } @@ -1644,7 +1651,8 @@ static void send_cgi_and_exit( pid = vfork(); if (pid < 0) { - /* TODO: log perror? */ + if (VERBOSE_1) + bb_simple_perror_msg("vfork"); log_and_exit(); } @@ -1659,14 +1667,13 @@ static void send_cgi_and_exit( close(fromCgi.rd); xmove_fd(toCgi.rd, 0); /* replace stdin with the pipe */ xmove_fd(fromCgi.wr, 1); /* replace stdout with the pipe */ - /* User seeing stderr output can be a security problem. - * If CGI really wants that, it can always do dup itself. */ - /* dup2(1, 2); */ /* Chdiring to script's dir */ script = last_slash; if (script != url) { /* paranoia */ *script = '\0'; + if (VERBOSE_3) + bb_error_msg("cd:%s", url + 1); if (chdir_or_warn(url + 1) != 0) { goto error_execing_cgi; } @@ -1696,13 +1703,20 @@ static void send_cgi_and_exit( } } #endif - /* restore default signal dispositions for CGI process */ + if (VERBOSE_2) + bb_error_msg("exec:%s"IF_FEATURE_HTTPD_CONFIG_WITH_SCRIPT_INTERPR(" %s"), + argv[0] + IF_FEATURE_HTTPD_CONFIG_WITH_SCRIPT_INTERPR(, argv[1]) + ); + /* Restore default signal dispositions for CGI process */ bb_signals(0 | (1 << SIGCHLD) | (1 << SIGPIPE) | (1 << SIGHUP) , SIG_DFL); - + /* User seeing stderr output can be a security problem. + * If CGI really wants that, it can always do dup itself. */ + /* dup2(1, 2); */ /* _NOT_ execvp. We do not search PATH. argv[0] is a filename * without any dir components and will only match a file * in the current directory */ @@ -1894,7 +1908,9 @@ static NOINLINE void send_file_and_exit(const char *url, int what) if (count < 0) { if (offset == range_start) /* was it the very 1st sendfile? */ break; /* fall back to read/write loop */ - goto fin; + if (VERBOSE_1) + bb_simple_perror_msg("sendfile error"); + log_and_exit(); } IF_FEATURE_HTTPD_RANGES(range_len -= count;) if (count == 0 || range_len == 0) @@ -1913,9 +1929,8 @@ static NOINLINE void send_file_and_exit(const char *url, int what) break; } if (count < 0) { - IF_FEATURE_USE_SENDFILE(fin:) - if (verbose > 1) - bb_simple_perror_msg("error"); + if (VERBOSE_1) + bb_simple_perror_msg("read error"); } log_and_exit(); } @@ -2222,7 +2237,7 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) /* this trick makes -v logging much simpler */ if (rmt_ip_str) applet_name = rmt_ip_str; - if (verbose > 2) + if (VERBOSE_3) bb_simple_error_msg("connected"); } #if ENABLE_FEATURE_HTTPD_ACL_IP @@ -2250,7 +2265,7 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) * being sent at all. * (Presumably it's a method to decrease latency?) */ - if (verbose > 2) + if (VERBOSE_3) bb_simple_error_msg("eof on read, closing"); /* Don't bother generating error page in this case, * just close the socket. @@ -2283,7 +2298,7 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) int proxy_fd; len_and_sockaddr *lsa; - if (verbose > 1) + if (VERBOSE_2) bb_error_msg("proxy:%s", urlp); lsa = host2sockaddr(proxy_entry->host_port, 80); if (!lsa) @@ -2383,7 +2398,7 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) } /* Log it */ - if (verbose > 1) + if (VERBOSE_2) bb_error_msg("url:%s", urlcopy); tptr = urlcopy; @@ -2664,7 +2679,9 @@ static void mini_httpd(int server_socket) if (fork() == 0) { /* child */ /* Do not reload config on HUP */ +//TODO: can make reload handler check the pid and do nothing in children? signal(SIGHUP, SIG_IGN); +//TODO: can move server_socket to fd 0, making the close here unnecessary? close(server_socket); xmove_fd(n, 0); xdup2(0, 1); @@ -2850,7 +2867,9 @@ int httpd_main(int argc UNUSED_PARAM, char **argv) xchdir(home_httpd); if (!(opt & OPT_INETD)) { + /* Make it unnecessary to wait for children */ signal(SIGCHLD, SIG_IGN); + server_socket = openServer(); #if ENABLE_FEATURE_HTTPD_SETUID /* drop privileges */ -- cgit v1.2.3-55-g6feb From 58b46b7d67c4063aafb94bf82f4e2f3d6e0e3878 Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Wed, 21 Jan 2026 18:43:41 +0100 Subject: networking/httpd_ratelimit_cgi.c: new example CGI handler text data bss dec hex filename 4003 40 352 4395 112b httpd_ratelimit_cgi Signed-off-by: Denys Vlasenko --- networking/httpd_helpers.sh | 7 +- networking/httpd_ratelimit_cgi.c | 242 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 networking/httpd_ratelimit_cgi.c diff --git a/networking/httpd_helpers.sh b/networking/httpd_helpers.sh index 8eaa2d456..d2c639840 100755 --- a/networking/httpd_helpers.sh +++ b/networking/httpd_helpers.sh @@ -1,6 +1,6 @@ #!/bin/sh -PREFIX="i486-linux-uclibc-" +PREFIX="i686-linux-musl-" OPTS="-static -static-libgcc \ -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 \ -Wall -Wshadow -Wwrite-strings -Wundef -Wstrict-prototypes -Werror \ @@ -22,3 +22,8 @@ ${PREFIX}gcc \ ${OPTS} \ -Wl,-Map -Wl,httpd_ssi.map \ httpd_ssi.c -o httpd_ssi && strip httpd_ssi + +${PREFIX}gcc \ +${OPTS} \ +-Wl,-Map -Wl,httpd_ssi.map \ +httpd_ratelimit_cgi.c -o httpd_ratelimit_cgi && strip httpd_ratelimit_cgi diff --git a/networking/httpd_ratelimit_cgi.c b/networking/httpd_ratelimit_cgi.c new file mode 100644 index 000000000..713274873 --- /dev/null +++ b/networking/httpd_ratelimit_cgi.c @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2026 Denys Vlasenko + * + * Licensed under GPLv2, see file LICENSE in this source tree. + */ + +/* + * This program is a CGI application. It is intended to rate-limit + * invocations of another, presumably resource-intensive CGI + * which you want to only allow less than N instances at any one time. + * + * Any extra clients who try to run the CGI will get the + * "429 Too Many Requests" HTTP response. + * + * The most efficient way to do so is to use a shebang-style executable file: + * #!/path/to/httpd_ratelimit_cgi /tmp/lockdir 99 /path/to/expensive_cgi + */ + +/* Build a-la +i486-linux-uclibc-gcc \ +-static -static-libgcc \ +-D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 \ +-Wall -Wshadow -Wwrite-strings -Wundef -Wstrict-prototypes -Werror \ +-Wold-style-definition -Wdeclaration-after-statement -Wno-pointer-sign \ +-Wmissing-prototypes -Wmissing-declarations \ +-Os -fno-builtin-strlen -finline-limit=0 -fomit-frame-pointer \ +-ffunction-sections -fdata-sections -fno-guess-branch-probability \ +-funsigned-char \ +-falign-functions=1 -falign-jumps=1 -falign-labels=1 -falign-loops=1 \ +-march=i386 -mpreferred-stack-boundary=2 \ +-Wl,-Map -Wl,link.map -Wl,--warn-common -Wl,--sort-common -Wl,--gc-sections \ +httpd_ratelimit_cgi.c -o httpd_ratelimit_cgi +*/ +#include +#include +#include +#include +#include +#include /* mkdir */ +#include + +static ssize_t full_write(int fd, const void *buf, size_t len) +{ + ssize_t cc; + ssize_t total; + + total = 0; + + while (len) { + cc = write(fd, buf, len); + + if (cc < 0) { + if (total) { + /* we already wrote some! */ + /* user can do another write to know the error code */ + return total; + } + return cc; /* write() returns -1 on failure. */ + } + + total += cc; + buf = ((const char *)buf) + cc; + len -= cc; + } + + return total; +} + +static void full_write2(int fd, const char *msg, const char *msg2) +{ + full_write(fd, msg, strlen(msg)); + full_write(fd, " '", 2); + full_write(fd, msg2, strlen(msg2)); + full_write(fd, "'\n", 2); +} + +static void write_and_die(int fd, const char *msg) +{ + full_write(fd, msg, strlen(msg)); + exit(0); +} + +static void write_and_die2(int fd, const char *msg, const char *msg2) +{ + full_write2(fd, msg, msg2); + exit(0); +} + +static void fmt_ul(char *dst, unsigned long n) +{ + char buf[sizeof(n)*3 + 2]; + char *p; + + p = buf + sizeof(buf) - 1; + *p = '\0'; + do { + *--p = (n % 10) + '0'; + n /= 10; + } while (n); + strcpy(dst, p); +} + +static long get_no(const char *s) +{ + const char *start = s; + long v = 0; + while (*s >= '0' && *s <= '9') + v = v * 10 + (*s++ - '0'); + if (start == s || *s != '\0' /*|| v < 0*/) + return -1; + return v; +} + +int main(int argc, char **argv) +{ + const char *lock_dir = "."; + unsigned long max_slots; + char *sp; + char *symno; + unsigned slot_num; + pid_t my_pid; + char my_pid_str[sizeof(long)*3]; + + argv++; + if (!argv[0] || !argv[1]) + write_and_die(2, "Usage: ratelimit [LOCKDIR] MAX_PROCS PROG [ARGS]\n"); + + /* ratelimit "[LOCKDIR] MAX_PROCS PROG" SHEBANG [ARGS] syntax? + * This happens if we are running as shebang file + * of the form "!#/path/to/ratelimit [/tmp/cgit] 10 CGI_BINARY" + * (in this case argv[1] is the shebang's filename) */ + sp = strchr(argv[0], ' '); + if (sp) { + *sp++ = '\0'; + /* convert to ratelimit "SOME\0THING" SHEBANG [ARGS] form */ + /* argv1 ^ */ + argv[1] = sp; + sp = strchr(sp, ' '); + if (sp) { /* "THING" also has a space? There is a LOCKDIR! */ + *sp++ = '\0'; + /* convert to ratelimit "SOME\0THI\0G" SHEBANG [ARGS] form */ + /* argv0^ ^argv1 */ + lock_dir = argv[0]; + argv[0] = argv[1]; + argv[1] = sp; + goto get_max; + } + } + + max_slots = get_no(argv[0]); + if (max_slots > 9999) { + /* ratelimit LOCKDIR MAX_PROCS PROG [ARGS] */ + lock_dir = argv[0]; + if (!lock_dir[0]) + write_and_die2(2, "Bad LOCKDIR", argv[0]); + argv++; + get_max: + max_slots = get_no(argv[0]); + if (max_slots > 9999) + write_and_die2(2, "Bad MAX_PROCS", argv[0]); + } + argv++; /* points to PROG [ARGS] */ + + { + char slot_path[strlen(lock_dir) + 16]; + symno = stpcpy(stpcpy(slot_path, lock_dir), "/lock."); + + my_pid = getpid(); + fmt_ul(my_pid_str, my_pid); + + /* Ensure lock directory exists (idempotent, ignores errors) */ + if (lock_dir[0] != '.' || lock_dir[1]) /* Don't bother with "." */ + mkdir(lock_dir, 0755); + + /* Starting slot varies per process */ + slot_num = my_pid; + + /* max_slots = 0 is allowed for testing */ + if (max_slots != 0) for (int i = 0; i < max_slots; i++) { + slot_num = (slot_num + 1) % max_slots; + fmt_ul(symno, slot_num); + + while (1) { + char buf[32]; + ssize_t len; + long old_pid; + + /* Try to claim atomically */ + if (symlink(my_pid_str, slot_path) == 0) + goto exec; + + /* Only handle EEXIST - other errors skip to next slot */ + if (errno != EEXIST) + break; + + /* Read existing target PID */ + len = readlink(slot_path, buf, sizeof(buf) - 1); + if (len < 1) { + /* Broken/empty - clean up and retry */ + unlink(slot_path); + continue; + } + buf[len] = '\0'; + + /* Parse PID */ + old_pid = get_no(buf); + if (old_pid <= 0 || old_pid > INT_MAX) { + /* Invalid PID string - clean up and retry */ + unlink(slot_path); + continue; + } + + /* Check if old process is alive */ + if (kill(old_pid, 0) == 0 || errno != ESRCH) { + /* Alive (or unexpected error): slot in use, try next */ + break; + } + + /* Dead: clean up and retry this slot */ + unlink(slot_path); + /* Loop continues to retry symlink() */ + } + } + + /* No slot available, return 429 */ + write_and_die(1, "HTTP/1.1 429 Too Many Requests\r\n" + "Content-Type: text/plain\r\n" + "Retry-After: 60\r\n" + "Connection: close\r\n\r\n" + "Too many concurrent requests\n" + ); + return 0; + } + +exec: + execv(argv[0], argv); + full_write2(2, "can't execute", argv[0]); + write_and_die(1, "HTTP/1.1 500 Internal Server Error\r\n" + "Content-Type: text/plain\r\n\r\n" + "Failed to execute binary\n"); + return 1; +} \ No newline at end of file -- cgit v1.2.3-55-g6feb From 71c703c26b1c783a6ba97289416e809db0200aba Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Wed, 21 Jan 2026 19:01:42 +0100 Subject: httpd: remove one close() from main loop function old new delta httpd_main 701 710 +9 Signed-off-by: Denys Vlasenko --- networking/httpd.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/networking/httpd.c b/networking/httpd.c index e96916480..94f6933f4 100644 --- a/networking/httpd.c +++ b/networking/httpd.c @@ -2651,6 +2651,7 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) static void mini_httpd(int server_socket) NORETURN; static void mini_httpd(int server_socket) { + xmove_fd(server_socket, 0); /* NB: it's best to not use xfuncs in this loop before fork(). * Otherwise server may die on transient errors (temporary * out-of-memory condition, etc), which is Bad(tm). @@ -2662,7 +2663,7 @@ static void mini_httpd(int server_socket) /* Wait for connections... */ fromAddr.len = LSA_SIZEOF_SA; - n = accept(server_socket, &fromAddr.u.sa, &fromAddr.len); + n = accept(0, &fromAddr.u.sa, &fromAddr.len); if (n < 0) continue; //TODO: we can reject connects from denied IPs right away; @@ -2681,8 +2682,7 @@ static void mini_httpd(int server_socket) /* Do not reload config on HUP */ //TODO: can make reload handler check the pid and do nothing in children? signal(SIGHUP, SIG_IGN); -//TODO: can move server_socket to fd 0, making the close here unnecessary? - close(server_socket); + /* close(0); - server socket. The next line does this for free */ xmove_fd(n, 0); xdup2(0, 1); @@ -2703,6 +2703,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])); + xmove_fd(server_socket, 0); /* NB: it's best to not use xfuncs in this loop before vfork(). * Otherwise server may die on transient errors (temporary * out-of-memory condition, etc), which is Bad(tm). @@ -2712,7 +2713,7 @@ static void mini_httpd_nommu(int server_socket, int argc, char **argv) int n; /* Wait for connections... */ - n = accept(server_socket, NULL, NULL); + n = accept(0, NULL, NULL); if (n < 0) continue; @@ -2723,7 +2724,7 @@ static void mini_httpd_nommu(int server_socket, int argc, char **argv) /* child */ /* Do not reload config on HUP */ signal(SIGHUP, SIG_IGN); - close(server_socket); + /* close(0); - server socket. The next line does this for free */ xmove_fd(n, 0); xdup2(0, 1); -- cgit v1.2.3-55-g6feb From 5ac9d0a865d0d9f8c0281d6111e352492b2cd6d2 Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Wed, 21 Jan 2026 19:16:29 +0100 Subject: httpd: stop disabling/enabling SIGHUP in every child function old new delta sighup_handler 30 47 +17 httpd_main 710 695 -15 ------------------------------------------------------------------------------ (add/remove: 0/0 grow/shrink: 1/1 up/down: 17/-15) Total: 2 bytes Signed-off-by: Denys Vlasenko --- networking/httpd.c | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/networking/httpd.c b/networking/httpd.c index 94f6933f4..49f60d967 100644 --- a/networking/httpd.c +++ b/networking/httpd.c @@ -478,6 +478,8 @@ struct globals { IF_FEATURE_HTTPD_BASIC_AUTH(const char *g_realm;) IF_FEATURE_HTTPD_BASIC_AUTH(char *remoteuser;) + pid_t parent_pid; + off_t file_size; /* -1 - unknown */ #if ENABLE_FEATURE_HTTPD_RANGES off_t range_start; @@ -1712,7 +1714,7 @@ static void send_cgi_and_exit( bb_signals(0 | (1 << SIGCHLD) | (1 << SIGPIPE) - | (1 << SIGHUP) + /*| (1 << SIGHUP) - not needed, we have a handler, and exec resets signals with handlers to DFL */ , SIG_DFL); /* User seeing stderr output can be a security problem. * If CGI really wants that, it can always do dup itself. */ @@ -2680,8 +2682,7 @@ static void mini_httpd(int server_socket) if (fork() == 0) { /* child */ /* Do not reload config on HUP */ -//TODO: can make reload handler check the pid and do nothing in children? - signal(SIGHUP, SIG_IGN); + /*signal(SIGHUP, SIG_IGN); - not needed, handler is a NOP in children (checks pid) */ /* close(0); - server socket. The next line does this for free */ xmove_fd(n, 0); xdup2(0, 1); @@ -2723,7 +2724,7 @@ static void mini_httpd_nommu(int server_socket, int argc, char **argv) if (vfork() == 0) { /* child */ /* Do not reload config on HUP */ - signal(SIGHUP, SIG_IGN); + /*signal(SIGHUP, SIG_IGN); - not needed, handler is a NOP in children (checks pid) */ /* close(0); - server socket. The next line does this for free */ xmove_fd(n, 0); xdup2(0, 1); @@ -2757,9 +2758,11 @@ static void mini_httpd_inetd(void) static void sighup_handler(int sig UNUSED_PARAM) { - int sv = errno; - parse_conf(DEFAULT_PATH_HTTPD_CONF, SIGNALED_PARSE); - errno = sv; + if (G.parent_pid == getpid()) { + int sv = errno; + parse_conf(DEFAULT_PATH_HTTPD_CONF, SIGNALED_PARSE); + errno = sv; + } } enum { @@ -2904,8 +2907,10 @@ int httpd_main(int argc UNUSED_PARAM, char **argv) #endif parse_conf(DEFAULT_PATH_HTTPD_CONF, FIRST_PARSE); - if (!(opt & OPT_INETD)) + if (!(opt & OPT_INETD)) { + G.parent_pid = getpid(); signal(SIGHUP, sighup_handler); + } xfunc_error_retval = 0; if (opt & OPT_INETD) -- cgit v1.2.3-55-g6feb From 3f36fe9c2f93d3c1409cc831a7dfb49b0d1998fd Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Thu, 22 Jan 2026 01:58:46 +0100 Subject: httpd: add -M MAXCONN - do not accept unlimited number of connections We were lacking even basic rate-limiting. function old new delta httpd_main 648 915 +267 handle_incoming_and_exit 2235 2264 +29 packed_usage 35868 35894 +26 cgi_io_loop_and_exit 537 552 +15 send_headers 704 712 +8 get_line 106 108 +2 .rodata 106829 106830 +1 send_file_and_exit 890 887 -3 mini_httpd 161 - -161 ------------------------------------------------------------------------------ (add/remove: 0/1 grow/shrink: 7/1 up/down: 348/-164) Total: 184 bytes Signed-off-by: Denys Vlasenko --- networking/httpd.c | 105 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 86 insertions(+), 19 deletions(-) diff --git a/networking/httpd.c b/networking/httpd.c index 49f60d967..39a7046f5 100644 --- a/networking/httpd.c +++ b/networking/httpd.c @@ -267,6 +267,7 @@ //usage: "[-ifv[v]]" //usage: " [-c CONFFILE]" //usage: " [-p [IP:]PORT]" +//usage: " [-M MAXCONN]" //usage: IF_FEATURE_HTTPD_SETUID(" [-u USER[:GRP]]") //usage: IF_FEATURE_HTTPD_BASIC_AUTH(" [-r REALM]") //usage: " [-h HOME]\n" @@ -277,6 +278,7 @@ //usage: "\n -f Run in foreground" //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_SETUID( //usage: "\n -u USER[:GRP] Set uid/gid after binding to port") //usage: IF_FEATURE_HTTPD_BASIC_AUTH( @@ -479,6 +481,8 @@ struct globals { IF_FEATURE_HTTPD_BASIC_AUTH(char *remoteuser;) pid_t parent_pid; + int children_fd; + int conn_limit; off_t file_size; /* -1 - unknown */ #if ENABLE_FEATURE_HTTPD_RANGES @@ -494,7 +498,6 @@ struct globals { #if ENABLE_FEATURE_HTTPD_CONFIG_WITH_SCRIPT_INTERPR Htaccess *script_i; /* config script interpreters */ #endif - char *iobuf; /* [IOBUF_SIZE] */ #define hdr_buf bb_common_bufsiz1 #define sizeof_hdr_buf COMMON_BUFSIZE char *hdr_ptr; @@ -508,6 +511,7 @@ struct globals { #if ENABLE_FEATURE_HTTPD_PROXY Htaccess_Proxy *proxy; #endif + char iobuf[IOBUF_SIZE]; }; #define G (*ptr_to_globals) #define verbose (G.verbose ) @@ -543,11 +547,11 @@ enum { #define g_auth (G.g_auth ) #define mime_a (G.mime_a ) #define script_i (G.script_i ) -#define iobuf (G.iobuf ) #define hdr_ptr (G.hdr_ptr ) #define hdr_cnt (G.hdr_cnt ) #define http_error_page (G.http_error_page ) #define proxy (G.proxy ) +#define iobuf (G.iobuf ) #define INIT_G() do { \ setup_common_bufsiz(); \ SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \ @@ -1045,7 +1049,8 @@ static void decodeBase64(char *data) */ static int openServer(void) { - unsigned n = bb_strtou(bind_addr_or_port, NULL, 10); + unsigned n; + n = bb_strtou(bind_addr_or_port, NULL, 10); if (!errno && n && n <= 0xffff) n = create_and_bind_stream_or_die(NULL, n); else @@ -2227,10 +2232,6 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) #endif char *HTTP_slash; - /* Allocation of iobuf is postponed until now - * (IOW, server process doesn't need to waste 8k) */ - iobuf = xmalloc(IOBUF_SIZE); - if (ENABLE_FEATURE_HTTPD_CGI || DEBUG || verbose) { /* NB: can be NULL (user runs httpd -i by hand?) */ rmt_ip_str = xmalloc_sockaddr2dotted(&fromAddr->u.sa); @@ -2643,6 +2644,57 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) ); } +static int count_children(void) +{ + int count; + int sz = pread(G.children_fd, iobuf, IOBUF_SIZE - 1, 0); + if (sz < 0) + return -1; + /* The actual format is "NUM NUM ", but future-proof for lack of last space, and for '\n' */ + count = 0; + if (sz > 0) { + char *p = iobuf; + iobuf[sz] = '\n'; + do { + if (*++p == ' ') + count++, p++; + } while (*p != '\n'); + if (p[-1] != ' ') /* it was "NUM NUM\n" (not "NUM NUM \n")? */ + count++; /* there were (NUMSPACES + 1) pids */ + } + return count; +} + +static int throttle_if_conn_limit(void) +{ + unsigned usec = 0xffff; /* 0.065535 seconds */ + int countdown, c; + for (;;) { + countdown = G.conn_limit; + c = count_children(); + if (c < 0) + break; /* can't count them */ + countdown -= c; + if (countdown <= 0) { /* we are at MAXCONN, pause until we are below */ + bb_error_msg("pausing, children:%u", c); + //bb_error_msg("pausing %ums, children:%u", usec/1000, c); + usleep(usec); + usec = ((usec << 1) | 1) & 0x1fffff; /* x2, up to 2.097151 seconds */ + continue; /* loop: count them again */ + } + if (VERBOSE_3) /* -vvv periodically shows # of children */ + bb_error_msg("children:%u", c ? c - 1 : c); +// "Why minus one?!" you ask. We _just now_ forked (see the caller) +// and immediately checked the # of children. +// Of course, the just-forked child did not have time to complete. +// Without "minus one" hack, this causes above statement to always show +// at least one child, gives wrong impression to the log's reader +// (looks like one child is stuck). + break; + } + return countdown; +} + /* * The main http server function. * Given a socket, listen for new connections and farm out @@ -2653,16 +2705,22 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) static void mini_httpd(int server_socket) NORETURN; static void mini_httpd(int server_socket) { + int countdown; + xmove_fd(server_socket, 0); /* NB: it's best to not use xfuncs in this loop before fork(). * Otherwise server may die on transient errors (temporary * out-of-memory condition, etc), which is Bad(tm). * Try to do any dangerous calls after fork. */ + countdown = G.conn_limit; while (1) { int n; len_and_sockaddr fromAddr; + if (G.children_fd > 0 && --countdown < 0) + countdown = throttle_if_conn_limit(); + /* Wait for connections... */ fromAddr.len = LSA_SIZEOF_SA; n = accept(0, &fromAddr.u.sa, &fromAddr.len); @@ -2698,6 +2756,7 @@ static void mini_httpd(int server_socket) static void mini_httpd_nommu(int server_socket, int argc, char **argv) NORETURN; static void mini_httpd_nommu(int server_socket, int argc, char **argv) { + int countdown; char *argv_copy[argc + 2]; argv_copy[0] = argv[0]; @@ -2710,9 +2769,13 @@ static void mini_httpd_nommu(int server_socket, int argc, char **argv) * out-of-memory condition, etc), which is Bad(tm). * Try to do any dangerous calls after fork. */ + countdown = G.conn_limit; while (1) { int n; + if (G.children_fd > 0 && --countdown < 0) + countdown = throttle_if_conn_limit(); + /* Wait for connections... */ n = accept(0, NULL, NULL); if (n < 0) @@ -2774,9 +2837,10 @@ enum { IF_FEATURE_HTTPD_AUTH_MD5( m_opt_md5 ,) IF_FEATURE_HTTPD_SETUID( u_opt_setuid ,) p_opt_port , - p_opt_inetd , - p_opt_foreground, - p_opt_verbose , + M_opt_maxconn , + i_opt_inetd , + f_opt_foreground, + v_opt_verbose , OPT_CONFIG_FILE = 1 << c_opt_config_file, OPT_DECODE_URL = 1 << d_opt_decode_url, OPT_HOME_HTTPD = 1 << h_opt_home_httpd, @@ -2785,9 +2849,9 @@ enum { OPT_MD5 = IF_FEATURE_HTTPD_AUTH_MD5( (1 << m_opt_md5 )) + 0, OPT_SETUID = IF_FEATURE_HTTPD_SETUID( (1 << u_opt_setuid )) + 0, OPT_PORT = 1 << p_opt_port, - OPT_INETD = 1 << p_opt_inetd, - OPT_FOREGROUND = 1 << p_opt_foreground, - OPT_VERBOSE = 1 << p_opt_verbose, + OPT_INETD = 1 << i_opt_inetd, + OPT_FOREGROUND = 1 << f_opt_foreground, + OPT_VERBOSE = 1 << v_opt_verbose, }; @@ -2809,6 +2873,7 @@ int httpd_main(int argc UNUSED_PARAM, char **argv) setlocale(LC_TIME, "C"); #endif + G.conn_limit = 256; home_httpd = xrealloc_getcwd_or_warn(NULL); /* We do not "absolutize" path given by -h (home) opt. * If user gives relative path in -h, @@ -2819,7 +2884,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:ifv" + "p:M:+ifv" "\0" /* -v counts, -i implies -f */ "vv:if", @@ -2829,6 +2894,7 @@ int httpd_main(int argc UNUSED_PARAM, char **argv) IF_FEATURE_HTTPD_AUTH_MD5(, &pass) IF_FEATURE_HTTPD_SETUID(, &s_ugid) , &bind_addr_or_port + , &G.conn_limit , &verbose ); if (opt & OPT_DECODE_URL) { @@ -2871,6 +2937,10 @@ int httpd_main(int argc UNUSED_PARAM, char **argv) xchdir(home_httpd); if (!(opt & OPT_INETD)) { + G.parent_pid = getpid(); + sprintf(iobuf, "/proc/self/task/%u/children", (unsigned)G.parent_pid); + G.children_fd = open(iobuf, O_RDONLY | O_CLOEXEC); + /* Make it unnecessary to wait for children */ signal(SIGCHLD, SIG_IGN); @@ -2905,16 +2975,13 @@ int httpd_main(int argc UNUSED_PARAM, char **argv) // setenv_long("SERVER_PORT", ???); } #endif - parse_conf(DEFAULT_PATH_HTTPD_CONF, FIRST_PARSE); - if (!(opt & OPT_INETD)) { - G.parent_pid = getpid(); - signal(SIGHUP, sighup_handler); - } xfunc_error_retval = 0; if (opt & OPT_INETD) mini_httpd_inetd(); /* never returns */ + + signal(SIGHUP, sighup_handler); #if BB_MMU if (!(opt & OPT_FOREGROUND)) bb_daemonize(0); /* don't change current directory */ -- cgit v1.2.3-55-g6feb From 649da41ca4cde6d3f56b78170c370045b7857738 Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Thu, 22 Jan 2026 10:42:20 +0100 Subject: httpd: fix compilation script of httpd_ratelimit_cgi.c Signed-off-by: Denys Vlasenko --- networking/httpd.c | 2 +- networking/httpd_helpers.sh | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/networking/httpd.c b/networking/httpd.c index 39a7046f5..4052cdce0 100644 --- a/networking/httpd.c +++ b/networking/httpd.c @@ -2679,7 +2679,7 @@ static int throttle_if_conn_limit(void) bb_error_msg("pausing, children:%u", c); //bb_error_msg("pausing %ums, children:%u", usec/1000, c); usleep(usec); - usec = ((usec << 1) | 1) & 0x1fffff; /* x2, up to 2.097151 seconds */ + usec = ((usec << 1) + 1) & 0x1fffff; /* x2, up to 2.097151 seconds */ continue; /* loop: count them again */ } if (VERBOSE_3) /* -vvv periodically shows # of children */ diff --git a/networking/httpd_helpers.sh b/networking/httpd_helpers.sh index d2c639840..038d2d27f 100755 --- a/networking/httpd_helpers.sh +++ b/networking/httpd_helpers.sh @@ -25,5 +25,7 @@ httpd_ssi.c -o httpd_ssi && strip httpd_ssi ${PREFIX}gcc \ ${OPTS} \ --Wl,-Map -Wl,httpd_ssi.map \ +-Wl,-Map -Wl,httpd_ratelimit_cgi.map \ httpd_ratelimit_cgi.c -o httpd_ratelimit_cgi && strip httpd_ratelimit_cgi + +size index.cgi httpd_ssi httpd_ratelimit_cgi -- cgit v1.2.3-55-g6feb From 851992f070fabcd3d559d67fad6ef642045e6ff0 Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Thu, 22 Jan 2026 13:58:28 +0100 Subject: httpd: allow http2 requests if proxying, tighten METHOD checks function old new delta handle_incoming_and_exit 2264 2312 +48 httpd_main 915 913 -2 ------------------------------------------------------------------------------ (add/remove: 0/0 grow/shrink: 1/1 up/down: 48/-2) Total: 46 bytes Signed-off-by: Denys Vlasenko --- networking/httpd.c | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/networking/httpd.c b/networking/httpd.c index 4052cdce0..6b533b473 100644 --- a/networking/httpd.c +++ b/networking/httpd.c @@ -2210,6 +2210,7 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) #endif #if ENABLE_FEATURE_HTTPD_CGI unsigned total_headers_len; + unsigned un; #endif const char *prequest; static const char request_GET[] ALIGN1 = "GET"; @@ -2280,14 +2281,12 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) /* Find URL */ // rfc2616: method and URI is separated by exactly one space - //urlp = strpbrk(iobuf, " \t"); - no, tab isn't allowed + // no tabs, no double spaces, no empty methods, URIs, etc: + // should be "METHOD /URI HTTP/xyz" ("\r\n" stripped by get_line) urlp = strchr(iobuf, ' '); if (urlp == NULL) send_headers_and_exit(HTTP_BAD_REQUEST); *urlp++ = '\0'; - //urlp = skip_whitespace(urlp); - should not be necessary - if (urlp[0] != '/') - send_headers_and_exit(HTTP_BAD_REQUEST); /* Find end of URL */ HTTP_slash = strchr(urlp, ' '); /* Is it " HTTP/"? */ @@ -2324,26 +2323,34 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) urlp + strlen(proxy_entry->url_from), /* "SFX" */ HTTP_slash /* "HTTP/xyz" */ ); + /* 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); } #endif + /* We don't support http2 "*" URI, enforce "/URI" form */ + if (urlp[0] != '/') + send_headers_and_exit(HTTP_BAD_REQUEST); - /* Determine type of request (GET/POST/...) */ + /* Determine METHOD of request (GET/POST/...). Case-sensitive (rfc7230,rfc9110) */ prequest = request_GET; - if (strcasecmp(iobuf, prequest) == 0) + if (strcmp(iobuf, prequest) == 0) goto found; prequest = request_HEAD; - if (strcasecmp(iobuf, prequest) == 0) + if (strcmp(iobuf, prequest) == 0) goto found; #if !ENABLE_FEATURE_HTTPD_CGI send_headers_and_exit(HTTP_NOT_IMPLEMENTED); #else prequest = request_POST; - if (strcasecmp(iobuf, prequest) == 0) + if (strcmp(iobuf, prequest) == 0) goto found; /* For CGI, allow DELETE, PUT, OPTIONS, etc too */ prequest = alloca(16); - safe_strncpy((char*)prequest, iobuf, 16); + un = safe_strncpy((char*)prequest, iobuf, 16) - prequest; + if (un < 1 || un >= 15) + send_headers_and_exit(HTTP_BAD_REQUEST); #endif found: /* Copy URL to stack-allocated char[] */ -- cgit v1.2.3-55-g6feb From f0a63eefae8af5989da4285416b371481fed4f46 Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Thu, 22 Jan 2026 14:21:55 +0100 Subject: httpd: reject request line and headers with control chars This removes the need to check various corner cases later. function old new delta get_line 108 130 +22 Signed-off-by: Denys Vlasenko --- networking/httpd.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/networking/httpd.c b/networking/httpd.c index 6b533b473..73f00ef39 100644 --- a/networking/httpd.c +++ b/networking/httpd.c @@ -1310,6 +1310,7 @@ static void send_headers_and_exit(int responseNum) * Read from the socket until '\n' or EOF. * '\r' chars are removed. * '\n' is replaced with NUL. + * Control chars and > 0x7e cause HTTP_BAD_REQUEST abort. * Return number of characters read or 0 if nothing is read * ('\r' and '\n' are not counted). * Data is returned in iobuf. @@ -1330,10 +1331,21 @@ static unsigned get_line(void) } hdr_cnt--; c = *hdr_ptr++; + + /* We really should only accept \n (for people debugging via telnet) + * and \r\n, but... \r\n can split across read(), harder to check. */ if (c == '\r') continue; if (c == '\n') break; + + /* rfc7230 allows tabs for header line continuation and as whitespace in values */ + if (c != '\t') { + /* Control chars aren't allowed in headers */ + if ((unsigned char)c < ' ' || (unsigned char)c == 0x7f) + send_headers_and_exit(HTTP_BAD_REQUEST); + /* hign bytes above 0x7f are heavily discouraged, but historically allowed */ + } iobuf[count] = c; if (count < (IOBUF_SIZE - 1)) /* check overflow */ count++; -- cgit v1.2.3-55-g6feb From 52a88341d6782569ec4f29d5ffde1a246c296c2b Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Thu, 22 Jan 2026 15:33:42 +0100 Subject: httpd: when reading headers, abort if they are too long function old new delta get_line 130 134 +4 Signed-off-by: Denys Vlasenko --- networking/httpd.c | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/networking/httpd.c b/networking/httpd.c index 73f00ef39..d8d0f8dd9 100644 --- a/networking/httpd.c +++ b/networking/httpd.c @@ -1308,20 +1308,22 @@ static void send_headers_and_exit(int responseNum) /* * Read from the socket until '\n' or EOF. + * Data is returned in iobuf[]. * '\r' chars are removed. * '\n' is replaced with NUL. - * Control chars and > 0x7e cause HTTP_BAD_REQUEST abort. + * Control chars and 0x7f cause HTTP_BAD_REQUEST abort. + * iobuf[] overflow causes HTTP_BAD_REQUEST abort. * Return number of characters read or 0 if nothing is read * ('\r' and '\n' are not counted). - * Data is returned in iobuf. */ static unsigned get_line(void) { unsigned count; - char c; count = 0; while (1) { + unsigned char c; + if (hdr_cnt <= 0) { alarm(HEADER_READ_TIMEOUT); hdr_cnt = safe_read(STDIN_FILENO, hdr_buf, sizeof_hdr_buf); @@ -1342,13 +1344,13 @@ static unsigned get_line(void) /* rfc7230 allows tabs for header line continuation and as whitespace in values */ if (c != '\t') { /* Control chars aren't allowed in headers */ - if ((unsigned char)c < ' ' || (unsigned char)c == 0x7f) + if (c < ' ' || c == 0x7f) send_headers_and_exit(HTTP_BAD_REQUEST); /* hign bytes above 0x7f are heavily discouraged, but historically allowed */ } - iobuf[count] = c; - if (count < (IOBUF_SIZE - 1)) /* check overflow */ - count++; + iobuf[count++] = c; + if (count >= IOBUF_SIZE) + send_headers_and_exit(HTTP_BAD_REQUEST); } ret: iobuf[count] = '\0'; -- cgit v1.2.3-55-g6feb From c1ed0c4049ad40750218393b9d102c35a75cd6d4 Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Thu, 22 Jan 2026 15:49:58 +0100 Subject: httpd: optimize header reading timeout code Set up the signal handler once, outside of main loop. Do not reset the timer if headers are big - they still are expected to arrive quickly. function old new delta httpd_main 913 939 +26 handle_incoming_and_exit 2312 2306 -6 get_line 134 126 -8 ------------------------------------------------------------------------------ (add/remove: 0/0 grow/shrink: 1/2 up/down: 26/-14) Total: 12 bytes Signed-off-by: Denys Vlasenko --- networking/httpd.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/networking/httpd.c b/networking/httpd.c index d8d0f8dd9..41289ae7f 100644 --- a/networking/httpd.c +++ b/networking/httpd.c @@ -322,7 +322,7 @@ #define IOBUF_SIZE 8192 #define MAX_HTTP_HEADERS_SIZE (32*1024) -#define HEADER_READ_TIMEOUT 60 +#define HEADER_READ_TIMEOUT 30 #define STR1(s) #s #define STR(s) STR1(s) @@ -1325,7 +1325,6 @@ static unsigned get_line(void) unsigned char c; if (hdr_cnt <= 0) { - alarm(HEADER_READ_TIMEOUT); hdr_cnt = safe_read(STDIN_FILENO, hdr_buf, sizeof_hdr_buf); if (hdr_cnt <= 0) goto ret; @@ -2273,8 +2272,8 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) if_ip_denied_send_HTTP_FORBIDDEN_and_exit(remote_ip); #endif - /* Install timeout handler. get_line() needs it. */ - signal(SIGALRM, send_REQUEST_TIMEOUT_and_exit); + /* Limit how long we expect clients to be sending headers */ + alarm(HEADER_READ_TIMEOUT); if (!get_line()) { /* EOF or error or empty line */ /* Observed Firefox to "speculatively" open @@ -2728,6 +2727,8 @@ static void mini_httpd(int server_socket) { int countdown; + signal(SIGALRM, send_REQUEST_TIMEOUT_and_exit); + xmove_fd(server_socket, 0); /* NB: it's best to not use xfuncs in this loop before fork(). * Otherwise server may die on transient errors (temporary @@ -2784,6 +2785,9 @@ 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);*/ + /* ^^^ WRONG. mini_httpd_inetd() does this */ + xmove_fd(server_socket, 0); /* NB: it's best to not use xfuncs in this loop before vfork(). * Otherwise server may die on transient errors (temporary @@ -2833,6 +2837,8 @@ static void mini_httpd_inetd(void) { len_and_sockaddr fromAddr; + signal(SIGALRM, send_REQUEST_TIMEOUT_and_exit); + memset(&fromAddr, 0, sizeof(fromAddr)); fromAddr.len = LSA_SIZEOF_SA; /* NB: can fail if user runs it by hand and types in http cmds */ -- cgit v1.2.3-55-g6feb From d8059bd827c61c66122a1bb34d5dc4f47448301c Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Thu, 22 Jan 2026 17:22:06 +0100 Subject: httpd: time out data writes after 60 seconds of no progress function old new delta prepare_write_timeout - 29 +29 static.tv - 16 +16 httpd_main 939 950 +11 send_file_and_exit 887 891 +4 handle_incoming_and_exit 2306 2298 -8 cgi_io_loop_and_exit 552 535 -17 ------------------------------------------------------------------------------ (add/remove: 2/0 grow/shrink: 2/2 up/down: 60/-25) Total: 35 bytes Signed-off-by: Denys Vlasenko --- networking/httpd.c | 58 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/networking/httpd.c b/networking/httpd.c index 41289ae7f..87ae969bf 100644 --- a/networking/httpd.c +++ b/networking/httpd.c @@ -323,6 +323,7 @@ #define MAX_HTTP_HEADERS_SIZE (32*1024) #define HEADER_READ_TIMEOUT 30 +#define DATA_WRITE_TIMEOUT 60 #define STR1(s) #s #define STR(s) STR1(s) @@ -1067,7 +1068,7 @@ static void log_and_exit(void) { /* Paranoia. IE said to be buggy. It may send some extra data * or be confused by us just exiting without SHUT_WR. Oh well. */ - shutdown(1, SHUT_WR); + shutdown(STDOUT_FILENO, SHUT_WR); /* Why?? (this also messes up stdin when user runs httpd -i from terminal) ndelay_on(0); @@ -1370,10 +1371,6 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post /* 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); - // We inconsistently handle a case when more POSTDATA from network // is coming than we expected. We may give *some part* of that // extra data to CGI. @@ -1814,9 +1811,6 @@ static NOINLINE void send_file_and_exit(const char *url, int what) send_headers_and_exit(HTTP_NOT_MODIFIED); } #endif - /* If you want to know about EPIPE below - * (happens if you abort downloads from local httpd): */ - signal(SIGPIPE, SIG_IGN); /* If not found, default is to not send "Content-type:" */ /*found_mime_type = NULL; - already is */ @@ -1910,6 +1904,8 @@ static NOINLINE void send_file_and_exit(const char *url, int what) #endif if (what & SEND_HEADERS) send_headers(HTTP_OK); + + /* Sending BODY */ #if ENABLE_FEATURE_USE_SENDFILE { off_t offset; @@ -1930,6 +1926,9 @@ static NOINLINE void send_file_and_exit(const char *url, int what) break; /* fall back to read/write loop */ if (VERBOSE_1) bb_simple_perror_msg("sendfile error"); +// SO_SNDTIME on our socket causes write timeouts manifest as EAGAIN "Resource temporarily unavailable". +// Not the best error message (when reading the log: "what resource?!") +// "timeout" is better - special-case it? log_and_exit(); } IF_FEATURE_HTTPD_RANGES(range_len -= count;) @@ -1942,8 +1941,12 @@ static NOINLINE void send_file_and_exit(const char *url, int what) ssize_t n; IF_FEATURE_HTTPD_RANGES(if (count > range_len) count = range_len;) n = full_write(STDOUT_FILENO, iobuf, count); - if (count != n) + if (count != n) { + if (VERBOSE_1 && n < 0) + bb_simple_perror_msg("write error"); +// see above about SO_SNDTIME break; + } IF_FEATURE_HTTPD_RANGES(range_len -= count;) if (range_len == 0) break; @@ -2205,7 +2208,25 @@ static Htaccess_Proxy *find_proxy_entry(const char *url) static void send_REQUEST_TIMEOUT_and_exit(int sig) NORETURN; static void send_REQUEST_TIMEOUT_and_exit(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(xfunc_error_retval); +} + +static void prepare_write_timeout(void) +{ + static const struct timeval tv = { .tv_sec = DATA_WRITE_TIMEOUT, .tv_usec = 0 }; + + /* stop header read timeout */ + alarm(0); + + /* make write() calls exit with error after DATA_WRITE_TIMEOUT */ + setsockopt(STDOUT_FILENO, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + /* this is less expensive than arming alarm() before every write */ } /* @@ -2323,8 +2344,10 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) send_headers_and_exit(HTTP_INTERNAL_SERVER_ERROR); if (connect(proxy_fd, &lsa->u.sa, lsa->len) < 0) send_headers_and_exit(HTTP_INTERNAL_SERVER_ERROR); - /* Disable peer header reading timeout */ - alarm(0); + + /* Disable header reading timeout */ + prepare_write_timeout(); + /* Config directive was of the form: * P:/url:[http://]hostname[:port]/new/path * When /urlSFX is requested, reverse proxy it @@ -2615,8 +2638,8 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) #endif } /* while extra header reading */ - /* We are done reading headers, disable peer timeout */ - alarm(0); + /* We are done reading headers, disable header timeout */ + prepare_write_timeout(); if (strcmp(bb_basename(urlcopy), HTTPD_CONF) == 0) { /* protect listing [/path]/httpd.conf or IP deny */ @@ -3004,6 +3027,15 @@ int httpd_main(int argc UNUSED_PARAM, char **argv) #endif parse_conf(DEFAULT_PATH_HTTPD_CONF, FIRST_PARSE); + /* 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); + /* If a _local_ simple file download (not CGI) from local httpd + * is aborted, you also can get SIGPIPE. + * Disabling it converts SIGPIPE into EPIPE error from sendfile/write. + * We handle that correctly. Hopefully. Maybe. + */ + xfunc_error_retval = 0; if (opt & OPT_INETD) mini_httpd_inetd(); /* never returns */ -- cgit v1.2.3-55-g6feb From 04d8729adb52be51a5987ced811fa6811b9d5c3f Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Thu, 22 Jan 2026 21:21:16 +0100 Subject: httpd: make timeout messages less confusing function old new delta send_file_and_exit 891 930 +39 Signed-off-by: Denys Vlasenko --- networking/httpd.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/networking/httpd.c b/networking/httpd.c index 87ae969bf..4c04df86f 100644 --- a/networking/httpd.c +++ b/networking/httpd.c @@ -1924,11 +1924,13 @@ static NOINLINE void send_file_and_exit(const char *url, int what) if (count < 0) { if (offset == range_start) /* was it the very 1st sendfile? */ break; /* fall back to read/write loop */ - if (VERBOSE_1) - bb_simple_perror_msg("sendfile error"); + if (VERBOSE_1) { + if (errno == EAGAIN) + errno = ETIMEDOUT; // SO_SNDTIME on our socket causes write timeouts manifest as EAGAIN "Resource temporarily unavailable". -// Not the best error message (when reading the log: "what resource?!") -// "timeout" is better - special-case it? +// Not the best error message (when reading the log: "Er... what resource?!") + bb_simple_perror_msg("sendfile error"); + } log_and_exit(); } IF_FEATURE_HTTPD_RANGES(range_len -= count;) @@ -1942,9 +1944,11 @@ static NOINLINE void send_file_and_exit(const char *url, int what) IF_FEATURE_HTTPD_RANGES(if (count > range_len) count = range_len;) n = full_write(STDOUT_FILENO, iobuf, count); if (count != n) { - if (VERBOSE_1 && n < 0) + if (VERBOSE_1 && n < 0) { + if (errno == EAGAIN) + errno = ETIMEDOUT; bb_simple_perror_msg("write error"); -// see above about SO_SNDTIME + } break; } IF_FEATURE_HTTPD_RANGES(range_len -= count;) -- cgit v1.2.3-55-g6feb From 2d78809b74c3849dfc0aedca8a1ce959d1e783ec Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Thu, 22 Jan 2026 22:31:36 +0100 Subject: httpd: do not force clean connection termination on write errors and timeouts function old new delta send_file_and_exit 891 931 +40 send_EOF_and_exit - 14 +14 cgi_io_loop_and_exit 535 538 +3 log_and_exit 44 33 -11 ------------------------------------------------------------------------------ (add/remove: 1/0 grow/shrink: 2/1 up/down: 57/-11) Total: 46 bytes Signed-off-by: Denys Vlasenko --- networking/httpd.c | 69 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/networking/httpd.c b/networking/httpd.c index 4c04df86f..ed6b0682e 100644 --- a/networking/httpd.c +++ b/networking/httpd.c @@ -290,7 +290,29 @@ //usage: "\n -e STRING HTML encode STRING" //usage: "\n -d STRING URL decode STRING" -/* TODO: use TCP_CORK, parse_config() */ +/* Robustness + * + * Even though the design is geared towards simplicity, it is meant to + * survive in a mildly adversarial environment: + * - it does not accept unlimited number of connections (-M MAXCONN) + * - it requires clients to send their incoming requests quickly + * (HEADER_READ_TIMEOUT) + * - if client stops consuming its result ("stalled receiver" attack), + * the server will drop the connection (DATA_WRITE_TIMEOUT) + * + * To limit the number of simultaneously running CGI children, + * you may use the helper tool, httpd_ratelimit_cgi.c: + * it replies with "429 Too Many Requests" and exits when limit is exceeded. + * The limit can be set per CGI type: "I accept up to 256 connections, + * but only up to 100 of them may be to /cgi-bin/simple_cgi, + * and 20 to /cgi-bin/HEAVY_cgi". + * + * TODO: + * - do not allow all MAXCONN connections be taken up by the same remote IP. + */ +#define HEADER_READ_TIMEOUT 30 +#define DATA_WRITE_TIMEOUT 60 + #include "libbb.h" #include "common_bufsiz.h" @@ -322,9 +344,6 @@ #define IOBUF_SIZE 8192 #define MAX_HTTP_HEADERS_SIZE (32*1024) -#define HEADER_READ_TIMEOUT 30 -#define DATA_WRITE_TIMEOUT 60 - #define STR1(s) #s #define STR(s) STR1(s) @@ -699,6 +718,7 @@ static int scan_ip_mask(const char *str, unsigned *ipp, unsigned *maskp) * path Path where to look for httpd.conf (without filename). * flag Type of the parse request. */ +//TODO: use parse_config() from libbb? /* flag param: */ enum { FIRST_PARSE = 0, /* path will be "/etc" */ @@ -1062,24 +1082,24 @@ static int openServer(void) /* * Log the connection closure and exit. + * Two variants: one signals EOF (clean termination), + * the other might signal that connection is reset, not closed normally + * (usually RST is sent if there is unsent buffered data in the socket buffer). */ static void log_and_exit(void) NORETURN; static void log_and_exit(void) { - /* Paranoia. IE said to be buggy. It may send some extra data - * or be confused by us just exiting without SHUT_WR. Oh well. */ - shutdown(STDOUT_FILENO, SHUT_WR); - /* Why?? - (this also messes up stdin when user runs httpd -i from terminal) - ndelay_on(0); - while (read(STDIN_FILENO, iobuf, IOBUF_SIZE) > 0) - continue; - */ - if (VERBOSE_3) bb_simple_error_msg("closed"); _exit(xfunc_error_retval); } +static void send_EOF_and_exit(void) NORETURN; +static void send_EOF_and_exit(void) +{ + /* This makes sure on TCP level, the connection is closed with FIN, not RST */ + shutdown(STDOUT_FILENO, SHUT_WR); + log_and_exit(); +} /* * Create and send HTTP response headers. @@ -1304,7 +1324,7 @@ static void send_headers_and_exit(int responseNum) IF_FEATURE_HTTPD_GZIP(content_gzip = 0;) file_size = -1; /* no Last-Modified:, ETag:, Content-Length: */ send_headers(responseNum); - log_and_exit(); + send_EOF_and_exit(); } /* @@ -1436,7 +1456,7 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post bb_error_msg("CGI killed, signal=%u", WTERMSIG(status)); } #endif - break; + send_EOF_and_exit(); } if (pfd[TO_CGI].revents) { @@ -1500,7 +1520,7 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post full_write(STDOUT_FILENO, HTTP_200, sizeof(HTTP_200)-1); full_write(STDOUT_FILENO, rbuf, out_cnt); } - break; /* CGI stdout is closed, exiting */ + send_EOF_and_exit(); /* CGI stdout is closed, exiting */ } out_cnt += count; count = 0; @@ -1535,7 +1555,7 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post } else { count = safe_read(fromCgi_rd, rbuf, IOBUF_SIZE); if (count <= 0) - break; /* eof (or error) */ + send_EOF_and_exit(); /* eof (or error) */ } if (full_write(STDOUT_FILENO, rbuf, count) != count) break; @@ -1794,10 +1814,10 @@ static NOINLINE void send_file_and_exit(const char *url, int what) dbg("can't open '%s'\n", url); /* Error pages are sent by using send_file_and_exit(SEND_BODY). * IOW: it is unsafe to call send_headers_and_exit - * if what is SEND_BODY! Can recurse! */ + * if "what" is SEND_BODY! Can recurse! */ if (what != SEND_BODY) send_headers_and_exit(HTTP_NOT_FOUND); - log_and_exit(); + send_EOF_and_exit(); } #if ENABLE_FEATURE_HTTPD_ETAG /* ETag is "hex(last_mod)-hex(file_size)" e.g. "5e132e20-417" */ @@ -1925,17 +1945,17 @@ static NOINLINE void send_file_and_exit(const char *url, int what) if (offset == range_start) /* was it the very 1st sendfile? */ break; /* fall back to read/write loop */ if (VERBOSE_1) { - if (errno == EAGAIN) - errno = ETIMEDOUT; // SO_SNDTIME on our socket causes write timeouts manifest as EAGAIN "Resource temporarily unavailable". // Not the best error message (when reading the log: "Er... what resource?!") + if (errno == EAGAIN) + errno = ETIMEDOUT; bb_simple_perror_msg("sendfile error"); } log_and_exit(); } IF_FEATURE_HTTPD_RANGES(range_len -= count;) if (count == 0 || range_len == 0) - log_and_exit(); + send_EOF_and_exit(); } } #endif @@ -1948,6 +1968,7 @@ static NOINLINE void send_file_and_exit(const char *url, int what) if (errno == EAGAIN) errno = ETIMEDOUT; bb_simple_perror_msg("write error"); + log_and_exit(); } break; } @@ -1959,7 +1980,7 @@ static NOINLINE void send_file_and_exit(const char *url, int what) if (VERBOSE_1) bb_simple_perror_msg("read error"); } - log_and_exit(); + send_EOF_and_exit(); } #if ENABLE_FEATURE_HTTPD_ACL_IP -- cgit v1.2.3-55-g6feb From 01ea35e81d7f3dcc9d5032ac53b794a5e9d7cedd Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Fri, 23 Jan 2026 02:10:34 +0100 Subject: httpd: simplify CGI code a bit, add a bunch of TODOs and FIXMEs function old new delta log_and_exit 33 25 -8 handle_incoming_and_exit 2298 2290 -8 send_cgi_and_exit 784 770 -14 cgi_io_loop_and_exit 538 477 -61 ------------------------------------------------------------------------------ (add/remove: 0/0 grow/shrink: 0/4 up/down: 0/-91) Total: -91 bytes Signed-off-by: Denys Vlasenko --- networking/httpd.c | 62 +++++++++++++++++++++++----------------- networking/httpd_ratelimit_cgi.c | 4 +-- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/networking/httpd.c b/networking/httpd.c index ed6b0682e..5101f86f7 100644 --- a/networking/httpd.c +++ b/networking/httpd.c @@ -309,6 +309,12 @@ * * 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 @@ -1091,7 +1097,7 @@ static void log_and_exit(void) { if (VERBOSE_3) bb_simple_error_msg("closed"); - _exit(xfunc_error_retval); + _exit_SUCCESS(); } static void send_EOF_and_exit(void) NORETURN; static void send_EOF_and_exit(void) @@ -1440,7 +1446,12 @@ 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) */ if (VERBOSE_3) { @@ -1479,15 +1490,15 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post 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) */ + * and there *is* POSTDATA to read from the peer + */ //count = post_len > (int)sizeof_hdr_buf ? (int)sizeof_hdr_buf : 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; + post_len -= count; /* can go negative (peer wrote more than expected POSTDATA) */ } else { /* no more POST data can be read */ post_len = 0; @@ -1498,34 +1509,34 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post /* There is something to read from CGI */ char *rbuf = iobuf; - /* Are we still buffering CGI output? */ + /* 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 + * if needed, but CGI still has to provide the 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, IOBUF_SIZE - 8); + count = safe_read(fromCgi_rd, rbuf + out_cnt, IOBUF_SIZE); if (count <= 0) { - /* eof (or error) and there was no "HTTP", - * send "HTTP/1.1 200 OK\r\n", then send received data */ - if (out_cnt) { - full_write(STDOUT_FILENO, HTTP_200, sizeof(HTTP_200)-1); - full_write(STDOUT_FILENO, rbuf, out_cnt); - } - send_EOF_and_exit(); /* CGI stdout is closed, exiting */ + /* eof (or error) and there was no "HTTP" (tiny 0..3 bytes output), + * send "HTTP/1.1 200 OK\r\n", then send received 0..3 bytes */ + goto write_HTTP_200_OK; + /* we'll read once more later, in "no longer buffering" + * code path, get another EOF there and exit */ } out_cnt += count; count = 0; /* "Status" header format is: "Status: 302 Redirected\r\n" */ if (out_cnt >= 8 && memcmp(rbuf, "Status: ", 8) == 0) { +//FIXME: "Status: " is not required to be the first header! It can be anywhere! +//FIXME: many servers also check "Location: ". If it exists but "Status: " does _not_, "302 Found" is assumed instead of "200 OK". /* send "HTTP/1.1 " */ if (full_write(STDOUT_FILENO, HTTP_200, 9) != 9) break; @@ -1534,29 +1545,31 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post count = out_cnt - 8; out_cnt = -1; /* buffering off */ } else if (out_cnt >= 4) { +//NB: Apache has no such autodetection. It always adds its own HTTP/ header, +//unless the CGI name starts with "nph-", in which case it passes its output verbatim to network. /* Did CGI add "HTTP"? */ if (memcmp(rbuf, HTTP_200, 4) != 0) { - /* there is no "HTTP", do it ourself */ + write_HTTP_200_OK: + /* there is no "HTTP", 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; } - /* 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: + /* 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 "Location: http://www.busybox.net\r\n" * echo -en "\r\n" */ count = out_cnt; out_cnt = -1; /* buffering off */ + /* NB: we mishandle CGI writing "Stat","us..." in two separate writes */ } } else { count = safe_read(fromCgi_rd, rbuf, IOBUF_SIZE); if (count <= 0) send_EOF_and_exit(); /* eof (or error) */ } +//FIXME: many (most?) servers translate bare "\n" to "\r\n", often only in the headers, not body (the part after empty line) if (full_write(STDOUT_FILENO, rbuf, count) != count) break; dbg("cgi read %d bytes: '%.*s'\n", count, count, rbuf); @@ -1697,8 +1710,6 @@ static void send_cgi_and_exit( /* Child process */ char *argv[3]; - xfunc_error_retval = 242; - /* NB: close _first_, then move fds! */ close(toCgi.wr); close(fromCgi.rd); @@ -1768,9 +1779,6 @@ static void send_cgi_and_exit( /* Parent process */ - /* Restore variables possibly changed by child */ - xfunc_error_retval = 0; - /* Pump data */ close(fromCgi.wr); close(toCgi.rd); @@ -2239,7 +2247,7 @@ static void send_REQUEST_TIMEOUT_and_exit(int sig UNUSED_PARAM) // /* writing timed out: exit without writing anything */ // if (VERBOSE_3) // bb_simple_error_msg("write timeout"); -// _exit(xfunc_error_retval); +// _exit_SUCCESS(); } static void prepare_write_timeout(void) @@ -2334,7 +2342,7 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) * just close the socket. */ //send_headers_and_exit(HTTP_BAD_REQUEST); - _exit(xfunc_error_retval); + _exit_SUCCESS(); } dbg("Request:'%s'\n", iobuf); diff --git a/networking/httpd_ratelimit_cgi.c b/networking/httpd_ratelimit_cgi.c index 713274873..96702131e 100644 --- a/networking/httpd_ratelimit_cgi.c +++ b/networking/httpd_ratelimit_cgi.c @@ -223,7 +223,7 @@ int main(int argc, char **argv) } /* No slot available, return 429 */ - write_and_die(1, "HTTP/1.1 429 Too Many Requests\r\n" + write_and_die(1, "Status: 429 Too Many Requests\r\n" "Content-Type: text/plain\r\n" "Retry-After: 60\r\n" "Connection: close\r\n\r\n" @@ -235,7 +235,7 @@ int main(int argc, char **argv) exec: execv(argv[0], argv); full_write2(2, "can't execute", argv[0]); - write_and_die(1, "HTTP/1.1 500 Internal Server Error\r\n" + write_and_die(1, "Status: 500 Internal Server Error\r\n" "Content-Type: text/plain\r\n\r\n" "Failed to execute binary\n"); return 1; -- cgit v1.2.3-55-g6feb From a5f120b6205c4e22792c818350b33bf57644ed1c Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Fri, 23 Jan 2026 10:14:04 +0100 Subject: httpd: simplify CGI headers handling, check "HTTP/1.1" prefix, not just "HTTP" function old new delta cgi_io_loop_and_exit 477 498 +21 .rodata 106830 106821 -9 ------------------------------------------------------------------------------ (add/remove: 0/0 grow/shrink: 1/1 up/down: 21/-9) Total: 12 bytes Signed-off-by: Denys Vlasenko --- include/platform.h | 2 ++ networking/httpd.c | 58 ++++++++++++++++++++++++++++-------------------------- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/include/platform.h b/include/platform.h index a5b61757f..c449075b9 100644 --- a/include/platform.h +++ b/include/platform.h @@ -211,6 +211,7 @@ # define IF_LITTLE_ENDIAN(...) /* How do bytes a,b,c,d (sequential in memory) look if fetched into uint32_t? */ # define PACK32_BYTES(a,b,c,d) (uint32_t)((d)+((c)<<8)+((b)<<16)+((a)<<24)) +# define PACK64_LITERAL_STR(s) (((uint64_t)PACK32_BYTES((s)[0],(s)[1],(s)[2],(s)[3])<<32) + PACK32_BYTES((s)[4],(s)[5],(s)[6],(s)[7])) #else # define SWAP_BE16(x) bswap_16(x) # define SWAP_BE32(x) bswap_32(x) @@ -221,6 +222,7 @@ # define IF_BIG_ENDIAN(...) # define IF_LITTLE_ENDIAN(...) __VA_ARGS__ # define PACK32_BYTES(a,b,c,d) (uint32_t)((a)+((b)<<8)+((c)<<16)+((d)<<24)) +# define PACK64_LITERAL_STR(s) (((uint64_t)PACK32_BYTES((s)[4],(s)[5],(s)[6],(s)[7])<<32) + PACK32_BYTES((s)[0],(s)[1],(s)[2],(s)[3])) #endif diff --git a/networking/httpd.c b/networking/httpd.c index 5101f86f7..e98da9c57 100644 --- a/networking/httpd.c +++ b/networking/httpd.c @@ -537,7 +537,7 @@ struct globals { #if ENABLE_FEATURE_HTTPD_PROXY Htaccess_Proxy *proxy; #endif - char iobuf[IOBUF_SIZE]; + char iobuf[IOBUF_SIZE] ALIGN8; }; #define G (*ptr_to_globals) #define verbose (G.verbose ) @@ -1425,7 +1425,7 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post /*pfd[FROM_CGI].events = POLLIN; - moved out of loop */ /*pfd[FROM_CGI].revents = 0; - not needed */ - /* gcc-4.8.0 still doesnt fill two shorts with one insn :( */ + /* gcc-4.8.0 still doesn't fill two shorts with one insn :( */ /* http://gcc.gnu.org/bugzilla/show_bug.cgi?id=47059 */ /* hopefully one day it will... */ pfd[TO_CGI].events = POLLOUT; @@ -1507,8 +1507,6 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post if (pfd[FROM_CGI].revents) { /* There is something to read from CGI */ - char *rbuf = iobuf; - /* Still buffering CGI output? */ if (out_cnt >= 0) { /* HTTP_200[] has single "\r\n" at the end. @@ -1523,34 +1521,34 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post * 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, IOBUF_SIZE); + count = safe_read(fromCgi_rd, iobuf + out_cnt, IOBUF_SIZE); if (count <= 0) { - /* eof (or error) and there was no "HTTP" (tiny 0..3 bytes output), - * send "HTTP/1.1 200 OK\r\n", then send received 0..3 bytes */ + /* EOF (or error, and out_cnt=0..7 + * send "HTTP/1.1 200 OK\r\n", then send received 0..7 bytes */ goto write_HTTP_200_OK; /* we'll read once more later, in "no longer buffering" * code path, get another EOF there and exit */ } out_cnt += count; count = 0; - /* "Status" header format is: "Status: 302 Redirected\r\n" */ - if (out_cnt >= 8 && memcmp(rbuf, "Status: ", 8) == 0) { + if (out_cnt >= 8) { //FIXME: "Status: " is not required to be the first header! It can be anywhere! //FIXME: many servers also check "Location: ". If it exists but "Status: " does _not_, "302 Found" is assumed instead of "200 OK". - /* send "HTTP/1.1 " */ - if (full_write(STDOUT_FILENO, HTTP_200, 9) != 9) - break; - /* skip "Status: " (including space, sending "HTTP/1.1 NNN" is wrong) */ - rbuf += 8; - count = out_cnt - 8; - out_cnt = -1; /* buffering off */ - } else if (out_cnt >= 4) { -//NB: Apache has no such autodetection. It always adds its own HTTP/ header, + /* "Status" header format is: "Status: 302 Redirected\r\n" */ + //if (memcmp(iobuf, "Status: ", 8) == 0) + if (*(uint64_t*)iobuf == PACK64_LITERAL_STR("Status: ")) { + /* send "HTTP/1.1 " */ + if (full_write(STDOUT_FILENO, HTTP_200, 9) != 9) + break; + /* skip "Status: " (including space, sending "HTTP/1.1 NNN" is wrong) */ + out_cnt -= 8; + memmove(iobuf, iobuf + 8, out_cnt); + } +//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. - /* Did CGI add "HTTP"? */ - if (memcmp(rbuf, HTTP_200, 4) != 0) { + else if (memcmp(iobuf, HTTP_200, 8) != 0) { /* Did CGI send "HTTP/1.1"? */ write_HTTP_200_OK: - /* there is no "HTTP", send "HTTP/1.1 200 OK\r\n" ourself */ + /* 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; } @@ -1562,17 +1560,16 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post */ count = out_cnt; out_cnt = -1; /* buffering off */ - /* NB: we mishandle CGI writing "Stat","us..." in two separate writes */ } } else { - count = safe_read(fromCgi_rd, rbuf, IOBUF_SIZE); + count = safe_read(fromCgi_rd, iobuf, IOBUF_SIZE); if (count <= 0) - send_EOF_and_exit(); /* eof (or error) */ + send_EOF_and_exit(); /* EOF (or error) */ } -//FIXME: many (most?) servers translate bare "\n" to "\r\n", often only in the headers, not body (the part after empty line) - if (full_write(STDOUT_FILENO, rbuf, count) != count) +//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; - dbg("cgi read %d bytes: '%.*s'\n", count, count, rbuf); + dbg("cgi read %d bytes: '%.*s'\n", count, count, iobuf); } /* if (pfd[FROM_CGI].revents) */ } /* while (1) */ log_and_exit(); @@ -2948,7 +2945,12 @@ int httpd_main(int argc UNUSED_PARAM, char **argv) IF_FEATURE_HTTPD_SETUID(const char *s_ugid = NULL;) IF_FEATURE_HTTPD_SETUID(struct bb_uidgid_t ugid;) IF_FEATURE_HTTPD_AUTH_MD5(const char *pass;) - +#if 0 // PACK64_LITERAL_STR test + char testing[16] = "Status: "; + printf("0x%08llx\n", PACK64_LITERAL_STR("Status: ")); + printf("0x%08llx\n", *(uint64_t*)testing); + exit(1); +#endif INIT_G(); #if ENABLE_LOCALE_SUPPORT -- cgit v1.2.3-55-g6feb From 5d33dbb67c776c5876413a7531cbd0123902d098 Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Fri, 23 Jan 2026 11:04:28 +0100 Subject: httpd: smarter handling of CGI's "Status: " header Do one less write() function old new delta handle_incoming_and_exit 2290 2297 +7 cgi_io_loop_and_exit 498 500 +2 .rodata 106821 106814 -7 ------------------------------------------------------------------------------ (add/remove: 0/0 grow/shrink: 2/1 up/down: 9/-7) Total: 2 bytes Signed-off-by: Denys Vlasenko --- networking/httpd.c | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/networking/httpd.c b/networking/httpd.c index e98da9c57..30f013364 100644 --- a/networking/httpd.c +++ b/networking/httpd.c @@ -1521,7 +1521,8 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post * 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, iobuf + out_cnt, IOBUF_SIZE); + count = safe_read(fromCgi_rd, iobuf + out_cnt, IOBUF_SIZE - 8); +// "- 8" is important, out_cnt can be up to 7 if (count <= 0) { /* EOF (or error, and out_cnt=0..7 * send "HTTP/1.1 200 OK\r\n", then send received 0..7 bytes */ @@ -1534,19 +1535,21 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post if (out_cnt >= 8) { //FIXME: "Status: " is not required to be the first header! It can be anywhere! //FIXME: many servers also check "Location: ". If it exists but "Status: " does _not_, "302 Found" is assumed instead of "200 OK". + uint64_t str8 = *(uint64_t*)iobuf; /* "Status" header format is: "Status: 302 Redirected\r\n" */ //if (memcmp(iobuf, "Status: ", 8) == 0) - if (*(uint64_t*)iobuf == PACK64_LITERAL_STR("Status: ")) { - /* send "HTTP/1.1 " */ - if (full_write(STDOUT_FILENO, HTTP_200, 9) != 9) - break; - /* skip "Status: " (including space, sending "HTTP/1.1 NNN" is wrong) */ - out_cnt -= 8; - memmove(iobuf, iobuf + 8, out_cnt); + if (str8 == PACK64_LITERAL_STR("Status: ")) { + /* Replace "Status: " */ + /* with "HTTP/1.1 " */ + memmove(iobuf + 1, iobuf, out_cnt); + out_cnt += 1; + memcpy(iobuf, HTTP_200, 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. - else if (memcmp(iobuf, HTTP_200, 8) != 0) { /* Did CGI send "HTTP/1.1"? */ + else /* Did CGI send "HTTP/1.1"? */ + //if (memcmp(iobuf, HTTP_200, 8) != 0) + if (str8 == PACK64_LITERAL_STR(HTTP_200)) { write_HTTP_200_OK: /* 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) @@ -2475,7 +2478,7 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) /* Log it */ if (VERBOSE_2) - bb_error_msg("url:%s", urlcopy); + bb_error_msg("%s %s", prequest, urlcopy); tptr = urlcopy; while ((tptr = strchr(tptr + 1, '/')) != NULL) { -- cgit v1.2.3-55-g6feb From 927ff335c5d4a41dc00b267bd4444546f758e3fa Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Fri, 23 Jan 2026 11:32:32 +0100 Subject: httpd: fix incorrect == comparison in last commit, must be != Signed-off-by: Denys Vlasenko --- networking/httpd.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/networking/httpd.c b/networking/httpd.c index 30f013364..0c5083a73 100644 --- a/networking/httpd.c +++ b/networking/httpd.c @@ -1536,6 +1536,8 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post //FIXME: "Status: " is not required to be the first header! It can be anywhere! //FIXME: many servers also check "Location: ". If it exists but "Status: " does _not_, "302 Found" is assumed instead of "200 OK". uint64_t str8 = *(uint64_t*)iobuf; + //bb_error_msg("from cgi:'%.*s'", out_cnt, iobuf); + /* "Status" header format is: "Status: 302 Redirected\r\n" */ //if (memcmp(iobuf, "Status: ", 8) == 0) if (str8 == PACK64_LITERAL_STR("Status: ")) { @@ -1549,7 +1551,7 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post //unless the CGI name starts with "nph-", in which case it passes its output verbatim to network. else /* Did CGI send "HTTP/1.1"? */ //if (memcmp(iobuf, HTTP_200, 8) != 0) - if (str8 == PACK64_LITERAL_STR(HTTP_200)) { + if (str8 != PACK64_LITERAL_STR(HTTP_200)) { write_HTTP_200_OK: /* 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) -- cgit v1.2.3-55-g6feb From 1a64fe3594b80dabcc7ccb7dce5b3391b27b0d24 Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Sat, 24 Jan 2026 02:01:42 +0100 Subject: httpd: implement POSTDATA read timeout, and -K KILLSECS CGI lifetime control function old new delta cgi_io_loop_and_exit 496 656 +160 sigalrm_handler 1 102 +101 .rodata 106814 106853 +39 send_cgi_and_exit 770 803 +33 packed_usage 35894 35924 +30 httpd_main 950 957 +7 handle_incoming_and_exit 2297 2292 -5 send_REQUEST_TIMEOUT_and_exit 10 - -10 ------------------------------------------------------------------------------ (add/remove: 0/1 grow/shrink: 6/1 up/down: 370/-15) Total: 355 bytes Signed-off-by: Denys Vlasenko --- networking/httpd.c | 169 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 110 insertions(+), 59 deletions(-) diff --git a/networking/httpd.c b/networking/httpd.c index 0c5083a73..ce557c1bf 100644 --- a/networking/httpd.c +++ b/networking/httpd.c @@ -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 .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) { -- cgit v1.2.3-55-g6feb From 43982ed49962526f7f042bcd9c5eb05ab4e586de Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Sat, 24 Jan 2026 05:21:10 +0100 Subject: httpd: code shrink via "split-globals" trick function old new delta httpd_main 957 968 +11 log_and_exit 25 26 +1 send_headers 712 708 -4 handle_incoming_and_exit 2292 2285 -7 sigalrm_handler 102 93 -9 parse_conf 1332 1323 -9 send_cgi_and_exit 803 790 -13 cgi_io_loop_and_exit 656 635 -21 ------------------------------------------------------------------------------ (add/remove: 0/0 grow/shrink: 2/6 up/down: 12/-63) Total: -51 bytes Signed-off-by: Denys Vlasenko --- include/libbb.h | 15 +++++++++++++++ networking/httpd.c | 6 +++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/include/libbb.h b/include/libbb.h index 8d252d455..17c9bc785 100644 --- a/include/libbb.h +++ b/include/libbb.h @@ -457,6 +457,11 @@ void *xmmap_anon(size_t size) FAST_FUNC; //sparc64,alpha,openrisc: fixed 8k pages #endif +#if defined(__x86_64__) || defined(i386) +/* 0x7f would be better, but it causes alignment problems */ +# define ARCH_GLOBAL_PTR_OFF 0x80 +#endif + #if defined BB_ARCH_FIXED_PAGESIZE # define IF_VARIABLE_ARCH_PAGESIZE(...) /*nothing*/ # define bb_getpagesize() BB_ARCH_FIXED_PAGESIZE @@ -2423,6 +2428,16 @@ void XZALLOC_CONST_PTR(const void *pptr, size_t size) FAST_FUNC; } \ } while (0) +#if defined(ARCH_GLOBAL_PTR_OFF) +# define SET_OFFSET_PTR_TO_GLOBALS(x) \ + ASSIGN_CONST_PTR(&ptr_to_globals, (char*)(x) + ARCH_GLOBAL_PTR_OFF) +# define OFFSET_PTR_TO_GLOBALS \ + ((struct globals*)((char*)ptr_to_globals - ARCH_GLOBAL_PTR_OFF)) +#else +# define SET_OFFSET_PTR_TO_GLOBALS(x) SET_PTR_TO_GLOBALS(x) +# define OFFSET_PTR_TO_GLOBALS ptr_to_globals +#endif + /* You can change LIBBB_DEFAULT_LOGIN_SHELL, but don't use it, * use bb_default_login_shell and following defines. diff --git a/networking/httpd.c b/networking/httpd.c index ce557c1bf..176eb244b 100644 --- a/networking/httpd.c +++ b/networking/httpd.c @@ -484,7 +484,6 @@ static const struct { }; struct globals { - int verbose; /* must be int (used by getopt32) */ smallint flg_deny_all; #if ENABLE_FEATURE_HTTPD_GZIP /* client can handle gzip / we are going to send gzip */ @@ -494,6 +493,7 @@ struct globals { #if ENABLE_FEATURE_HTTPD_CGI smallint cgi_output; #endif + int verbose; /* must be int (used by getopt32) */ time_t last_mod; #if ENABLE_FEATURE_HTTPD_ETAG char *if_none_match; @@ -555,7 +555,7 @@ struct globals { #endif char iobuf[IOBUF_SIZE] ALIGN8; }; -#define G (*ptr_to_globals) +#define G (*OFFSET_PTR_TO_GLOBALS) #define verbose (G.verbose ) #define flg_deny_all (G.flg_deny_all ) #if ENABLE_FEATURE_HTTPD_GZIP @@ -598,7 +598,7 @@ enum { #define iobuf (G.iobuf ) #define INIT_G() do { \ setup_common_bufsiz(); \ - SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \ + SET_OFFSET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \ IF_FEATURE_HTTPD_BASIC_AUTH(g_realm = "Web Server Authentication";) \ IF_FEATURE_HTTPD_RANGES(range_start = -1;) \ bind_addr_or_port = STR(CONFIG_FEATURE_HTTPD_PORT_DEFAULT); \ -- cgit v1.2.3-55-g6feb From 5cba59e9ce72b592f1c439c2d15d3691d09b3525 Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Sat, 24 Jan 2026 05:30:55 +0100 Subject: awk: use more understandable form of "split-globals" trick function old new delta parse_expr 986 998 +12 chain_group 633 640 +7 next_token 930 934 +4 getvar_s 102 101 -1 awk_main 891 888 -3 evaluate 3379 3355 -24 ------------------------------------------------------------------------------ (add/remove: 0/0 grow/shrink: 3/3 up/down: 23/-28) Total: -5 bytes Signed-off-by: Denys Vlasenko --- archival/gzip.c | 3 +++ editors/awk.c | 72 ++++++++++++++++++++++++++------------------------------- 2 files changed, 36 insertions(+), 39 deletions(-) diff --git a/archival/gzip.c b/archival/gzip.c index 91bd4d09d..e8080892b 100644 --- a/archival/gzip.c +++ b/archival/gzip.c @@ -2215,6 +2215,9 @@ int gzip_main(int argc UNUSED_PARAM, char **argv) }; #endif +// TODO: use less ugly "split-globals" trick via SET_OFFSET_PTR_TO_GLOBALS(). +// The problem is, the current method strategically places G2.heap[] +// (~24 references) so that it has zero offset. SET_PTR_TO_GLOBALS((char *)xzalloc(sizeof(struct globals)+sizeof(struct globals2)) + sizeof(struct globals)); diff --git a/editors/awk.c b/editors/awk.c index 64e752f4b..beba487fb 100644 --- a/editors/awk.c +++ b/editors/awk.c @@ -583,14 +583,10 @@ static const char vValues[] ALIGN1 = static const uint16_t PRIMES[] ALIGN2 = { 251, 1021, 4093, 16381, 65521 }; -/* Globals. Split in two parts so that first one is addressed - * with (mostly short) negative offsets. - * NB: it's unsafe to put members of type "double" - * into globals2 (gcc may fail to align them). - */ struct globals { - double t_double; chain beginseq, mainseq, endseq; + + double t_double; chain *seq; node *break_ptr, *continue_ptr; xhash *ahash; /* argument names, used only while parsing function bodies */ @@ -618,9 +614,8 @@ struct globals { smallint next_token__concat_inserted; uint32_t next_token__save_tclass; uint32_t next_token__save_info; -}; -struct globals2 { - uint32_t t_info; /* often used */ + + uint32_t t_info; uint32_t t_tclass; char *t_string; int t_lineno; @@ -654,41 +649,40 @@ struct globals2 { char g_buf[MAXVARFMT + 1]; }; -#define G1 (ptr_to_globals[-1]) -#define G (*(struct globals2 *)ptr_to_globals) +#define G (*OFFSET_PTR_TO_GLOBALS) /* For debug. nm --size-sort awk.o | grep -vi ' [tr] ' */ //char G1size[sizeof(G1)]; // 0x70 //char Gsize[sizeof(G)]; // 0x2f8 /* Trying to keep most of members accessible with short offsets: */ //char Gofs_seed[offsetof(struct globals2, evaluate__seed)]; // 0x7c -#define t_double (G1.t_double ) -#define beginseq (G1.beginseq ) -#define mainseq (G1.mainseq ) -#define endseq (G1.endseq ) -#define seq (G1.seq ) -#define break_ptr (G1.break_ptr ) -#define continue_ptr (G1.continue_ptr) -#define ahash (G1.ahash ) -#define fnhash (G1.fnhash ) -#define vhash (G1.vhash ) +#define t_double (G.t_double ) +#define beginseq (G.beginseq ) +#define mainseq (G.mainseq ) +#define endseq (G.endseq ) +#define seq (G.seq ) +#define break_ptr (G.break_ptr ) +#define continue_ptr (G.continue_ptr) +#define ahash (G.ahash ) +#define fnhash (G.fnhash ) +#define vhash (G.vhash ) #define fdhash ahash //^^^^^^^^^^^^^^^^^^ ahash is cleared after every function parsing, // and ends up empty after parsing phase. Thus, we can simply reuse it // for fdhash in execution stage. -#define g_progname (G1.g_progname ) -#define g_lineno (G1.g_lineno ) -#define num_fields (G1.num_fields ) -#define num_alloc_fields (G1.num_alloc_fields) -#define Fields (G1.Fields ) -#define g_pos (G1.g_pos ) -#define g_saved_ch (G1.g_saved_ch ) -#define got_program (G1.got_program ) -#define icase (G1.icase ) -#define exiting (G1.exiting ) -#define nextrec (G1.nextrec ) -#define nextfile (G1.nextfile ) -#define is_f0_split (G1.is_f0_split ) -#define t_rollback (G1.t_rollback ) +#define g_progname (G.g_progname ) +#define g_lineno (G.g_lineno ) +#define num_fields (G.num_fields ) +#define num_alloc_fields (G.num_alloc_fields) +#define Fields (G.Fields ) +#define g_pos (G.g_pos ) +#define g_saved_ch (G.g_saved_ch ) +#define got_program (G.got_program ) +#define icase (G.icase ) +#define exiting (G.exiting ) +#define nextrec (G.nextrec ) +#define nextfile (G.nextfile ) +#define is_f0_split (G.is_f0_split ) +#define t_rollback (G.t_rollback ) #define t_info (G.t_info ) #define t_tclass (G.t_tclass ) #define t_string (G.t_string ) @@ -699,7 +693,7 @@ struct globals2 { #define rsplitter (G.rsplitter ) #define g_buf (G.g_buf ) #define INIT_G() do { \ - SET_PTR_TO_GLOBALS((char*)xzalloc(sizeof(G1)+sizeof(G)) + sizeof(G1)); \ + SET_OFFSET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \ t_tclass = TC_NEWLINE; \ G.evaluate__seed = 1; \ } while (0) @@ -1168,9 +1162,9 @@ static int istrue(var *v) */ static uint32_t next_token(uint32_t expected) { -#define concat_inserted (G1.next_token__concat_inserted) -#define save_tclass (G1.next_token__save_tclass) -#define save_info (G1.next_token__save_info) +#define concat_inserted (G.next_token__concat_inserted) +#define save_tclass (G.next_token__save_tclass) +#define save_info (G.next_token__save_info) char *p; const char *tl; -- cgit v1.2.3-55-g6feb From a2f8c89aecf473f905da6eba53d02fb23eca93cd Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Sat, 24 Jan 2026 13:29:50 +0100 Subject: httpd: code shrink function old new delta send_file_and_exit 931 933 +2 get_line 126 120 -6 httpd_main 968 959 -9 send_headers 708 694 -14 handle_incoming_and_exit 2285 2270 -15 cgi_io_loop_and_exit 635 620 -15 ------------------------------------------------------------------------------ (add/remove: 0/0 grow/shrink: 1/5 up/down: 2/-59) Total: -57 bytes Signed-off-by: Denys Vlasenko --- networking/httpd.c | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/networking/httpd.c b/networking/httpd.c index 176eb244b..956ad3504 100644 --- a/networking/httpd.c +++ b/networking/httpd.c @@ -512,9 +512,6 @@ struct globals { Htaccess_IP *ip_a_d; /* config allow/deny lines */ #endif - IF_FEATURE_HTTPD_BASIC_AUTH(const char *g_realm;) - IF_FEATURE_HTTPD_BASIC_AUTH(char *remoteuser;) - pid_t parent_pid; int children_fd; int conn_limit; @@ -527,14 +524,14 @@ struct globals { #endif #if ENABLE_FEATURE_HTTPD_BASIC_AUTH + const char *g_realm; + char *remoteuser; Htaccess *g_auth; /* config user:password lines */ #endif Htaccess *mime_a; /* config mime types */ #if ENABLE_FEATURE_HTTPD_CONFIG_WITH_SCRIPT_INTERPR Htaccess *script_i; /* config script interpreters */ #endif -#define hdr_buf bb_common_bufsiz1 -#define sizeof_hdr_buf COMMON_BUFSIZE char *hdr_ptr; int hdr_cnt; #if ENABLE_FEATURE_HTTPD_CGI || ENABLE_FEATURE_HTTPD_PROXY @@ -544,9 +541,6 @@ struct globals { 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 #if ENABLE_FEATURE_HTTPD_ERROR_PAGES const char *http_error_page[ARRAY_SIZE(http_response_type)]; #endif @@ -554,6 +548,16 @@ struct globals { Htaccess_Proxy *proxy; #endif char iobuf[IOBUF_SIZE] ALIGN8; + +/* We also use the common buffer as a global buffer: + * = as input buffer for request line, headers, and POSTDATA + * = when retrieving ordinary file (not CGI, proxy, etc), we generate and store file's etag + */ +#define hdr_buf bb_common_bufsiz1 +#define sizeof_hdr_buf COMMON_BUFSIZE +#if ENABLE_FEATURE_HTTPD_ETAG +#define etag bb_common_bufsiz1 +#endif }; #define G (*OFFSET_PTR_TO_GLOBALS) #define verbose (G.verbose ) @@ -1300,7 +1304,7 @@ static void send_headers(unsigned responseNum) date_str, #endif #if ENABLE_FEATURE_HTTPD_ETAG - G.etag, + etag, #endif file_size ); @@ -1712,19 +1716,19 @@ static void send_cgi_and_exit( setenv1("REMOTE_ADDR", p); if (cp) { *cp = ':'; -#if ENABLE_FEATURE_HTTPD_SET_REMOTE_PORT_TO_ENV +# if ENABLE_FEATURE_HTTPD_SET_REMOTE_PORT_TO_ENV setenv1("REMOTE_PORT", cp + 1); -#endif +# endif } } if (G.POST_len > 0) putenv(xasprintf("CONTENT_LENGTH=%u", G.POST_len)); -#if ENABLE_FEATURE_HTTPD_BASIC_AUTH +# if ENABLE_FEATURE_HTTPD_BASIC_AUTH if (remoteuser) { setenv1("REMOTE_USER", remoteuser); putenv((char*)"AUTH_TYPE=Basic"); } -#endif +# endif /* setenv1("SERVER_NAME", safe_gethostname()); - don't do this, * just run "env SERVER_NAME=xyz httpd ..." instead */ @@ -1768,7 +1772,7 @@ static void send_cgi_and_exit( argv[0] = script; argv[1] = NULL; -#if ENABLE_FEATURE_HTTPD_CONFIG_WITH_SCRIPT_INTERPR +# if ENABLE_FEATURE_HTTPD_CONFIG_WITH_SCRIPT_INTERPR { char *suffix = strrchr(script, '.'); @@ -1785,7 +1789,7 @@ static void send_cgi_and_exit( } } } -#endif +# endif if (VERBOSE_2) bb_error_msg("exec:%s"IF_FEATURE_HTTPD_CONFIG_WITH_SCRIPT_INTERPR(" %s"), argv[0] @@ -1870,13 +1874,13 @@ static NOINLINE void send_file_and_exit(const char *url, int what) } #if ENABLE_FEATURE_HTTPD_ETAG /* ETag is "hex(last_mod)-hex(file_size)" e.g. "5e132e20-417" */ - sprintf(G.etag, "\"%llx-%llx\"", (unsigned long long)last_mod, (unsigned long long)file_size); + sprintf(etag, "\"%llx-%llx\"", (unsigned long long)last_mod, (unsigned long long)file_size); if (G.if_none_match) { - dbg("If-None-Match:'%s' file's ETag:'%s'\n", G.if_none_match, G.etag); + dbg("If-None-Match:'%s' file's ETag:'%s'\n", G.if_none_match, etag); /* Weak ETag comparision. * If-None-Match may have many ETags but they are quoted so we can use simple substring search */ - if (strstr(G.if_none_match, G.etag)) + if (strstr(G.if_none_match, etag)) send_headers_and_exit(HTTP_NOT_MODIFIED); } #endif -- cgit v1.2.3-55-g6feb