Implement authenticated HTTPS OTA uploads with bounded streaming, image validation, reboot coordination, and lifecycle exclusion. Add the admin UI, regression tests, and Phase 10 acceptance documentation.
5.1 KiB
Firmware upload backend host tests
Run from the repository root:
python3 tests/web_firmware_update/run.py
python3 tests/web_cookie_auth/run.py
The first compiles the unchanged production upload source, production auth
admission helpers/parser, and production server upload reservation/typed reboot
functions with -Wall -Wextra -Werror and trapping undefined-behavior checks.
It uses installed ESP-IDF image-layout headers (IDF_PATH, otherwise the normal
PlatformIO framework directory). It performs no firmware build or device action.
HTTP IO, private header-adapter admission, database, OTA/flash and FreeRTOS
scheduling are doubles. The existing cookie-auth suite separately exercises
production private-adapter framing/duplicate-header handling and session storage.
The 61 cases cover:
- Cookie, Origin, CSRF, admin, query/method and readiness rejection before reads.
- Raw content type, nonempty body, actual partition capacity and inactive OTA app selection; bytewise partial image prefix and bounded/exact streaming writes.
- Wrong magic/chip/segment/descriptor/hash-header rejection before OTA erase.
- Service/identity busy, allocation/task creation and OTA begin/write/end/metadata/ boot-selection failures; handle consumption versus abort cleanup.
- Parsed SDK image length mismatch, final session revocation/currentness failure.
- EOF before/after OTA begin, stalled receive and total slow-drip deadlines.
- Reservation rejection of competing upload/typed reboot, including during receive and boot selection; response failure at every response stage.
- Successful commit followed by delayed reboot-owner action; failed response retains selection but does not reboot and releases reservations/resources.
These are deterministic failure-injection tests, not real SDK image-integrity, flash, concurrent scheduler, TLS, network, power-loss or hardware test evidence. Stop/start/rotation guard use is source-checked, not dynamically executed here.
Parent integration contract
POST /api/firmware, raw application/octet-stream, known Content-Length,
existing cookie + same-origin Origin + X-CSRF-Token, admin only. Use the
existing browser CORS-mode/same-origin-credentials request pattern. No multipart,
chunked transfer, Expect, query parameters, signature/version policy or filename
validation. Standard ESP32-S3 application image with appended SDK SHA-256 digest
is required. A filename alone is never trusted.
200 {"ok":true,"rebooting":true} means SDK validation and boot selection
succeeded. After synchronous send success, the preallocated owner task waits
500 ms and calls esp_restart() without stopping HTTPD or retaining a request
or socket. It keeps service/identity reservations until reset. Send success is
not peer receipt. Send failure after selection schedules no reboot, releases
reservations, and leaves the accepted image selected for a later reset. A lost
acknowledgement is therefore uncertain; never automatically retry.
All errors use {"error":"code"}:
| HTTP | Codes |
|---|---|
| 400 | invalid_request, invalid_firmware, firmware_incomplete |
| 401 | authentication_required |
| 403 | origin, csrf, admin_required |
| 408 | firmware_timeout |
| 413 | firmware_too_large |
| 415 | firmware_content_type |
| 500 | firmware_write_failed, firmware_commit_failed |
| 503 | unavailable, busy, firmware_unavailable, firmware_resources |
Rejections with unread bodies close rather than asking HTTPD to discard the remaining upload. Responses are JSON, no-store, nosniff and no-referrer. Errors contain no SDK diagnostic text or secret material. A boot-selection API failure is not retried or followed by reboot; flash/power failures at metadata commit cannot offer transactional certainty beyond SDK guarantees.
The synchronous handler may block other HTTPD work for the upload duration. A 4 KiB heap buffer and a 2 KiB reboot-owner stack are prepared before flashing; no whole-image allocation. Receive checks enforce 10 seconds without progress and 120 seconds total, using the server's existing one-second receive timeout. These checks do not preempt SDK flash/validation calls or scheduler delays. Only the inactive app and SDK OTA metadata are written, not NVS/data partitions.
Integration work outside backend ownership
- Server URI capacity is now 40;
server_lifecycle.pycurrently fails its hardcoded 36-initializer extraction check (now 37). Its old 39-handler capacity, required-registration positions 1–16 and optional positions 17–18 also need adjustment to 40, 1–17 and 18–19, respectively, plus a firmware-handler fake. That existing fixture is intentionally not edited here. - HTTPS stop/start/rotation and typed reboot share the upload reservation; direct
identity mutation is separately reserved. Direct
esp_restart()callers insystem_console.c,ssh_transport.c,web_admin_transport.c, andlocal_status_ui.cdo not share this fence. Those files were outside the backend's allowed ownership. The parent must coordinate those reboot paths for global software-reboot exclusion; do not claim that property yet.