Mark web administration complete, centralize current contracts and acceptance evidence, and remove superseded slice records. Update roadmap, architecture notes, and test references without changing firmware sources.
129 lines
24 KiB
Markdown
129 lines
24 KiB
Markdown
# Web administration contracts
|
||
|
||
Current, accepted firmware behavior. Phase status and executed-evidence limits belong in the [roadmap](roadmap.md#phase-8--role-based-users-and-administrative-access--complete) and [acceptance record](web_administration_acceptance.md), not in implementation timelines. [Regression procedures](user_administration_tests.md#integrated-web-administration-regression-procedure) describe checks, not results. Source is authoritative; start with the [code map](agent/code-map.md).
|
||
|
||
## Authentication and admission
|
||
|
||
- HTTPS-only, same-origin login replaces Basic authentication entirely. Four digest-only opaque sessions have absolute one-hour expiry; four pre-login challenges last 120 seconds. Login is globally bounded to five credential attempts per 60 seconds. Live sessions/challenges/tickets are not evicted to admit another client.
|
||
- Raw tokens travel only in the host-only `__Host-` cookie with Secure, HttpOnly, SameSite=Strict, Path=/ and explicit lifetime; no Domain. Session records retain copied secret-free principals, CSRF state and nonzero, nonreused originating-session IDs. Passwords, raw cookies/tickets, verifiers and private keys never enter routine snapshots/logs/completion/display.
|
||
- Mutation admission requires current cookie/principal, strict Origin and CSRF validation; administration additionally requires current `admin`. Normal users retain serial/status but cannot invoke administration directly. Authentication POST fetches use CORS mode with fixed same-origin URLs and same-origin credentials: do not accept Origin `null` to compensate for browser no-referrer behavior.
|
||
- Session-store initialization is part of admitted HTTPS start; authentication failure gates HTTPS. Failed start/accepted stop disables and wipes session state. Logout invalidates only the originating session before socket cleanup; account mutation invalidates that account's sessions/tickets, including deletion/recreation, without revoking unrelated accounts. Currentness checks remain authoritative if best-effort notifications fail.
|
||
- Four serial tickets and two admin tickets are digest-only, single-use, 30-second, session/principal-bound records. Cookie/Origin/ticket/currentness and transport admission precede explicit WebSocket 101. Store RNG/SHA/database calls run outside short spinlocks; IDs/expiry/epochs fence stale publication without nested store/transport locks.
|
||
- HTTPD remains bounded to six sockets, two serial WebSockets, one admin WebSocket and 39 method/path handlers; LRU eviction is disabled. Sessions, sockets, tickets and the two shared remote-console slots are separate capacity limits. Optional settings/admin failures preserve unrelated routes where their initialization contract permits; UART0 and native USB remain independent of web readiness.
|
||
|
||
`web_httpd_adapter` alone accesses private IDF 5.5.0 HTTPD state. It rejects duplicate/ambiguous headers, postpones 101 until admission, and wipes consumed header scratch while preserving right-aligned unread bytes. Optional Settings registration stages descriptor/name allocations before publishing either, avoiding the pinned public registration failure path. Re-audit these private boundaries on SDK upgrades and same-version SDK patches: the version guard does not detect patches that retain the same version number. HTTPD response headers are pointer-backed, not copied; both `Set-Cookie` value buffers must remain valid and distinct through response send. Do not reuse or wipe those buffers before sending completes. Do not enable header/ticket debug logging. Auth documents, scripts and sensitive responses are no-store with CSP/no-referrer/frame-denial protections; authored loader changes require matching CSP hashes. Generated assets are not a normal documentation/build output.
|
||
|
||
## Terminal and console ownership
|
||
|
||
Switching Serial/Admin/Settings never creates a second serial client, requests/releases the writer lease or reconnects serial. Hidden connected terminals keep draining; only selected terminal input is sent. Both browser terminals have separate 5,000-line scrollback and 64 KiB callback-accounted pending-output bounds with visible browser-drop counts. Admin input is bounded to 4 KiB admission and 512-byte frames. Admin open/reopen is explicit; admin close is isolated. Disconnect pauses serial reconnect but retains login; logout/expiry/revocation closes affected sockets normally.
|
||
|
||
The document binds terminal state to its first validated username/role/session-stable CSRF tuple. A different identity requires a clean document before showing retained output. Pagehide fences sockets/work and hides scrollback until same-session revalidation; admin is not automatically reopened. Fit readiness caches only success and uses at most three generation-fenced animation-frame retries per external request.
|
||
|
||
`admin_ssh_console` is the sole `esp_console_run()` caller for UART0, admin SSH and browser admin. Two fixed remote-console slots are shared across SSH/browser, not two per transport. Queue records carry copied principals and transport-qualified generation tokens. Owner currentness is checked outside console locks before commands and during prompts (250 ms polling plus validation/scheduling latency), then identity is rechecked. This is operation admission, not cancellation or rollback of arbitrary executing handlers. Consumed output and retired prompt/history state are wiped.
|
||
|
||
HTTPD alone owns browser-admin socket IO and its 1,552-byte PSRAM-only payload. A 20 ms timer queues at most one owner poll, not a new task. Close uses current-owner shutdown, not queued reusable `sock_db *` pointers. Detach fences submitters; queued state retires only after successful HTTPD destruction. Failed destruction retains ownership and prevents unsafe restart/reuse.
|
||
|
||
### Browser-shell policy
|
||
|
||
Typed Settings permissions do not expand shell permissions. Parsed canonical arguments, not raw prefixes or completion suggestions, control admission:
|
||
|
||
- Browser `web` permits only `web status`, `web stop`, and exact `web certificate rotate --force`; certificate info/reset, diagnostics/performance and other web forms are denied.
|
||
- Browser `wifi`/`mdns` permit only status. Network mutation belongs to typed Settings or UART0/admin SSH.
|
||
- Browser `user` permits status/list/show and interactive add/password plus forced role/delete for **other accounts only**. Self changes, generated passwords, key commands and recovery are denied there; typed Accounts supports the separately bounded self/generated/key workflows.
|
||
- Browser SSH stop/disconnect/reset and host-key mutation are denied; typed SSH Settings has its own safe owner path. Do not claim full browser-shell parity.
|
||
- Browser `reboot` and owner-relative `exit` are supported. First-admin provisioning uses normal `user add` on UART0; unavailable-database recovery is UART0-only. The legacy `user bootstrap` and web credential commands no longer exist.
|
||
|
||
Self-affecting shell actions use the existing bounded drain/control path (up to ten seconds plus a short delay), not guaranteed peer delivery. Browser certificate rotation hands a typed action after drain/200 ms to the existing 12 KiB dispatcher, never crypto/NVS on the 4 KiB control stack. Pending input is discarded through execution, and an executing slot remains reserved across self-detach. UART0/admin SSH retain canonical recovery actions.
|
||
|
||
## Typed settings API and operation lifetime
|
||
|
||
All routes below are under `/api/settings/`. Each domain has bodyless GET snapshot plus GET/POST operation unless noted. Requests reject queries, malformed/oversized/duplicate/unknown fields and inappropriate bodies; mutation routes require bounded JSON with current admin/Origin/CSRF. GET allows absent Origin but rejects mismatch. Snapshot contention/unavailability is not an empty successful projection. HTTPD performs bounded admission/encoding only; canonical mutations run on the existing four-entry administration dispatcher using **IDs only**, not command strings, credentials, request pointers or socket handles.
|
||
|
||
| Domain and source | Snapshot / operation suffix | Request / snapshot / result buffer bounds (bytes) |
|
||
|---|---|---|
|
||
| `web_serial_settings`, serial service | `serial` / `serial-operation` | 256 / 256 / 96 |
|
||
| `web_account_settings`, user database | `accounts` / `account-operation` | 768 / 1024 (accounts), 512 (keys) / 96 |
|
||
| `web_network_settings`, Wi-Fi + mDNS | `network` / `network-operation` | 768 / 2048 / 128 |
|
||
| `web_display_settings`, local status UI | `display` / `display-operation` | 256 / 128 / 96 |
|
||
| `web_broker_settings`, session broker | `broker` / `broker-operation` | 256 / 2048 / 96 |
|
||
| `web_ssh_settings`, SSH owner/security | `ssh` / `ssh-operation` | 256 / 768 / 96 |
|
||
| `web_lifecycle_settings`, HTTPS owner/security | `lifecycle` / `lifecycle-operation` | 256 / 320 / 96 |
|
||
|
||
Mutation bodies use at most four receive attempts. Each domain retains one original-login-bound pending/result slot, nonreused operation IDs and an executing reservation. Results are replaceable, login-isolated observations, not durable history or idempotency keys. Another tab can replace a completed result. Dispatcher currentness and the 30-second dequeue deadline precede canonical owner admission. **Admitted work may finish after logout, revocation, timeout or navigation.** A failed response, 401 or disappearance of a result proves neither success nor cancellation; inspect current state before any deliberate retry.
|
||
|
||
Accounts and Network each use a one-second timer to cancel/wipe queued, non-executing secrets after 30 seconds plus scheduling latency. Shared input wipes on dequeue/rejection; executing locals wipe on return. This is not a hard wall-clock erasure/execution guarantee. Other domain deadlines are dequeue checks, not new cancellation timers. UI fences late/session-changed responses, bounds request/result checking to 15 seconds and never automatically replays/restores mutations. Serial/Accounts/Network/Display/Broker use bounded automatic checks where implemented (at most ten one-second GETs); SSH and HTTPS/Reboot require manual Check Result/Refresh.
|
||
|
||
### Serial and Display
|
||
|
||
Serial Apply/Defaults change RAM; Save persists working device state, not browser drafts. Load follows canonical defaults/fallback behavior. Reset uses canonical apply/persist/best-effort rollback. Reconfiguration/stop discards serial-service RX/TX and task-local pending data but preserves broker clients, lease and already-fanned output. USB with DTR may restart a stopped service. `/api/status` uses a zero-wait consistent serial snapshot (`running:null` if unavailable).
|
||
|
||
Display accepts dim/off timeouts 0–86400 seconds and an expected nonzero configuration generation. All writers, including CLI/legacy Apply, share a zero-wait owner reservation; NVS runs outside critical sections. Save stabilizes selected RAM; Load retains canonical fallback without rewriting NVS; Reset commits defaults **before** RAM publication, leaving RAM unchanged on failure. Buttons/diagnostic holds update activity, not configuration generation. Configuration needs an available UI task, not a physically present panel, and never takes over I2C.
|
||
|
||
### Accounts and authorized keys
|
||
|
||
The database owns eight accounts, final-admin protection and conditional target username/account-ID/auth-generation checks inside its mutation lock. Create/password/role/delete and key mutations share canonical commit/invariant logic. HTTPD uses zero-wait secret-free projections, not blocking CLI snapshots. Successful mutations request target-only web/SSH revocation; self mutation can revoke access before result retrieval.
|
||
|
||
Separate bodyless POST `accounts/generate-password` returns one 24-character value without mutation or retained retrieval. The browser uses a 60-second context-bound saved acknowledgement before separate submission; this is UX, not delivery proof, and JavaScript strings cannot be securely wiped. POST `accounts/keys` returns only slot/type/SHA-256 fingerprints for the selected identity. Key add/delete/clear share the operation slot; public-key import is at most 384 decoded text bytes in the 768-byte request and uses canonical Ed25519/P-256 validation. Three stable key indices may be sparse; never interpret response-array position as an index. No stored key blob, verifier or password is exported.
|
||
|
||
### Network
|
||
|
||
Wi-Fi config/runtime is one zero-wait consistent projection; mDNS is a separate projection, not cross-domain atomic authorization. Four stable profiles carry enabled/priority/security/SSID/password-configured metadata. `mixed` means WPA2-or-stronger, not open. `announced` is expected STA announcement, not verified DNS.
|
||
|
||
SSID wire values are reversible **bytes**, maximum 32: printable ASCII, standard single-character JSON escapes and `\u00HH`, with no raw non-ASCII, non-byte Unicode or surrogates. NUL/non-UTF-8 round-trip. UI text is UTF-8-encoded before byte serialization; exact reversible text or literal hex preserves existing bytes and BOM, with no silent replacement/truncation.
|
||
|
||
Flat operations select one domain/target:
|
||
|
||
- `wifi-patch` + Wi-Fi generation: optional boot policy, AP policy/channel/SSID/password/clear; `profile-patch` additionally selects stable profile 0–3 and optional enabled/priority/security.
|
||
- `wifi-save|wifi-load` require Wi-Fi generation. `start|stop|reconnect|next-profile` use canonical connection controls, not explicit selected-profile connection.
|
||
- `mdns-set` requires its generation and suffix; `mdns-save|mdns-load|mdns-defaults` require its generation. Suffix is 1–55 lowercase ASCII letters/digits/hyphens, no leading/trailing hyphen, producing `sak-<suffix>`.
|
||
|
||
Omitted fields preserve current bytes under the Wi-Fi mutex. Password Keep omits, Replace is 8–63 printable ASCII bytes, disabled-STA Clear is explicit; replacement and clear cannot coexist. AP clear is always denied, even while off. No saved PSK or length is returned. Wi-Fi compare/merge/whole-candidate validation and required queue admission precede RAM publication. Save stabilizes selected bytes; Load reads stored configuration only and cannot generate fallback secrets. No browser Wi-Fi reset/default-secret/export route exists.
|
||
|
||
Edits require explicit Save. Disabled-profile-only edits do not restart active radio; enabled-policy/AP changes follow canonical asynchronous restart. Start/Stop change RAM enabled-at-boot; Reconnect/Next are no-ops while stopped. Next follows enabled priority order with wrap; editor selection is not connection selection. mDNS independently owns generation/persistence; the Wi-Fi manager owns radio/reannouncement. mDNS RAM change followed by queue failure is reported without rollback; its Load may choose deterministic MAC-derived defaults.
|
||
|
||
Network result fields are `id/action/state/error`: `pending`, `accepted`, `ok`, `failed`, `cancelled`, `stale`, `invalid`, `loaded_defaults`, `applied_not_queued`, or login-isolated `idle`. `accepted` means RAM/owner-queue admission, **not** association/DHCP/DNS completion. `ok` denotes explicit Save. Loss can precede response; recover via STA/AP and inspect before retrying. UART0 administers recovery; native USB provides network-independent UART1, not an admin console.
|
||
|
||
### Broker and contextual controls
|
||
|
||
Broker management atomically copies eight compact client rows, writer and lease generation under one zero-wait mutex acquisition, without reading UART payloads or consuming events. Conditional assignment compares the selected nonzero target ID and lease generation under the same force-writer lock before any effects. Stale/absent targets conflict; same-current-target assignment is a no-op only after validation. Accepted serial TX is not recalled by transfer.
|
||
|
||
IDs have three slot bits and 29 generation bits; exhausted client slots retire until reboot rather than wrap. The separate 32-bit lease version saturates at UINT32_MAX, survives counter clear and advances on grant/release/revoke before advisory event delivery. Forced transfer can advance twice; it is not a count. Saturation blocks typed assignment but leaves ordinary request/release/disconnect and canonical recovery force available. Reboot invalidates old browser sessions.
|
||
|
||
Serial/Wi-Fi/Broker quick views reuse one nonmodal settings host and existing controllers/drafts, not parallel editors. Hover/focus/click/tap never mutate. Network quick mode excludes password controls. Full-page drafts are protected; promotion preserves controller/selection, departure fences reads/timers without cancelling admitted work. Dismissal restores the prior terminal without stealing unrelated focus; explicit Escape/Close returns focus to its trigger.
|
||
|
||
Broker contextual reads use one timer five seconds after successful completion and a five-second whole-read deadline, stopping on errors/uncertainty/departure. Explicit target/lease selection is not rebased by refresh. Sticky stale/absence latches require deliberate reselection even if later snapshots match again. Confirmation captures exact versions before session revalidation; guarded aria-disabled controls preserve focus during updates.
|
||
|
||
## Service lifecycle and identity rotation
|
||
|
||
Service generations are saturated, distinct from identity and session generations, and advance on admitted canonical lifecycle attempts including failures; counter clear/reinit cannot revive stale versions. Public zero-wait metadata authorizes nothing. Conditional controls compare under the canonical owner reservation; no snapshot-check/unconditional-mutate gap is allowed. Reboot invalidates originating logins and boot-local reservation IDs.
|
||
|
||
### HTTPS and reboot
|
||
|
||
GET `lifecycle` has seven fields: `generation`, `running`, `transitioning`, `controllable`, `identity_generation`, `fingerprint`, `rotatable`. Stored HTTPS fingerprint is 64 lowercase hex SHA-256 digits. Stop/restart/reboot require exactly action + service generation; rotate also requires identity generation. Unknown/duplicate/escaped/coerced fields and zero/saturated versions reject. Unavailable identity yields generation zero/empty fingerprint/not rotatable without removing ordinary service controls.
|
||
|
||
Self-cutting HTTP actions use **successful synchronous send return → one nonreused-ID HTTPD callback → existing dispatcher**. Send return is not browser receipt. The callback never waits or performs lifecycle work, and captures no request/fd/reusable operation pointer. A two-second ACK admission deadline and post-validation 30-second dequeue deadline are not execution bounds. Accepted-but-lost HTTPD work retains one reservation until callback arrival or successful HTTPD destruction; failed destruction cannot release it or accumulate more callbacks. The original login/current admin is revalidated before owner admission.
|
||
|
||
`web_server_stop_current()`/`web_server_restart_current()` compare and reserve under the server mutex. Restart retains transition ownership through stop/start; failed stop skips start, failed cleanup retains handles and canonical recovery, and reinit preserves the error. `web_server_reboot_current()` reserves the HTTPS generation then uses canonical `esp_restart()` outside locks, not an HTTPD stop wait or self-console cleanup.
|
||
|
||
`web_server_replace_identity(service_generation, identity_generation, reset, &committed)` reserves **service before identity**, before any crypto/storage. Both nonzero generations mean conditional healthy-running rotation; both zero retain CLI semantics; conditional Reset is invalid. Canonical CLI/browser-shell rotation and direct security rotate/reset share nonreused task-owner-bound identity reservation tokens. Only the owner can replace once/release; exhaustion fails closed until reboot. Crypto/NVS run outside service/security mutexes and spinlocks; commit precedes live publication and old-key wipe. Reservation remains held while reserved start copies the committed pair.
|
||
|
||
HTTPS ordering is **commit → stop → restart**. Precommit generation/RNG/storage failure leaves identity, HTTPD and logins unchanged (service version may advance after admission). Postcommit lifecycle failure never rolls back identity; failed stop can leave old served and new stored fingerprints different. Running replacement restarts HTTPS; canonical stopped rotation stays stopped, while CLI TLS-only reset can recover unavailable identity and starts a stopped service. Browser has no Reset/recovery/export action.
|
||
|
||
Save drafts; rotation/restart invalidates all web logins and closes both browser routes. Inspect `web certificate info` through trusted UART0, verify fingerprint before renewing trust, then reload/sign in freshly. Accepting a warning alone is not trusted verification. Use canonical UART0/admin SSH `web stop` / `web start` for retained-server recovery. Network/SSH/USB are not stopped by HTTPS-only operations; whole-device reboot interrupts all transports and loses unsaved RAM.
|
||
|
||
### SSH
|
||
|
||
GET `ssh` supplies service/session state plus identity generation, fixed P-256 algorithm, unpadded OpenSSH `SHA256:` base64 fingerprint and rotatable flag. Service actions use exactly `action`, `generation`, `target`; rotate adds `identity_generation` and requires target zero. Start/stop and exact-session disconnect use published state, saturated service generation and the command mutex; exhausted SSH session slots retire rather than wrap. Disconnect success is an owner close request, not completed teardown. HTTPD never calls wolfSSH or waits for the SSH task.
|
||
|
||
`ssh_transport_replace_identity()` compares/reserves **service then identity before stop or storage**, holding the existing command mutex across **stop → generate/commit/publish → conditional restart**. Canonical UART0/deferred admin-SSH rotation/reset and direct security mutations share the task-bound nonreused identity reservation. Crypto/NVS run outside security locks/spinlocks. Both nonzero versions select conditional rotation (also while stopped); both zero preserve canonical semantics, with no conditional Reset.
|
||
|
||
Failed stop/timeout skips mutation and never starts again; pending owner work is not cancelled. Persistence failure after successful stop may already have disconnected clients and attempts old-identity restart if previously running. Committed replacement remains committed if restart fails. Stopped rotation stays stopped; canonical reset can recover unavailable material/start stopped SSH. Only the SSH owner frees runtime context after all slots retire, before clearing cleanup admission; start rejects orphan handles. wolfSSH copies DER into its context; caller/candidate/superseded private bytes are wiped.
|
||
|
||
SSH changes leave invoking HTTPS available, so they use the ordinary ID-dispatcher/result path, not HTTPS's self-cutting ACK gate. UI confirms both versions and all-SSH/session scope, retains stale selection and manual 15-second/no-replay flow. A failed result may represent partial effects. Verify new trust through UART0 `ssh host-key info` before updating known_hosts; no HTTPS relogin is inherently required. Browser Reset/recovery/key export and user authorized-key changes are separate, excluded from host rotation.
|
||
|
||
## Diagnostics, recovery and scope boundaries
|
||
|
||
See [admission diagnostics](web_admission_diagnostics.md), [ordinary HTTPS idle cleanup](https_idle_cleanup.md), [throughput diagnostics](web_throughput_diagnostics.md) and [legacy storage compatibility](legacy_credential_removal.md). Broker read means transport handoff, not peer receipt; capture non-consuming counters before disconnect. TLS `-0x004C` is generic NET_RECV_FAILED, not evidence of OOM. Resource minima and counter observations require attribution, not inferred causes.
|
||
|
||
Phase 8D.15's dedicated typed network-diagnostics UI/API was removed: diagnostics remain shell-based, subject to frontend policy. The unimplemented 8D.19 ordinary browser-session/native-USB control expansion was removed; existing SSH controls remain. No full shell parity, browser identity recovery/reset/export, encryption, secure boot or OTA is implied by acceptance. UART0 is the administrative recovery authority; native USB is binary-transparent, network-independent UART1 access. Neither permits bypassing the broker's single writer or recalling already-admitted work.
|