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.
4.6 KiB
type, tags, sources, updated
| type | tags | sources | updated | ||||
|---|---|---|---|---|---|---|---|
| concept |
|
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_urlfeature is configured (by the driver'sconfigureInputPush()) 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 NodeEventEmitter) decouples the HTTP/transport layer from business logic — drivers/pushes emit; the entry flow subscribes. Keeps the app device-adapter-pattern.
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 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:
- Content-Length is mandatory. The device's embedded HTTP server does not accept chunked
request bodies. Node uses chunked encoding when
Content-Lengthis absent, so the device silently ignores the body and returns{"status":0}anyway — the write looks successful but nothing changes. Always setContent-Length. - The
passfield caps at 31 chars (longer is silently truncated → Digest mismatch). The generated push password is 24 hex chars (96 bits). - (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.