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
+30
View File
@@ -0,0 +1,30 @@
import { networkInterfaces } from "node:os";
// Figure out which local IP a device should call back on. For input-push, the
// device needs OUR address on ITS subnet — pick the local IPv4 interface whose
// network contains the device's IP. Override with BACKEND_HOST_IP if the
// auto-pick is wrong (e.g. multi-homed host). See wiki/concepts/device-input-flow.md.
export function backendIpForDevice(deviceHost: string): string | null {
if (process.env.BACKEND_HOST_IP) return process.env.BACKEND_HOST_IP;
const ip = deviceHost.split(".").map(Number);
if (ip.length !== 4 || ip.some((o) => Number.isNaN(o))) return null;
for (const ifaces of Object.values(networkInterfaces())) {
for (const i of ifaces ?? []) {
if (i.family !== "IPv4" || i.internal) continue;
const addr = i.address.split(".").map(Number);
const mask = i.netmask.split(".").map(Number);
if (addr.length !== 4 || mask.length !== 4) continue;
const sameNet = ip.every((o, k) => (o & mask[k]!) === (addr[k]! & mask[k]!));
if (sameNet) return i.address;
}
}
return null;
}
/** Backend port the device should call (the server's listen port). */
export function backendPort(): number {
return Number(process.env.PORT ?? 3000);
}