Unify browser connection controls (Connect / Disconnect)
This commit is contained in:
@@ -102,7 +102,7 @@ HTTP Basic authentication uses `user_database`. Before administrator bootstrap,
|
|||||||
|
|
||||||
The boot-local Basic-authentication cache has four RAM entries and a five-minute sliding lifetime. It stores a keyed digest of the complete `Authorization` header rather than the raw header, and every hit revalidates principal currentness. Its current lack of locking relies on the single-HTTPD-owner execution model.
|
The boot-local Basic-authentication cache has four RAM entries and a five-minute sliding lifetime. It stores a keyed digest of the complete `Authorization` header rather than the raw header, and every hit revalidates principal currentness. Its current lack of locking relies on the single-HTTPD-owner execution model.
|
||||||
|
|
||||||
A WebSocket connection requires a one-time, principal-bound ticket with a maximum 30-second lifetime. Only four tickets can be outstanding; minting another evicts the live entry with the earliest expiry. Ticket issuance and upgrade also validate a supplied `Origin` against `https://<Host>`; absence of `Origin` is accepted for non-browser clients. Tickets are stored as digests, consumed before currentness validation, and are never persisted. An admitted session starts the serial service if necessary, creates a broker client, and opportunistically requests writer ownership. The web transport has two fixed session slots. Binary frames carry serial data; small text messages request or release writer ownership. HTTPD owns socket send/close operations, while the web transport task mediates broker work through bounded scheduling. Browser Disconnect closes the WebSocket and pauses automatic reconnect until the user explicitly selects Reconnect.
|
A WebSocket connection requires a one-time, principal-bound ticket with a maximum 30-second lifetime. Only four tickets can be outstanding; minting another evicts the live entry with the earliest expiry. Ticket issuance and upgrade also validate a supplied `Origin` against `https://<Host>`; absence of `Origin` is accepted for non-browser clients. Tickets are stored as digests, consumed before currentness validation, and are never persisted. An admitted session starts the serial service if necessary, creates a broker client, and opportunistically requests writer ownership. The web transport has two fixed session slots. Binary frames carry serial data; small text messages request or release writer ownership. HTTPD owns socket send/close operations, while the web transport task mediates broker work through bounded scheduling. The browser's combined Connect/Disconnect control closes the WebSocket and pauses automatic reconnect; after a user-paused disconnect it changes to Connect, which resumes connection attempts.
|
||||||
|
|
||||||
Web serial initialization is failure-isolated from the base HTTPS service: if the transport cannot initialize, `web_server_init()` can still succeed and serve authenticated non-WebSocket routes.
|
Web serial initialization is failure-isolated from the base HTTPS service: if the transport cannot initialize, `web_server_init()` can still succeed and serve authenticated non-WebSocket routes.
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ This is a semantic map, not a complete file inventory. Start here, then read the
|
|||||||
- Dependencies: user database, secure random, broker, successful Wi-Fi manager initialization at boot, mbedTLS/HTTPS server; actual network reachability is an operational prerequisite, not an initializer invariant
|
- Dependencies: user database, secure random, broker, successful Wi-Fi manager initialization at boot, mbedTLS/HTTPS server; actual network reachability is an operational prerequisite, not an initializer invariant
|
||||||
- Flow: `browser -> HTTPS Basic auth -> ticket -> WebSocket -> web transport -> broker`
|
- Flow: `browser -> HTTPS Basic auth -> ticket -> WebSocket -> web transport -> broker`
|
||||||
- Ownership: HTTPD owns socket send/close work; transport task owns broker mediation; two fixed WebSocket slots and four outstanding tickets.
|
- Ownership: HTTPD owns socket send/close work; transport task owns broker mediation; two fixed WebSocket slots and four outstanding tickets.
|
||||||
- Security constraints: Basic-auth cache hits still revalidate principal currentness; browser Disconnect closes the WebSocket and pauses automatic reconnect until Reconnect is selected. Changes to the authored inline loader must update its hard-coded CSP hash in the same change.
|
- Security constraints: Basic-auth cache hits still revalidate principal currentness; the browser's combined Connect/Disconnect control closes the WebSocket and pauses automatic reconnect until Connect is selected. Changes to the authored inline loader must update its hard-coded CSP hash in the same change.
|
||||||
- Asset constraint: `web_assets_data.c` is checked-in generated input to the build; do not hand-edit or regenerate casually.
|
- Asset constraint: `web_assets_data.c` is checked-in generated input to the build; do not hand-edit or regenerate casually.
|
||||||
|
|
||||||
## SSH
|
## SSH
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ When the Wi-Fi station receives an IPv4 address, the Wi-Fi manager announces `sa
|
|||||||
| `web certificate rotate --force` | Replace the HTTPS certificate and private key. |
|
| `web certificate rotate --force` | Replace the HTTPS certificate and private key. |
|
||||||
| `web reset --force` | Explicitly replace missing, incompatible, or damaged legacy credentials and web material. |
|
| `web reset --force` | Explicitly replace missing, incompatible, or damaged legacy credentials and web material. |
|
||||||
|
|
||||||
HTTPS listens on port 443 only. Authenticate with any current user-database username/password; both `user` and `admin` roles receive the existing status and browser-terminal interface. The device serves vendored xterm.js without Internet access. Browser sessions use one-time account-bound tickets, binary WebSocket frames, and the broker's one-writer rule. Selecting **Disconnect** closes the current WebSocket and pauses automatic reconnect; select **Reconnect** to resume connection attempts. Account mutations revoke only that account's tickets and sessions.
|
HTTPS listens on port 443 only. Authenticate with any current user-database username/password; both `user` and `admin` roles receive the existing status and browser-terminal interface. The device serves vendored xterm.js without Internet access. Browser sessions use one-time account-bound tickets, binary WebSocket frames, and the broker's one-writer rule. The combined **Connect**/**Disconnect** control closes the current WebSocket and pauses automatic reconnect when active; after a user-paused disconnect, it changes to **Connect** to resume connection attempts. Account mutations revoke only that account's tickets and sessions.
|
||||||
|
|
||||||
## SSH serial transport
|
## SSH serial transport
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -141,7 +141,7 @@ Implemented and hardware-validated:
|
|||||||
- Viewport-constrained terminal fitting without recursive page growth.
|
- Viewport-constrained terminal fitting without recursive page growth.
|
||||||
- Validation with multiple clients, ANSI colors, advanced escape sequences, and full-screen terminal software.
|
- Validation with multiple clients, ANSI colors, advanced escape sequences, and full-screen terminal software.
|
||||||
|
|
||||||
Post-validation enhancement implemented; browser regression validation pending: Disconnect explicitly closes the terminal WebSocket and pauses automatic reconnect until Reconnect is selected.
|
Post-validation enhancement implemented; browser regression validation pending: the combined Connect/Disconnect control explicitly closes the terminal WebSocket and pauses automatic reconnect until Connect is selected.
|
||||||
|
|
||||||
### Phase 6 — Authenticated SSH serial transport
|
### Phase 6 — Authenticated SSH serial transport
|
||||||
|
|
||||||
|
|||||||
+14
-11
@@ -119,8 +119,7 @@ static const char s_index_html[] =
|
|||||||
"<div class=\"button-row\">\n"
|
"<div class=\"button-row\">\n"
|
||||||
"<button id=\"request-control\" class=\"button primary\" type=\"button\" disabled>Request control</button>\n"
|
"<button id=\"request-control\" class=\"button primary\" type=\"button\" disabled>Request control</button>\n"
|
||||||
"<button id=\"release-control\" class=\"button danger\" type=\"button\" disabled>Release control</button>\n"
|
"<button id=\"release-control\" class=\"button danger\" type=\"button\" disabled>Release control</button>\n"
|
||||||
"<button id=\"disconnect\" class=\"button danger\" type=\"button\">Disconnect</button>\n"
|
"<button id=\"connection-toggle\" class=\"button danger\" type=\"button\">Disconnect</button>\n"
|
||||||
"<button id=\"reconnect\" class=\"button\" type=\"button\">Reconnect</button>\n"
|
|
||||||
"</div>\n"
|
"</div>\n"
|
||||||
"<p id=\"input-state\" class=\"input-state\" data-enabled=\"false\" aria-live=\"polite\">"
|
"<p id=\"input-state\" class=\"input-state\" data-enabled=\"false\" aria-live=\"polite\">"
|
||||||
"Observer mode — terminal input is disabled.</p>\n"
|
"Observer mode — terminal input is disabled.</p>\n"
|
||||||
@@ -152,8 +151,7 @@ static const char s_app_js[] =
|
|||||||
"const connectionDetail = element('connection-detail');\n"
|
"const connectionDetail = element('connection-detail');\n"
|
||||||
"const requestControl = element('request-control');\n"
|
"const requestControl = element('request-control');\n"
|
||||||
"const releaseControl = element('release-control');\n"
|
"const releaseControl = element('release-control');\n"
|
||||||
"const disconnectButton = element('disconnect');\n"
|
"const connectionToggle = element('connection-toggle');\n"
|
||||||
"const reconnectButton = element('reconnect');\n"
|
|
||||||
"const terminalHost = element('terminal');\n"
|
"const terminalHost = element('terminal');\n"
|
||||||
"const terminal = new Terminal({\n"
|
"const terminal = new Terminal({\n"
|
||||||
" allowProposedApi: false, convertEol: false, cursorBlink: true, disableStdin: true,\n"
|
" allowProposedApi: false, convertEol: false, cursorBlink: true, disableStdin: true,\n"
|
||||||
@@ -196,7 +194,10 @@ static const char s_app_js[] =
|
|||||||
" terminal.options.disableStdin = !writer;\n"
|
" terminal.options.disableStdin = !writer;\n"
|
||||||
" requestControl.disabled = !socketOpen() || writer;\n"
|
" requestControl.disabled = !socketOpen() || writer;\n"
|
||||||
" releaseControl.disabled = !socketOpen() || !writer;\n"
|
" releaseControl.disabled = !socketOpen() || !writer;\n"
|
||||||
" disconnectButton.disabled = unloading || (!reconnectEnabled && socket === null && ticketAbort === null);\n"
|
" const connectionActive = reconnectEnabled || socket !== null || ticketAbort !== null || reconnectTimer !== null;\n"
|
||||||
|
" connectionToggle.disabled = unloading;\n"
|
||||||
|
" connectionToggle.textContent = connectionActive ? 'Disconnect' : 'Connect';\n"
|
||||||
|
" connectionToggle.classList.toggle('danger', connectionActive);\n"
|
||||||
" inputState.dataset.enabled = writer ? 'true' : 'false';\n"
|
" inputState.dataset.enabled = writer ? 'true' : 'false';\n"
|
||||||
" inputState.textContent = writer\n"
|
" inputState.textContent = writer\n"
|
||||||
" ? 'Writer mode — terminal input is enabled.'\n"
|
" ? 'Writer mode — terminal input is enabled.'\n"
|
||||||
@@ -335,7 +336,14 @@ static const char s_app_js[] =
|
|||||||
"releaseControl.addEventListener('click', () => {\n"
|
"releaseControl.addEventListener('click', () => {\n"
|
||||||
" if (role === 'writer' && socketOpen()) socket.send('release-writer');\n"
|
" if (role === 'writer' && socketOpen()) socket.send('release-writer');\n"
|
||||||
"});\n"
|
"});\n"
|
||||||
"disconnectButton.addEventListener('click', () => {\n"
|
"connectionToggle.addEventListener('click', () => {\n"
|
||||||
|
" const connectionActive = reconnectEnabled || socket !== null || ticketAbort !== null || reconnectTimer !== null;\n"
|
||||||
|
" if (!connectionActive) {\n"
|
||||||
|
" reconnectEnabled = true;\n"
|
||||||
|
" reconnectDelay = 1000;\n"
|
||||||
|
" connect();\n"
|
||||||
|
" return;\n"
|
||||||
|
" }\n"
|
||||||
" reconnectEnabled = false;\n"
|
" reconnectEnabled = false;\n"
|
||||||
" ++connectionGeneration;\n"
|
" ++connectionGeneration;\n"
|
||||||
" clearReconnectTimer();\n"
|
" clearReconnectTimer();\n"
|
||||||
@@ -346,11 +354,6 @@ static const char s_app_js[] =
|
|||||||
" setRole('observer');\n"
|
" setRole('observer');\n"
|
||||||
" setConnection('Disconnected', 'warn', 'Disconnected by user. Automatic reconnect is paused.');\n"
|
" setConnection('Disconnected', 'warn', 'Disconnected by user. Automatic reconnect is paused.');\n"
|
||||||
"});\n"
|
"});\n"
|
||||||
"reconnectButton.addEventListener('click', () => {\n"
|
|
||||||
" reconnectEnabled = true;\n"
|
|
||||||
" reconnectDelay = 1000;\n"
|
|
||||||
" connect();\n"
|
|
||||||
"});\n"
|
|
||||||
"const fitTerminal = () => {\n"
|
"const fitTerminal = () => {\n"
|
||||||
" fitFrame = 0;\n"
|
" fitFrame = 0;\n"
|
||||||
" const bounds = terminalHost.getBoundingClientRect();\n"
|
" const bounds = terminalHost.getBoundingClientRect();\n"
|
||||||
|
|||||||
Reference in New Issue
Block a user