Discovers ONVIF cameras (Hikvision/Annke) over WS-Discovery, keeps a server-side registry and an encrypted credential store, and serves info, snapshot and credentials routes for each camera. The home page is now a Server Component that lists known cameras from the registry on load, without a scan (vrek iss-a0hz0py). The snapshot refresh rate lives in the URL (?refresh=), and a scan refreshes the server-rendered list. Route input is validated with zod (iss-rjqy3hy), and /api/discover no longer returns raw error messages (iss-dbwgww8). Adds @tanstack/react-query and zod as dependencies, with a QueryClient provider in the root layout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
101 lines
4.0 KiB
JavaScript
101 lines
4.0 KiB
JavaScript
// WS-Discovery diagnostic: sends several Probe variants and listens both on the
|
|
// sending socket (unicast replies) and on 239.255.255.250:3702 (multicast replies).
|
|
// Usage: node scripts/probe-debug.mjs [interfaceName] [timeoutMs] [targetIp]
|
|
// With targetIp, probes are sent by unicast to targetIp:3702 instead of multicast.
|
|
import dgram from "node:dgram";
|
|
import os from "node:os";
|
|
import { randomUUID } from "node:crypto";
|
|
|
|
const ifaceName = process.argv[2];
|
|
const timeoutMs = Number(process.argv[3]) || 5000;
|
|
const target = process.argv[4];
|
|
const GROUP = "239.255.255.250";
|
|
const PORT = 3702;
|
|
|
|
const ifaces = os.networkInterfaces();
|
|
const candidates = Object.entries(ifaces).flatMap(([name, addrs]) =>
|
|
(addrs ?? [])
|
|
.filter((a) => a.family === "IPv4" && !a.internal)
|
|
.map((a) => ({ name, address: a.address })),
|
|
);
|
|
console.log("IPv4 interfaces:", candidates.map((c) => `${c.name}=${c.address}`).join(", "));
|
|
const iface = ifaceName ? candidates.find((c) => c.name === ifaceName) : candidates[0];
|
|
if (!iface) {
|
|
console.error(`No IPv4 interface ${ifaceName ?? ""} found`);
|
|
process.exit(1);
|
|
}
|
|
console.log(`Using ${iface.name} (${iface.address}), timeout ${timeoutMs}ms\n`);
|
|
|
|
const probe = (types) => {
|
|
const id = `urn:uuid:${randomUUID()}`;
|
|
return {
|
|
id,
|
|
xml:
|
|
`<?xml version="1.0" encoding="UTF-8"?>` +
|
|
`<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" ` +
|
|
`xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing" ` +
|
|
`xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery" ` +
|
|
`xmlns:dn="http://www.onvif.org/ver10/network/wsdl" ` +
|
|
`xmlns:tds="http://www.onvif.org/ver10/device/wsdl">` +
|
|
`<s:Header><a:MessageID>${id}</a:MessageID>` +
|
|
`<a:To s:mustUnderstand="1">urn:schemas-xmlsoap-org:ws:2005:04:discovery</a:To>` +
|
|
`<a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</a:Action>` +
|
|
`</s:Header><s:Body><d:Probe>${types ? `<d:Types>${types}</d:Types>` : ""}</d:Probe></s:Body>` +
|
|
`</s:Envelope>`,
|
|
};
|
|
};
|
|
const variants = {
|
|
"NetworkVideoTransmitter": probe("dn:NetworkVideoTransmitter"),
|
|
"tds:Device": probe("tds:Device"),
|
|
"no Types": probe(null),
|
|
};
|
|
|
|
const seen = new Set();
|
|
function report(via) {
|
|
return (msg, rinfo) => {
|
|
const text = msg.toString();
|
|
if (/<[^>]*:?Probe>/.test(text) && !/ProbeMatch/.test(text)) return; // our own / others' probes
|
|
const relates = text.match(/RelatesTo[^>]*>([^<]+)</)?.[1];
|
|
const variant = Object.entries(variants).find(([, v]) => v.id === relates)?.[0] ?? "unknown";
|
|
const xaddrs = text.match(/XAddrs[^>]*>([^<]+)</)?.[1];
|
|
const key = `${via}|${rinfo.address}|${variant}`;
|
|
if (seen.has(key)) return;
|
|
seen.add(key);
|
|
console.log(`REPLY via ${via} from ${rinfo.address}:${rinfo.port} to probe "${variant}"`);
|
|
console.log(` XAddrs: ${xaddrs ?? "(none)"}`);
|
|
if (!xaddrs) console.log(` raw: ${text.slice(0, 500)}`);
|
|
};
|
|
}
|
|
|
|
const unicast = dgram.createSocket({ type: "udp4", reuseAddr: true });
|
|
unicast.on("message", report("unicast socket"));
|
|
unicast.on("error", (e) => console.error("unicast socket error:", e));
|
|
|
|
const mcast = dgram.createSocket({ type: "udp4", reuseAddr: true });
|
|
mcast.on("message", report("multicast 3702"));
|
|
mcast.on("error", (e) => console.error(`multicast listener error (port ${PORT} busy?):`, e.message));
|
|
mcast.bind(PORT, () => {
|
|
try {
|
|
mcast.addMembership(GROUP, iface.address);
|
|
} catch (e) {
|
|
console.error("addMembership failed:", e.message);
|
|
}
|
|
});
|
|
|
|
unicast.bind(0, iface.address, () => {
|
|
unicast.setMulticastInterface(iface.address);
|
|
unicast.setMulticastTTL(4);
|
|
console.log(`Sending from ${iface.address}:${unicast.address().port}`);
|
|
for (const [name, { xml }] of Object.entries(variants)) {
|
|
unicast.send(xml, PORT, target ?? GROUP, (err) =>
|
|
console.log(`sent "${name}" to ${target ?? GROUP}${err ? ` ERROR ${err.message}` : ""}`),
|
|
);
|
|
}
|
|
});
|
|
|
|
setTimeout(() => {
|
|
console.log(`\nDone. ${seen.size} reply(ies).`);
|
|
unicast.close();
|
|
mcast.close();
|
|
}, timeoutMs);
|