diff --git a/httpd/httpd.c b/httpd/httpd.c index f2ca2d36..733f681b 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -90,6 +90,7 @@ __xdata uint32_t last_session_use; #define TSTATE_POST 4 #define TSTATE_MULTIPART 5 #define TSTATE_POSTBODY 6 +#define TSTATE_POSTHEADER 7 extern __xdata uint16_t crc_value; __xdata uint16_t crc_final; @@ -746,11 +747,47 @@ static void post_body_continue(void) } + +/* + * Buffer a POST request header across TCP receive events. Stop copying at + * CRLFCRLF so any body bytes in the final segment stay in uip_appdata. + * + * Return the body offset in the current receive buffer, 0 while incomplete, + * or 0xffff after sending an error response. + */ +static uint16_t post_header_take(void) +{ + uint16_t i; + + for (i = 0; i < uip_len; i++) { + if (pre_acc + 1 >= CONFIG_UPLOAD_BUF) { + uip_conn->appstate.tstate = TSTATE_NONE; + send_bad_request(); + return 0xffff; + } + config_buf[pre_acc++] = ((__xdata uint8_t *)uip_appdata)[i]; + config_buf[pre_acc] = NUL; + if (pre_acc >= 4 + && config_buf[pre_acc - 4] == '\r' + && config_buf[pre_acc - 3] == '\n' + && config_buf[pre_acc - 2] == '\r' + && config_buf[pre_acc - 1] == '\n') { + uip_conn->appstate.tstate = TSTATE_NONE; + return i + 1; + } + } + + postbody_start = ticks; + uip_conn->appstate.tstate = TSTATE_POSTHEADER; + return 0; +} + void handle_post(void) { __xdata struct httpd_state * __xdata s = &(uip_conn->appstate); __xdata uint8_t *p = uip_appdata; __xdata uint8_t *request_path = p + 6; + uint16_t body_off; if (s->tstate == TSTATE_POSTBODY) { post_body_continue(); @@ -759,6 +796,14 @@ void handle_post(void) // Was the multipart header sent in multiple packets? if (s->tstate != TSTATE_MULTIPART) { + if (s->tstate != TSTATE_POSTHEADER) + pre_acc = 0; + body_off = post_header_take(); + if (!body_off || body_off == 0xffff) + return; + p = config_buf; + request_path = p + 6; + dbg_string("Is POST\n"); p += 5; // Skip post // Find end of request path @@ -772,9 +817,12 @@ void handle_post(void) dbg_string("Boundary: >"); dbg_string_x(boundary); dbg_string("<\n"); if (!*p || !content_type) { dbg_string("Bad Request!\n"); - send_not_found(); + send_bad_request(); return; } + + pre_acc = 0; + p = (__xdata uint8_t *)uip_appdata + body_off; if (is_word(request_path, "upload")) { if (!authenticated) { send_unauthorized(); @@ -824,7 +872,6 @@ void handle_post(void) handle_firmware_fragment(p); return; } else if (is_word(request_path, "cmd")) { - p += 4; if (!authenticated) { send_unauthorized(); return; @@ -843,7 +890,6 @@ void handle_post(void) return; } - p += 4; postbody_endpoint = POSTBODY_LOGIN; if (!post_body_take(p)) return; @@ -890,9 +936,9 @@ void httpd_appcall(void) dbg_string("Closing because everything has been transmitted\n"); uip_close(); s->tstate = TSTATE_CLOSED; - } else if (s->tstate == TSTATE_POSTBODY + } else if ((s->tstate == TSTATE_POSTBODY || s->tstate == TSTATE_POSTHEADER) && (uint16_t)ticks - postbody_start > POSTBODY_TIMEOUT) { - dbg_string("Body never arrived\n"); + dbg_string("POST request never completed\n"); uip_abort(); s->tstate = TSTATE_CLOSED; } @@ -964,11 +1010,11 @@ void httpd_appcall(void) #endif p = uip_appdata; if (is_word(p, "POST") || s->tstate == TSTATE_MULTIPART - || s->tstate == TSTATE_POSTBODY) { + || s->tstate == TSTATE_POSTBODY || s->tstate == TSTATE_POSTHEADER) { handle_post(); // If this is an ongoing post stream, then wait for the next packet if (s->tstate == TSTATE_POST || s->tstate == TSTATE_MULTIPART - || s->tstate == TSTATE_POSTBODY) { + || s->tstate == TSTATE_POSTBODY || s->tstate == TSTATE_POSTHEADER) { uip_len = 0; return; } diff --git a/test/test_httpd_tx.c b/test/test_httpd_tx.c index 3f056030..7aa97f33 100644 --- a/test/test_httpd_tx.c +++ b/test/test_httpd_tx.c @@ -156,6 +156,7 @@ void send_vlanlist(void) { } void send_lldp(void) {} extern uint8_t authenticated; +extern char passwd[21]; /* uip.c defines the listen table but no header declares it; the bench reads it * to confirm the port httpd_init() asked for is the one uIP is watching. */ @@ -362,6 +363,22 @@ static int first_body_mismatch(void) return -1; } +static int stream_contains(const char *needle) +{ + size_t n = strlen(needle); + + for (int i = 0; i + (int)n <= stream_len; i++) + if (!memcmp(stream + i, needle, n)) + return 1; + return 0; +} + +static void login_session_start(void) +{ + session_start(MSS_FULL); + strcpy(passwd, "1234"); +} + static void report(const char *tag) { if (verbose) @@ -493,6 +510,157 @@ static void scenario_bad_l4_checksum(void) "bad checksum control: the file that follows is intact"); } + +static void scenario_fragmented_post_header(void) +{ + static const char first[] = + "POST /login HTTP/1.1\r\n" + "Host: sw\r\n" + "Content-Len"; + static const char second[] = + "gth: 8\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "\r\n" + "pwd=1234"; + + login_session_start(); + client_send(TCP_ACK | TCP_PSH, first, (int)strlen(first)); + CHECK(stream_len == 0, + "fragmented header: incomplete first fragment has no response"); + + client_send(TCP_ACK | TCP_PSH, second, (int)strlen(second)); + CHECK(stream_contains("HTTP/1.1 302 Found\r\n"), + "fragmented header: split Content-Length is accepted"); + CHECK(stream_contains("Location: index.html\r\n"), + "fragmented header: login body is parsed after reassembly"); +} + +static void scenario_split_content_type(void) +{ + static const char first[] = + "POST /login HTTP/1.1\r\n" + "Content-Length: 8\r\n" + "Content-Ty"; + static const char second[] = + "pe: application/x-www-form-urlencoded\r\n" + "\r\n" + "pwd=1234"; + + login_session_start(); + client_send(TCP_ACK | TCP_PSH, first, (int)strlen(first)); + CHECK(stream_len == 0, + "split Content-Type: incomplete header has no response"); + client_send(TCP_ACK | TCP_PSH, second, (int)strlen(second)); + CHECK(stream_contains("Location: index.html\r\n"), + "split Content-Type: field name can span receive events"); +} + +static void scenario_split_header_terminator(void) +{ + static const char first[] = + "POST /login HTTP/1.1\r\n" + "Content-Length: 8\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "\r"; + static const char second[] = "\npwd=1234"; + + login_session_start(); + client_send(TCP_ACK | TCP_PSH, first, (int)strlen(first)); + CHECK(stream_len == 0, + "split terminator: CRLFCR fragment has no premature response"); + client_send(TCP_ACK | TCP_PSH, second, (int)strlen(second)); + CHECK(stream_contains("Location: index.html\r\n"), + "split terminator: CRLFCRLF can span receive events"); +} + +static void scenario_header_then_body(void) +{ + static const char header[] = + "POST /login HTTP/1.1\r\n" + "Content-Length: 8\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "\r\n"; + + login_session_start(); + client_send(TCP_ACK | TCP_PSH, header, (int)strlen(header)); + CHECK(stream_len == 0, + "header-only segment: waits for the declared body"); + client_send(TCP_ACK | TCP_PSH, "pwd=1234", 8); + CHECK(stream_contains("Location: index.html\r\n"), + "header-only segment: following body completes login"); +} + +static void scenario_header_with_partial_body(void) +{ + static const char first[] = + "POST /login HTTP/1.1\r\n" + "Content-Length: 8\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "\r\n" + "pwd="; + + login_session_start(); + client_send(TCP_ACK | TCP_PSH, first, (int)strlen(first)); + CHECK(stream_len == 0, + "partial body: waits when final header segment has only part of body"); + client_send(TCP_ACK | TCP_PSH, "1234", 4); + CHECK(stream_contains("Location: index.html\r\n"), + "partial body: existing body reassembly takes over correctly"); +} + +static void scenario_post_header_timeout(void) +{ + static const char partial[] = + "POST /login HTTP/1.1\r\n" + "Content-Length: 8\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n"; + + login_session_start(); + ticks = 0; + client_send(TCP_ACK | TCP_PSH, partial, (int)strlen(partial)); + CHECK(stream_len == 0, + "header timeout: incomplete header has no immediate response"); + + /* The firmware timeout is 5 seconds; 2000 host ticks is safely beyond it. */ + ticks = 2000; + run_periodic(1); + CHECK(!uip_conn_active(0), + "header timeout: connection is aborted when the header never completes"); +} + +static void scenario_oversized_post_header(void) +{ + static const char start[] = + "POST /login HTTP/1.1\r\n" + "X-Fill: "; + char fill[1200]; + + login_session_start(); + memset(fill, 'a', sizeof(fill)); + client_send(TCP_ACK | TCP_PSH, start, (int)strlen(start)); + + /* Keep extending a header with no CRLFCRLF until the fixed buffer refuses it. */ + for (int i = 0; i < 16 && !stream_contains("400 Bad Request"); i++) + client_send(TCP_ACK | TCP_PSH, fill, (int)sizeof(fill)); + + CHECK(stream_contains("HTTP/1.1 400 Bad Request\r\n"), + "oversized header: rejected before the header buffer overflows"); +} + +static void scenario_fragmented_missing_content_length(void) +{ + static const char first[] = + "POST /login HTTP/1.1\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n"; + static const char second[] = "\r\npwd=1234"; + + login_session_start(); + client_send(TCP_ACK | TCP_PSH, first, (int)strlen(first)); + client_send(TCP_ACK | TCP_PSH, second, (int)strlen(second)); + CHECK(stream_contains("HTTP/1.1 411 Length Required\r\n"), + "fragmented header: missing Content-Length still returns 411"); +} + int main(int argc, char **argv) { if (argc > 1 && !strcmp(argv[1], "-v")) @@ -504,6 +672,14 @@ int main(int argc, char **argv) scenario_growing_window(); scenario_rexmit_after_shrink(); scenario_bad_l4_checksum(); + scenario_fragmented_post_header(); + scenario_split_content_type(); + scenario_split_header_terminator(); + scenario_header_then_body(); + scenario_header_with_partial_body(); + scenario_post_header_timeout(); + scenario_oversized_post_header(); + scenario_fragmented_missing_content_length(); printf("\n%s (%d failure%s)\n", failures ? "BENCH: FAILURES" : "BENCH: ALL PASS",