From 1bd4a8d3cc84decd77c3b6f6a7a67f161605c196 Mon Sep 17 00:00:00 2001 From: bloqaudio Date: Fri, 21 Aug 2026 08:49:29 -0500 Subject: [PATCH 1/4] httpd: buffer a firmware upload's part header before streaming A firmware image cannot be buffered whole, so the upload is streamed to flash, and stream_upload() already resumes across TCP segments. The multipart preamble did not: handle_post() walked the part headers from the start of whichever segment it held, so a client that split inside the octet-stream part header lost its place, never started streaming, and the request hung. Firefox splits exactly there, right after filename=". Buffer the multipart body only until the octet-stream part header is complete, locate the payload, then stream from that point; later segments stream as before. The header reuses the configuration buffer, which is idle during a firmware upload, so no extra memory is needed. --- httpd/httpd.c | 73 +++++++++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/httpd/httpd.c b/httpd/httpd.c index 84413a9..585eb62 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -49,6 +49,7 @@ __xdata uint8_t boundary[72]; __xdata uint8_t config_upload; __xdata uint8_t config_buf[CONFIG_UPLOAD_BUF]; __xdata uint16_t cfg_pos, cfg_hdr, cfg_body, cfg_end, cfg_last; +__xdata uint16_t pre_acc; __xdata uint8_t cfg_bl; __xdata uint8_t * __xdata content_type = 0; __xdata uint8_t * __xdata session = 0; @@ -240,17 +241,6 @@ void send_unauthorized(void) } -__xdata uint8_t *skip_boundary(__xdata uint8_t *p) -{ - while (*p) { - if (is_word_x(p, boundary)) - return p + strlen_x(boundary); - p++; - } - return p; -} - - __xdata uint8_t *scan_header(__xdata uint8_t * __xdata p) { content_type = 0; @@ -386,21 +376,38 @@ static uint8_t config_take(void) } +// unlike scan_header(), keeps no auth state, so it may run on every buffered segment +static uint16_t preamble_payload_start(__xdata uint16_t n) +{ + for (cfg_pos = 0; cfg_pos + 24 <= n; cfg_pos++) { + if (strstart(&config_buf[cfg_pos], "application/octet-stream")) + break; + } + if (cfg_pos + 24 > n) + return 0; + cfg_pos += 24; + while (cfg_pos + 3 < n && !strstart(&config_buf[cfg_pos], "\r\n\r\n")) + cfg_pos++; + if (cfg_pos + 3 >= n) + return 0; + return cfg_pos + 4; +} + + /* * Reads post data from the http stream and writes it into flash memory * Input: the current position in the TCP buffer (uip_appdata) * Returns 1: More data to read, 0: Upload complete, all parts reads */ -uint8_t stream_upload(uint16_t bptr) +uint8_t stream_upload(__xdata uint8_t *p, uint16_t bptr, uint16_t plen) { - __xdata uint8_t *p = uip_appdata; __xdata struct httpd_state * __xdata s = &(uip_conn->appstate); dbg_string("Stream_upload called: "); dbg_short(bptr); dbg_char('\n'); do { - if (bptr >= uip_len) { + if (bptr >= plen) { s->tstate = TSTATE_POST; return 1; } @@ -431,7 +438,7 @@ uint8_t stream_upload(uint16_t bptr) flash_region.addr = uptr; flash_region.len = 1; flash_write_bytes(flash_buf); - if (bptr >= uip_len) + if (bptr >= plen) return 0; if(!verify_crc) //ugly hack to signal connection finished after config upload. @@ -512,6 +519,7 @@ void handle_post(void) uptr = FIRMWARE_UPLOAD_START; verify_crc = 1; max_upload = 1024576; + pre_acc = 0; } else if (is_word(request_path, "config")) { if (!authenticated) { send_unauthorized(); @@ -600,21 +608,21 @@ void handle_post(void) slen = strtox(outbuf, "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n"); return; } - // We skip the intial parts as part of the header - do { - p = skip_boundary(p); - if (!*p) { - s->tstate = TSTATE_MULTIPART; - return; - } - p = scan_header(p); - if (!*p) - goto bad_request; - if (!content_type) // We are waiting for the part with the octet stream - continue; - } while (!is_word(content_type, "application/octet-stream")); + cfg_pos = uip_len - (p - uip_appdata); + if (pre_acc + cfg_pos >= CONFIG_UPLOAD_BUF) { + print_string("Firmware upload header too large, aborting.\n"); + s->tstate = TSTATE_NONE; + send_bad_request(); + return; + } + memcpy(config_buf + pre_acc, p, cfg_pos); + pre_acc += cfg_pos; + cfg_end = preamble_payload_start(pre_acc); + if (!cfg_end) { + s->tstate = TSTATE_MULTIPART; + return; + } dbg_string("Have content octets\n"); - p += 4; // Skip \r\n\r\n sequence at end of preamble of part flash_init(0); // Re-initialize flash for non-DIO operation, otherwise flashing fails set_sys_led_state(SYS_LED_FAST); @@ -622,7 +630,7 @@ void handle_post(void) crc_value = 0; bindex = 0; write_len = 0; - stream_upload(p - uip_appdata); + stream_upload(config_buf, cfg_end, pre_acc); dbg_string("Done reading first fragment\n"); return; @@ -633,9 +641,6 @@ void handle_post(void) } slen = strtox(outbuf, "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n"); return; -bad_request: - send_bad_request(); - return; } @@ -703,7 +708,7 @@ void httpd_appcall(void) } else if (uip_newdata() && s->tstate == TSTATE_POST) { // Check here maxupload by subtracting uip_len and close socekt if fails! if (max_upload - uip_len > 0) { - stream_upload(0); + stream_upload(uip_appdata, 0, uip_len); write_char('.'); } else { send_bad_request(); From be7796a22c011e2940737ce6af33bf87caef83b1 Mon Sep 17 00:00:00 2001 From: bloqaudio Date: Tue, 25 Aug 2026 09:06:56 -0500 Subject: [PATCH 2/4] httpd: send a real verdict for firmware uploads A firmware upload previously ended in a silent connection close, leaving the client unable to distinguish a verified upload from a failed one. Send an explicit 200/400 verdict with the CRC result, with Content-Length so the browser completes the response before the reset, and only reset the chip once the verdict has been fully ACKed. The unconditional close after a config upload is gone since the buffered config path answers with its own response, so drop the now-unreachable close hack from the streaming path. --- httpd/httpd.c | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/httpd/httpd.c b/httpd/httpd.c index 585eb62..fa80b01 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -61,6 +61,9 @@ __xdata uint32_t max_upload; __xdata uint16_t short_parsed; __xdata char passwd[21]; +// Set when a verified firmware upload awaits its response ACK, after +// which the chip resets to apply the staged image +__xdata uint8_t fw_reset_pending; __xdata char session_id[SESSION_ID_LENGTH + 1]; __xdata uint8_t authenticated; __xdata uint32_t now; @@ -92,6 +95,7 @@ void httpd_init(void) __banked // Start listening to port 80 uip_listen(HTONS(80)); s->tstate = TSTATE_CLOSED; + fw_reset_pending = 0; // xdata is not zeroed by the startup code } @@ -420,17 +424,23 @@ uint8_t stream_upload(__xdata uint8_t *p, uint16_t bptr, uint16_t plen) flash_write_bytes(flash_buf); uptr += write_len; write_len = 0; - // TODO: This is a bit premature, what about a nice web-page saying the device will reset??? if (verify_crc) { dbg_string("CRC16: "); dbg_short(crc_final); dbg_char('\n'); + // Both bodies are 33 bytes; Content-Length lets the + // browser complete the response without waiting for + // the connection close (which a reset would swallow) if (crc_final == 0xb001) { print_string("Checksum OK.\nUpload to flash done, will reset!\n"); - // close connection to avoid retries by browser - uip_close(); - reset_chip(); + slen = strtox(outbuf, "HTTP/1.1 200 OK\r\nContent-Length: 33\r\n" + "Content-Type: text/plain\r\n\r\n" + "OK: checksum verified, rebooting\n"); + // Reset once the response is fully ACKed + fw_reset_pending = 1; } else { print_string("Checksum incorrect! Aborting.\n"); - uip_close(); + slen = strtox(outbuf, "HTTP/1.1 400 Bad Request\r\nContent-Length: 33\r\n" + "Content-Type: text/plain\r\n\r\n" + "NO: checksum failed, not applied\n"); } } // Make sure there is a 0 at the end of the uploaded data @@ -440,9 +450,6 @@ uint8_t stream_upload(__xdata uint8_t *p, uint16_t bptr, uint16_t plen) flash_write_bytes(flash_buf); if (bptr >= plen) return 0; - if(!verify_crc) - //ugly hack to signal connection finished after config upload. - uip_close(); return 1; } if (p[bptr] == boundary[bindex]) { @@ -630,6 +637,10 @@ void handle_post(void) crc_value = 0; bindex = 0; write_len = 0; + // A verdict is only built once the upload part completes; + // clear any stale response so the completion check in the + // appcall POST branch cannot send leftovers + slen = 0; stream_upload(config_buf, cfg_end, pre_acc); dbg_string("Done reading first fragment\n"); @@ -704,11 +715,20 @@ void httpd_appcall(void) cont_len -= slen; cont_addr += slen; s->tstate = TSTATE_TX; + } else if (fw_reset_pending) { + // The upload verdict has been fully ACKed by the client; + // now it is safe to reset and apply the staged image + print_string("Resetting to apply update\n"); + reset_chip(); } } else if (uip_newdata() && s->tstate == TSTATE_POST) { // Check here maxupload by subtracting uip_len and close socekt if fails! if (max_upload - uip_len > 0) { stream_upload(uip_appdata, 0, uip_len); + // A completed part with a built verdict must go out + // through the normal TX path + if (s->tstate == TSTATE_NONE && slen) + goto do_send; write_char('.'); } else { send_bad_request(); From b63117ba6c59417e373c70d16d57fcc466f29218 Mon Sep 17 00:00:00 2001 From: bloqaudio Date: Thu, 27 Aug 2026 08:51:01 -0500 Subject: [PATCH 3/4] httpd: scan the upload preamble with a register cursor The preamble scan cursor and the buffered-length parameter carry no state between calls, so they do not need static xdata slots. As plain locals the compiler places both in registers, trimming 64 bytes of BANK1 code and two bytes of xdata. The offsets shared by config_take() stay static: direct data space is fully allocated on machines like the SWTGW218AS, so plain locals there add overlay bytes that no longer link, and xdata-class locals spill three temporaries into direct space while growing the code by roughly 120 bytes. Document pre_acc, whose accumulation across TCP segments is why it must remain global. --- httpd/httpd.c | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/httpd/httpd.c b/httpd/httpd.c index fa80b01..0e73f57 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -49,6 +49,7 @@ __xdata uint8_t boundary[72]; __xdata uint8_t config_upload; __xdata uint8_t config_buf[CONFIG_UPLOAD_BUF]; __xdata uint16_t cfg_pos, cfg_hdr, cfg_body, cfg_end, cfg_last; +// part-header bytes buffered so far; accumulates across TCP segments __xdata uint16_t pre_acc; __xdata uint8_t cfg_bl; __xdata uint8_t * __xdata content_type = 0; @@ -381,20 +382,22 @@ static uint8_t config_take(void) // unlike scan_header(), keeps no auth state, so it may run on every buffered segment -static uint16_t preamble_payload_start(__xdata uint16_t n) +static uint16_t preamble_payload_start(uint16_t n) { - for (cfg_pos = 0; cfg_pos + 24 <= n; cfg_pos++) { - if (strstart(&config_buf[cfg_pos], "application/octet-stream")) + uint16_t pos; + + for (pos = 0; pos + 24 <= n; pos++) { + if (strstart(&config_buf[pos], "application/octet-stream")) break; } - if (cfg_pos + 24 > n) + if (pos + 24 > n) return 0; - cfg_pos += 24; - while (cfg_pos + 3 < n && !strstart(&config_buf[cfg_pos], "\r\n\r\n")) - cfg_pos++; - if (cfg_pos + 3 >= n) + pos += 24; + while (pos + 3 < n && !strstart(&config_buf[pos], "\r\n\r\n")) + pos++; + if (pos + 3 >= n) return 0; - return cfg_pos + 4; + return pos + 4; } From e826180d068d0bd3a43ff471f89564ae90861c9a Mon Sep 17 00:00:00 2001 From: bloqaudio Date: Sun, 30 Aug 2026 02:15:06 -0500 Subject: [PATCH 4/4] httpd: pass stream_upload() state through an xdata struct --- httpd/httpd.c | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/httpd/httpd.c b/httpd/httpd.c index 0e73f57..23fef5d 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -401,20 +401,27 @@ static uint16_t preamble_payload_start(uint16_t n) } +// Source window for stream_upload(); filled by the caller before the call +__xdata struct { + __xdata uint8_t *p; + uint16_t bptr; + uint16_t plen; +} upload_settings; + /* * Reads post data from the http stream and writes it into flash memory - * Input: the current position in the TCP buffer (uip_appdata) + * Input: upload_settings, set by the caller * Returns 1: More data to read, 0: Upload complete, all parts reads */ -uint8_t stream_upload(__xdata uint8_t *p, uint16_t bptr, uint16_t plen) +uint8_t stream_upload(void) { __xdata struct httpd_state * __xdata s = &(uip_conn->appstate); dbg_string("Stream_upload called: "); - dbg_short(bptr); dbg_char('\n'); + dbg_short(upload_settings.bptr); dbg_char('\n'); do { - if (bptr >= plen) { + if (upload_settings.bptr >= upload_settings.plen) { s->tstate = TSTATE_POST; return 1; } @@ -451,15 +458,15 @@ uint8_t stream_upload(__xdata uint8_t *p, uint16_t bptr, uint16_t plen) flash_region.addr = uptr; flash_region.len = 1; flash_write_bytes(flash_buf); - if (bptr >= plen) + if (upload_settings.bptr >= upload_settings.plen) return 0; return 1; } - if (p[bptr] == boundary[bindex]) { + if (upload_settings.p[upload_settings.bptr] == boundary[bindex]) { if (!bindex) crc_final = crc_value; - crc16(p + bptr); - bptr++; + crc16(upload_settings.p + upload_settings.bptr); + upload_settings.bptr++; bindex++; } else { if (bindex) { @@ -467,8 +474,8 @@ uint8_t stream_upload(__xdata uint8_t *p, uint16_t bptr, uint16_t plen) write_len += bindex; bindex = 0; } - crc16(p + bptr); - flash_buf[write_len++] = p[bptr++]; + crc16(upload_settings.p + upload_settings.bptr); + flash_buf[write_len++] = upload_settings.p[upload_settings.bptr++]; if (write_len >= FLASH_PAGE_SIZE) { dbg_string("len: "); dbg_short(write_len); dbg_char(' '); dbg_string("CRC16: "); dbg_short(crc_value); dbg_char('\n'); @@ -644,7 +651,10 @@ void handle_post(void) // clear any stale response so the completion check in the // appcall POST branch cannot send leftovers slen = 0; - stream_upload(config_buf, cfg_end, pre_acc); + upload_settings.p = config_buf; + upload_settings.bptr = cfg_end; + upload_settings.plen = pre_acc; + stream_upload(); dbg_string("Done reading first fragment\n"); return; @@ -727,7 +737,10 @@ void httpd_appcall(void) } else if (uip_newdata() && s->tstate == TSTATE_POST) { // Check here maxupload by subtracting uip_len and close socekt if fails! if (max_upload - uip_len > 0) { - stream_upload(uip_appdata, 0, uip_len); + upload_settings.p = uip_appdata; + upload_settings.bptr = 0; + upload_settings.plen = uip_len; + stream_upload(); // A completed part with a built verdict must go out // through the normal TX path if (s->tstate == TSTATE_NONE && slen)