3294f188dd
Secure the device→backend input push, and configure it automatically when the
admin assigns the device (no manual URL/secret entry).
Auth — HTTP Digest (chosen by hardware testing: the device can't push to a
self-signed HTTPS backend, but does Digest correctly; a URL token is sniffable/
logged):
- digest-auth.ts: MD5 qop=auth challenge/verify, single-use nonces (replay
resistance). Password never crosses the wire.
- push route: Digest + source-IP allowlist; per-device pushUser/pushPassword from
lane_devices. Still not behind the SPA cookie/CSRF auth (machine call). The
signed event log remains the real anti-fraud guarantee.
Auto-config on assign:
- setup assign: for push-capable devices, generate Digest creds, call
configureInputPush to write them + the push URLs to the device, store the creds
(password not echoed back). net.ts derives the backend IP on the device's
subnet (BACKEND_HOST_IP override).
- driver configureInputPush sets auth=2 + creds; PushConfig carries the creds.
- removed the earlier URL-token approach.
Two hard-won device-write bugs fixed in the driver:
- configApi now sets an explicit Content-Length — the device silently ignores
chunked request bodies (Node's default without Content-Length), so every config
write looked successful ({"status":0}) but did nothing. This was the root cause
of the session's "writes don't apply" mystery.
- #writeConfig polls until the change is verified, retrying (the device reboots on
apply; back-to-back writes were lost). The `pass` field caps at 31 chars, so the
generated password is 24 hex chars.
Verified on hardware: assign auto-configures the device; all 4 inputs then push
with Digest auth, zero failures. wiki/device-input-flow updated.
88 lines
4.6 KiB
Markdown
88 lines
4.6 KiB
Markdown
---
|
|
type: concept
|
|
tags: [parking, architecture, devices, entry-flow]
|
|
sources: []
|
|
updated: 2026-06-15
|
|
---
|
|
|
|
# Device Input Flow (button → backend → relay)
|
|
|
|
How a physical button press drives the entry lane. The **backend is the source of truth**: the
|
|
device only *reports* the press; the host decides and commands the relay. This is the host-in-the-
|
|
loop flow the [[dingtian-relay]] makes possible (and the [[uhppote-controller]] could not).
|
|
|
|
## The path (no polling)
|
|
|
|
```
|
|
car arrives → driver presses button (input I_N, dry contact to GND)
|
|
→ device HTTP-pushes GET …/api/devices/dingtian/<deviceId>/input/<N>/on
|
|
→ backend: emit internal device event (device-events bus)
|
|
→ backend entry flow: create + sign an entry event, print the ticket
|
|
→ backend: pulseOpen(N) over UDP → barrier opens
|
|
→ (on release) device pushes …/input/<N>/off
|
|
```
|
|
|
|
- **Push, not poll.** The device's `input_link_url` feature is configured (by the driver's
|
|
`configureInputPush()`) to call the backend on each input edge — see [[dingtian-relay]]. The
|
|
driver's poll path remains only as a dev/fallback aid.
|
|
- **Per-input path** carries the input number in the URL (`…/input/3/on`), so routing needs no
|
|
body parsing. Both edges (`on`/`off`) are sent.
|
|
- **Internal event bus** (`device-events.ts`, a Node `EventEmitter`) decouples the HTTP/transport
|
|
layer from business logic — drivers/pushes emit; the entry flow subscribes. Keeps the app
|
|
[[device-adapter-pattern|device-agnostic]].
|
|
|
|
## Trust model (important — flat network, no VLAN)
|
|
|
|
The relay-control direction (host → device) is **unauthenticated UDP**, and the site is a **flat
|
|
network with no VLAN** ([[network-isolation]] is not yet enforceable here). So we do **not** trust
|
|
the device or the network. Instead:
|
|
|
|
- **Every barrier open is a host decision, recorded as a signed event BEFORE the relay fires**
|
|
([[append-only-event-chain]]). If anyone opens the relay out-of-band (which the flat network
|
|
allows), there is **no matching signed event → a detectable anomaly**. The anti-fraud guarantee
|
|
is the **signed log**, not device/network auth.
|
|
- The inbound push endpoint is intentionally **not behind the SPA's cookie/CSRF auth** (it's a
|
|
machine call from the device). It is guarded by **HTTP Digest auth** + a **source-IP allowlist**
|
|
(defence-in-depth), but these are *not* the security boundary.
|
|
- This sharpens under the [[autonomous-direction|unmanned]] roadmap: with no operator, tamper
|
|
detection via the signed log matters more than perimeter auth.
|
|
|
|
## Push authentication — Digest (decided by hardware testing)
|
|
|
|
The secret must not be in the URL (sniffable, logged) and the password must not cross the wire in
|
|
the clear. We **empirically tested the device** to pick the strongest achievable option:
|
|
|
|
| Option | Device result |
|
|
| --- | --- |
|
|
| HTTPS (self-signed) | ❌ device won't push to a self-signed cert |
|
|
| **Digest auth** (`auth=2`) | ✅ **works** — full 401-nonce challenge/response |
|
|
| Basic auth | ✅ works (but password base64 on the wire) |
|
|
| URL token | rejected by design (visible in URL/logs) |
|
|
|
|
→ **HTTP Digest** (MD5, qop=auth). The password is never sent (only a nonce-keyed hash); nonces
|
|
are **single-use** (replay resistance). Per-device credentials (`pushUser`/`pushPassword`) are
|
|
generated by the backend on **device assign**, written to the device's `input_link_url` config,
|
|
and stored in `lane_devices` — the admin never types a URL or secret. HTTPS would be stronger but
|
|
the device can't do it here; Digest + the signed log is the practical answer on a flat network.
|
|
See `apps/server/src/digest-auth.ts`.
|
|
|
|
## Dingtian config-write gotchas (cost a lot of debugging)
|
|
|
|
Writing the device's config API (`/api/v2/config_set.cgi`) has two non-obvious traps — both now
|
|
handled in the driver:
|
|
|
|
1. **Content-Length is mandatory.** The device's embedded HTTP server does **not** accept chunked
|
|
request bodies. Node uses chunked encoding when `Content-Length` is absent, so the device
|
|
silently ignores the body and returns `{"status":0}` anyway — the write looks successful but
|
|
nothing changes. Always set `Content-Length`.
|
|
2. **The `pass` field caps at 31 chars** (longer is silently truncated → Digest mismatch). The
|
|
generated push password is 24 hex chars (96 bits).
|
|
3. (Also: the device reboots on apply, so the driver writes then **polls until the change is
|
|
verified**, retrying — back-to-back writes onto a rebooting device are lost.)
|
|
|
|
## Status
|
|
|
|
Input push **verified on hardware** with Digest auth (all 4 inputs, real presses authenticated, no
|
|
failures). The entry
|
|
flow itself (signed event + ticket print + `pulseOpen`) is the next build — see [[dingtian-relay]].
|