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 <vda.linux@googlemail.com>
This commit is contained in:
Denys Vlasenko 2026-01-22 22:31:36 +01:00
parent 04d8729adb
commit 2d78809b74

View file

@ -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