Merge upstream/main into PR #582; preserve HTTP upload reset ownership

This commit is contained in:
avicarmeli committed 2026-10-20 21:25:37 +03:00
commit 674dc23a27
2 files changed
+493 -17

No files matched your search

+71 -15
View File
@@ -55,8 +55,8 @@ __xdata uint8_t boundary[72];
#define CONFIG_UPLOAD_BUF (CONFIG_LEN + 384)
__xdata uint8_t config_upload;
__xdata uint8_t config_buf[CONFIG_UPLOAD_BUF];
// bytes buffered in config_buf so far (config body, or a firmware part
// header); accumulates across TCP segments
// bytes buffered in config_buf so far (HTTP header, POST body, or a firmware
// part header); accumulates across TCP segments
__xdata uint16_t pre_acc;
__xdata uint8_t * __xdata content_type = 0;
__xdata uint8_t * __xdata session = 0;
@@ -87,6 +87,7 @@ __xdata uint32_t last_session_use;
#define TSTATE_POST 4
#define TSTATE_MULTIPART 5
#define TSTATE_POSTBODY 6
#define TSTATE_HEADER 7
extern __xdata uint16_t crc_value;
__xdata uint16_t crc_final;
@@ -106,7 +107,8 @@ void httpd_init(void) __banked
uip_listen(HTONS(80));
// Not through uip_conn: it only points at a connection while uIP is
// handling one, and nothing has set it yet at init time.
uip_conns[0].appstate.tstate = TSTATE_CLOSED;
for (uint8_t i = 0; i < UIP_CONNS; i++)
uip_conns[i].appstate.tstate = TSTATE_CLOSED;
}
@@ -742,7 +744,42 @@ static void post_body_continue(void)
}
void handle_post(void)
/*
* Buffer an HTTP 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 request_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_HEADER;
return 0;
}
void handle_post(uint16_t body_off)
{
__xdata struct httpd_state * __xdata s = &(uip_conn->appstate);
__xdata uint8_t *p = uip_appdata;
@@ -755,6 +792,9 @@ void handle_post(void)
// Was the multipart header sent in multiple packets?
if (s->tstate != TSTATE_MULTIPART) {
p = config_buf;
request_path = p + 6;
dbg_string("Is POST\n");
p += 5; // Skip post
// Find end of request path
@@ -768,9 +808,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();
@@ -796,7 +839,10 @@ void handle_post(void)
dbg_string("Configuration upload\n");
verify_crc = 0;
config_upload = 1;
pre_acc = 0;
// config_take() matches CRLF-prefixed delimiters, including the first.
config_buf[0] = '\r';
config_buf[1] = '\n';
pre_acc = 2;
}
// Check for other POST requests, which are not multipart, below
} else {
@@ -820,7 +866,6 @@ void handle_post(void)
handle_firmware_fragment(p);
return;
} else if (is_word(request_path, "cmd")) {
p += 4;
if (!authenticated) {
send_unauthorized();
return;
@@ -839,7 +884,6 @@ void handle_post(void)
return;
}
p += 4;
postbody_endpoint = POSTBODY_LOGIN;
if (!post_body_take(p))
return;
@@ -887,9 +931,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_HEADER)
&& (uint16_t)ticks - postbody_start > POSTBODY_TIMEOUT) {
dbg_string("Body never arrived\n");
dbg_string("HTTP request never completed\n");
uip_abort();
s->tstate = TSTATE_CLOSED;
}
@@ -952,6 +996,7 @@ void httpd_appcall(void)
cont_len = 0;
dbg_char('<'); dbg_short(uip_len); dbg_char('\n');
__xdata uint8_t *p = uip_appdata;
uint16_t body_off = 0;
// Mark end of request header with \0
p[uip_len] = 0;
#ifdef DEBUG
@@ -960,9 +1005,21 @@ void httpd_appcall(void)
dbg_char('\n');
#endif
p = uip_appdata;
if (is_word(p, "POST") || s->tstate == TSTATE_MULTIPART
|| s->tstate == TSTATE_POSTBODY) {
handle_post();
if (s->tstate != TSTATE_MULTIPART && s->tstate != TSTATE_POSTBODY) {
if (s->tstate != TSTATE_HEADER)
pre_acc = 0;
body_off = request_header_take();
if (!body_off) {
uip_len = 0;
return;
}
if (body_off == 0xffff)
goto do_send;
}
if (s->tstate == TSTATE_MULTIPART || s->tstate == TSTATE_POSTBODY
|| is_word(config_buf, "POST")) {
handle_post(body_off);
// 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) {
@@ -972,12 +1029,11 @@ void httpd_appcall(void)
goto do_send;
}
// We only expect a GET request here.
p = config_buf;
if (!is_word(p, "GET")) {
send_bad_request();
goto do_send;
}
dbg_string("GET request ");
p += 4;
scan_header(p);
+422 -2
View File
@@ -46,6 +46,9 @@
#define FILE_START FDATA_START_login_html
#define FILE_NAME "/login.html"
#define FILE_LEN 6000
#define PROTECTED_FILE_NAME "/index.html"
#define PROTECTED_FILE_START (FILE_START + FILE_LEN + 64)
#define PROTECTED_FILE_LEN 64
#define STREAM_MAX 16384
static int failures;
@@ -75,11 +78,17 @@ uint8_t flash_buf[FLASH_BUF_SIZE];
uint8_t rx_headers[16];
struct flash_region_t flash_region;
static uint8_t written_config[CONFIG_LEN];
static uint16_t written_config_len;
static uint32_t config_erase_addr, config_write_addr;
static int config_erases, config_writes;
const char * const mime_strings[] = { "text/html", "image/svg+xml", "image/x-icon",
"image/png", "text/javascript", "text/css", "text/plain" };
const struct f_data f_data[] = {
{ FILE_NAME, FILE_START, FILE_LEN, mime_HTML, 0 },
{ PROTECTED_FILE_NAME, PROTECTED_FILE_START, PROTECTED_FILE_LEN, mime_HTML, 0 },
{ 0, 0, 0, mime_HTML, 0 },
};
@@ -90,8 +99,20 @@ void flash_read_bulk(uint8_t *dst)
}
void flash_init(uint8_t enable_dio) { (void)enable_dio; }
void flash_sector_erase(void) { }
void flash_write_bytes(uint8_t *ptr) { (void)ptr; }
void flash_sector_erase(void)
{
config_erases++;
config_erase_addr = flash_region.addr;
}
void flash_write_bytes(uint8_t *ptr)
{
config_writes++;
config_write_addr = flash_region.addr;
written_config_len = flash_region.len;
if (written_config_len <= sizeof(written_config))
memcpy(written_config, ptr, written_config_len);
}
const char *get_flash_size_str(void) { return "512 kB"; }
void crc16_bank1(uint8_t *v) { (void)v; }
static unsigned reset_calls;
@@ -157,6 +178,8 @@ void send_vlanlist(void) { }
void send_lldp(void) {}
extern uint8_t authenticated;
extern char session_id[];
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. */
@@ -363,6 +386,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)
@@ -606,6 +645,371 @@ static void scenario_upload_reset_owner(void)
uip_flags = UIP_ACKDATA;
httpd_appcall();
CHECK(reset_calls == 0, "failed CRC verdict ACK does not reset");
}
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");
}
static void scenario_fragmented_get_cookie(void)
{
static const char first[] =
"GET " PROTECTED_FILE_NAME " HTTP/1.1\r\n"
"Host: sw\r\n"
"Cook";
static const char second[] =
"ie: session=0123456789ab\r\n"
"\r\n";
session_start(MSS_FULL);
strcpy(session_id, "0123456789ab");
client_send(TCP_ACK | TCP_PSH, first, (int)strlen(first));
CHECK(stream_len == 0,
"fragmented GET cookie: no redirect before the header completes");
client_send(TCP_ACK | TCP_PSH, second, (int)strlen(second));
CHECK(stream_contains("HTTP/1.1 200 OK\r\n"),
"fragmented GET cookie: protected index is served");
CHECK(!stream_contains("Location: login.html\r\n"),
"fragmented GET cookie: session is not lost on the redirected GET");
}
static void scenario_fragmented_get_cookie_value(void)
{
static const char first[] =
"GET " PROTECTED_FILE_NAME " HTTP/1.1\r\n"
"Host: sw\r\n"
"Cookie: session=012345";
static const char second[] =
"6789ab\r\n"
"\r\n";
session_start(MSS_FULL);
strcpy(session_id, "0123456789ab");
client_send(TCP_ACK | TCP_PSH, first, (int)strlen(first));
CHECK(stream_len == 0,
"fragmented GET cookie value: waits for the rest of the header");
client_send(TCP_ACK | TCP_PSH, second, (int)strlen(second));
CHECK(stream_contains("HTTP/1.1 200 OK\r\n"),
"fragmented GET cookie value: split session value authenticates");
}
static void scenario_split_get_header_terminator(void)
{
static const char first[] =
"GET " PROTECTED_FILE_NAME " HTTP/1.1\r\n"
"Cookie: session=0123456789ab\r\n"
"\r";
static const char second[] = "\n";
session_start(MSS_FULL);
strcpy(session_id, "0123456789ab");
client_send(TCP_ACK | TCP_PSH, first, (int)strlen(first));
CHECK(stream_len == 0,
"fragmented GET terminator: CRLFCR does not trigger parsing");
client_send(TCP_ACK | TCP_PSH, second, 1);
CHECK(stream_contains("HTTP/1.1 200 OK\r\n"),
"fragmented GET terminator: CRLFCRLF may span receive events");
}
static void scenario_fragmented_get_without_cookie(void)
{
static const char first[] =
"GET " PROTECTED_FILE_NAME " HTTP/1.1\r\n"
"Host: sw\r\n";
static const char second[] = "\r\n";
session_start(MSS_FULL);
strcpy(session_id, "0123456789ab");
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 302 Found\r\n"),
"fragmented GET without cookie: protected file still redirects");
CHECK(stream_contains("Location: login.html\r\n"),
"fragmented GET without cookie: authentication is not carried over");
}
static void scenario_get_header_timeout(void)
{
static const char partial[] =
"GET " PROTECTED_FILE_NAME " HTTP/1.1\r\n"
"Host: sw\r\n"
"Cookie: session=012345";
session_start(MSS_FULL);
strcpy(session_id, "0123456789ab");
ticks = 0;
client_send(TCP_ACK | TCP_PSH, partial, (int)strlen(partial));
CHECK(stream_len == 0,
"fragmented GET timeout: incomplete header has no immediate response");
ticks = 2000;
run_periodic(1);
CHECK(!uip_conn_active(0),
"fragmented GET timeout: incomplete request is eventually aborted");
}
static void scenario_oversized_get_header(void)
{
static const char start[] =
"GET " PROTECTED_FILE_NAME " HTTP/1.1\r\n"
"X-Fill: ";
char fill[1200];
session_start(MSS_FULL);
memset(fill, 'a', sizeof(fill));
client_send(TCP_ACK | TCP_PSH, start, (int)strlen(start));
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 GET header: fixed header buffer is bounds checked");
}
static void scenario_split_method_names(void)
{
static const char get[] =
"GET " PROTECTED_FILE_NAME " HTTP/1.1\r\n"
"Cookie: session=0123456789ab\r\n\r\n";
static const char post[] =
"POST /login HTTP/1.1\r\n"
"Content-Length: 8\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n\r\n"
"pwd=1234";
int get_waits = 1, get_ok = 1, post_waits = 1, post_ok = 1;
for (int split = 1; split < 3; split++) {
session_start(MSS_FULL);
strcpy(session_id, "0123456789ab");
client_send(TCP_ACK | TCP_PSH, get, split);
if (stream_len)
get_waits = 0;
client_send(TCP_ACK | TCP_PSH, get + split, (int)strlen(get) - split);
if (!stream_contains("HTTP/1.1 200 OK\r\n"))
get_ok = 0;
}
CHECK(get_waits, "split GET method: incomplete verb waits for the header");
CHECK(get_ok, "split GET method: every split inside GET serves the file");
for (int split = 1; split < 4; split++) {
login_session_start();
client_send(TCP_ACK | TCP_PSH, post, split);
if (stream_len)
post_waits = 0;
client_send(TCP_ACK | TCP_PSH, post + split, (int)strlen(post) - split);
if (!stream_contains("Location: index.html\r\n"))
post_ok = 0;
}
CHECK(post_waits, "split POST method: incomplete verb waits for the header");
CHECK(post_ok, "split POST method: every split inside POST completes login");
}
static void scenario_config_upload(void)
{
static const char expected[] = "hostname upload-test\nip 10.0.0.1\n";
static const char body[] =
"--upload-boundary\r\n"
"Content-Disposition: form-data; name=\"file\"; filename=\"config.txt\"\r\n"
"Content-Type: text/plain\r\n\r\n"
"hostname upload-test\nip 10.0.0.1\n"
"\r\n--upload-boundary--\r\n";
char request[768];
int request_len, first_bad_response = -1, first_bad_flash = -1;
request_len = snprintf(request, sizeof(request),
"POST /config HTTP/1.1\r\n"
"Host: sw\r\n"
"Cookie: session=0123456789ab\r\n"
"Content-Type: multipart/form-data; boundary=upload-boundary\r\n"
"Content-Length: %zu\r\n\r\n%s", sizeof(body) - 1, body);
if (request_len < 0 || request_len >= (int)sizeof(request)) {
CHECK(0, "multipart/config: test request fits the client buffer");
return;
}
/* The same well-formed request must work in one event and with a split
* anywhere in its HTTP header, first multipart delimiter, part headers,
* configuration, or closing delimiter. No extra CRLF is added to the body. */
for (int split = 0; split <= request_len; split++) {
session_start(MSS_FULL);
strcpy(session_id, "0123456789ab");
config_erases = config_writes = 0;
written_config_len = 0;
memset(written_config, 0xa5, sizeof(written_config));
if (split)
client_send(TCP_ACK | TCP_PSH, request, split);
if (split < request_len)
client_send(TCP_ACK | TCP_PSH, request + split, request_len - split);
if (!stream_contains("HTTP/1.1 200 OK\r\n") && first_bad_response < 0)
first_bad_response = split;
if ((config_erases != 1 || config_writes != 1
|| config_erase_addr != CONFIG_START || config_write_addr != CONFIG_START
|| written_config_len != sizeof(expected)
|| memcmp(written_config, expected, sizeof(expected))) && first_bad_flash < 0)
first_bad_flash = split;
}
if (first_bad_response >= 0)
printf(" multipart/config: first rejected request split at byte %d\n", first_bad_response);
if (first_bad_flash >= 0)
printf(" multipart/config: first incorrect flash write at split %d\n", first_bad_flash);
CHECK(first_bad_response < 0,
"multipart/config: complete request and every two-event split return 200");
CHECK(first_bad_flash < 0,
"multipart/config: every split writes the exact configuration and NUL to its sector");
}
int main(int argc, char **argv)
@@ -620,6 +1024,22 @@ int main(int argc, char **argv)
scenario_rexmit_after_shrink();
scenario_bad_l4_checksum();
scenario_upload_reset_owner();
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();
scenario_fragmented_get_cookie();
scenario_fragmented_get_cookie_value();
scenario_split_get_header_terminator();
scenario_fragmented_get_without_cookie();
scenario_get_header_timeout();
scenario_oversized_get_header();
scenario_split_method_names();
scenario_config_upload();
printf("\n%s (%d failure%s)\n",
failures ? "BENCH: FAILURES" : "BENCH: ALL PASS",