Add admin firmware upload support
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.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
# Firmware upload backend host tests
|
||||
|
||||
Run from the repository root:
|
||||
|
||||
```sh
|
||||
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.py` currently 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 in
|
||||
`system_console.c`, `ssh_transport.c`, `web_admin_transport.c`, and
|
||||
`local_status_ui.c` do 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.
|
||||
Reference in New Issue
Block a user