Dingtian input push: HTTP Digest auth + auto-config on assign

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.
This commit is contained in:
2026-06-14 16:39:08 +02:00
parent 23919164ee
commit 3294f188dd
9 changed files with 403 additions and 77 deletions
+27
View File
@@ -99,6 +99,33 @@ export function hasPreconditions(
return typeof (device as Partial<PreconditionDevice>).checkPreconditions === "function";
}
// --- Push configuration (device → backend) -------------------------------
// Optional capability: a device that can be told to HTTP-push its input/button
// events to our backend (vs. the host polling it). The backend configures the
// device with where to call and a shared-secret token embedded in the path.
// The Dingtian board implements this via its "Input Link URL" feature.
// See wiki/concepts/device-input-flow.md.
export interface PushConfigurableDevice {
configureInputPush(opts: PushConfig): Promise<void>;
}
export interface PushConfig {
/** Backend host the device should call (our IP on the device's subnet). */
readonly host: string;
readonly port: number;
/** Path prefix the device appends `/<input>/<on|off>` to,
* e.g. `/api/devices/dingtian/<deviceId>/input`. */
readonly pathBase: string;
/** HTTP Digest credentials the device authenticates the push with. */
readonly auth: { user: string; password: string };
}
export function hasPushConfig(
device: Device,
): device is Device & PushConfigurableDevice {
return typeof (device as Partial<PushConfigurableDevice>).configureInputPush === "function";
}
// --- Readers (RF / optical; TCP-IP or Wiegand) ---------------------------
export interface ReaderDevice extends Device {
/** Emits when a credential is read (card number, plate, QR payload, …). */