Add optional Fingerbank device fingerprinting to deep-check
Folded into the existing on-demand Deep check button: queries Fingerbank's interrogate API with the device's MAC plus the SSDP SERVER header when deep-check-device.sh finds one, showing the confidence band alongside the result. Runs directly from the API container (no host-level access needed, just an outbound HTTPS call), unlike the SSDP/mDNS steps. Confirmed via direct testing: without DHCP fingerprint data (which we structurally don't have, not being the DHCP server), MAC-only queries often can't get past manufacturer-level confidence -- same info the free OUI lookup already provides. Documented honestly in docs/device-discovery.md rather than overselling it. Still worth having as opt-in enrichment for devices that do expose richer signals. Gated behind optional FINGERBANK_API_KEY -- missing key, API errors, or no match all degrade gracefully without affecting the rest of deep-check's local findings. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,3 +44,10 @@ SNAPSHOT_RETENTION_HOURS=24
|
|||||||
# Dedicated key for SshHostCollector (omv, ripper) — see docs/ssh-collector-key-setup.md.
|
# Dedicated key for SshHostCollector (omv, ripper) — see docs/ssh-collector-key-setup.md.
|
||||||
# In docker-compose this is mounted from ./ssh/monitor_ed25519 (gitignored, not this path).
|
# In docker-compose this is mounted from ./ssh/monitor_ed25519 (gitignored, not this path).
|
||||||
SSH_PRIVATE_KEY_PATH=./ssh/monitor_ed25519
|
SSH_PRIVATE_KEY_PATH=./ssh/monitor_ed25519
|
||||||
|
|
||||||
|
# Optional. Free API key from fingerbank.org — enriches the "Deep check" button's
|
||||||
|
# results with device fingerprinting (MAC + any SSDP signal found). Without a DHCP
|
||||||
|
# fingerprint (which we don't have access to, not being the DHCP server), this often
|
||||||
|
# only reaches manufacturer-level confidence, same as the free OUI lookup — see
|
||||||
|
# docs/device-discovery.md. Omit entirely to skip this enrichment.
|
||||||
|
FINGERBANK_API_KEY=
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export interface AppConfig {
|
|||||||
discoveryFilePath: string;
|
discoveryFilePath: string;
|
||||||
oidc: OidcConfig | undefined;
|
oidc: OidcConfig | undefined;
|
||||||
deepCheck: DeepCheckHostConfig | undefined;
|
deepCheck: DeepCheckHostConfig | undefined;
|
||||||
|
fingerbankApiKey: string | undefined;
|
||||||
hosts: HostsConfig;
|
hosts: HostsConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +137,7 @@ export function loadConfig(hostsConfigPath: string): AppConfig {
|
|||||||
privateKeyPath: sshPrivateKeyPath,
|
privateKeyPath: sshPrivateKeyPath,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
fingerbankApiKey: process.env.FINGERBANK_API_KEY,
|
||||||
hosts,
|
hosts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,6 +205,13 @@ export function getRecentDevices(db: Database.Database, sinceHours = 24): Device
|
|||||||
.all({ cutoff: `-${sinceHours} hours` }) as DeviceRow[];
|
.all({ cutoff: `-${sinceHours} hours` }) as DeviceRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDeviceMac(db: Database.Database, ip: string): string | null {
|
||||||
|
const row = db.prepare(`SELECT mac FROM devices WHERE ip = @ip`).get({ ip }) as
|
||||||
|
| { mac: string }
|
||||||
|
| undefined;
|
||||||
|
return row?.mac ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
export function setDeviceLabel(db: Database.Database, mac: string, label: string): void {
|
export function setDeviceLabel(db: Database.Database, mac: string, label: string): void {
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`INSERT INTO device_labels (mac, label) VALUES (@mac, @label)
|
`INSERT INTO device_labels (mac, label) VALUES (@mac, @label)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface DeepCheckResult {
|
|||||||
reverseDns: string | null;
|
reverseDns: string | null;
|
||||||
ssdp: {
|
ssdp: {
|
||||||
location: string;
|
location: string;
|
||||||
|
server: string | null;
|
||||||
friendlyName: string | null;
|
friendlyName: string | null;
|
||||||
manufacturer: string | null;
|
manufacturer: string | null;
|
||||||
modelName: string | null;
|
modelName: string | null;
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
export interface FingerbankResult {
|
||||||
|
deviceName: string | null;
|
||||||
|
score: number | null;
|
||||||
|
version: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FingerbankSignals {
|
||||||
|
mac: string;
|
||||||
|
// "Upnp string" per Fingerbank's docs -- we pass the SSDP SERVER header
|
||||||
|
// captured by deep-check-device.sh, when present (e.g.
|
||||||
|
// "Linux/3.10 UPnP/1.0 MiniDLNA/1.1"), as extra fingerprinting signal
|
||||||
|
// beyond the bare MAC.
|
||||||
|
upnpServer: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On-demand only (folded into the existing deep-check button), never
|
||||||
|
// automatic/bulk -- keeps this well under Fingerbank's free-tier query
|
||||||
|
// limits and avoids sending every device's MAC to a third party by default.
|
||||||
|
export async function identifyDevice(
|
||||||
|
apiKey: string,
|
||||||
|
signals: FingerbankSignals
|
||||||
|
): Promise<FingerbankResult | null> {
|
||||||
|
const body: Record<string, unknown> = { mac: signals.mac };
|
||||||
|
if (signals.upnpServer) body.upnp_user_agents = [signals.upnpServer];
|
||||||
|
|
||||||
|
const res = await fetch("https://api.fingerbank.org/api/v2/combinations/interrogate", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 404) {
|
||||||
|
// No match in Fingerbank's database -- not an error, just nothing found.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Fingerbank API returned ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await res.json()) as {
|
||||||
|
device_name?: string;
|
||||||
|
score?: number;
|
||||||
|
version?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
deviceName: data.device_name ?? null,
|
||||||
|
score: data.score ?? null,
|
||||||
|
version: data.version ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -99,7 +99,7 @@ async function main() {
|
|||||||
|
|
||||||
registerAuthRoutes(app, db, cfg.oidc !== undefined);
|
registerAuthRoutes(app, db, cfg.oidc !== undefined);
|
||||||
registerHostRoutes(app, db);
|
registerHostRoutes(app, db);
|
||||||
registerDeviceRoutes(app, db, cfg.deepCheck);
|
registerDeviceRoutes(app, db, cfg.deepCheck, cfg.fingerbankApiKey);
|
||||||
|
|
||||||
if (cfg.oidc) {
|
if (cfg.oidc) {
|
||||||
const oidcConfig = await initOidc(cfg.oidc);
|
const oidcConfig = await initOidc(cfg.oidc);
|
||||||
|
|||||||
@@ -6,8 +6,15 @@ import type Database from "better-sqlite3";
|
|||||||
// require()) -- import the default and destructure instead.
|
// require()) -- import the default and destructure instead.
|
||||||
import macOuiLookup from "mac-oui-lookup";
|
import macOuiLookup from "mac-oui-lookup";
|
||||||
const { getVendor } = macOuiLookup;
|
const { getVendor } = macOuiLookup;
|
||||||
import { getRecentDevices, setDeviceLabel, clearDeviceLabel, type DeviceRow } from "../db/index.js";
|
import {
|
||||||
|
getRecentDevices,
|
||||||
|
getDeviceMac,
|
||||||
|
setDeviceLabel,
|
||||||
|
clearDeviceLabel,
|
||||||
|
type DeviceRow,
|
||||||
|
} from "../db/index.js";
|
||||||
import { deepCheckDevice } from "../discovery/deepCheck.js";
|
import { deepCheckDevice } from "../discovery/deepCheck.js";
|
||||||
|
import { identifyDevice } from "../discovery/fingerbank.js";
|
||||||
import type { DeepCheckHostConfig } from "../config/index.js";
|
import type { DeepCheckHostConfig } from "../config/index.js";
|
||||||
|
|
||||||
async function requireAuth(req: FastifyRequest, reply: FastifyReply) {
|
async function requireAuth(req: FastifyRequest, reply: FastifyReply) {
|
||||||
@@ -40,7 +47,8 @@ const IP_RE = /^192\.168\.1\.([0-9]{1,3})$/;
|
|||||||
export function registerDeviceRoutes(
|
export function registerDeviceRoutes(
|
||||||
app: FastifyInstance,
|
app: FastifyInstance,
|
||||||
db: Database.Database,
|
db: Database.Database,
|
||||||
deepCheckConfig: DeepCheckHostConfig | undefined
|
deepCheckConfig: DeepCheckHostConfig | undefined,
|
||||||
|
fingerbankApiKey: string | undefined
|
||||||
): void {
|
): void {
|
||||||
app.get("/api/devices", { preHandler: requireAuth }, async () => {
|
app.get("/api/devices", { preHandler: requireAuth }, async () => {
|
||||||
const rows = getRecentDevices(db);
|
const rows = getRecentDevices(db);
|
||||||
@@ -72,9 +80,10 @@ export function registerDeviceRoutes(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Admin-triggered, single-device, on-demand -- not automatic, to avoid the
|
// Admin-triggered, single-device, on-demand -- not automatic, to avoid the
|
||||||
// noise/risk of doing this for the whole subnet on every poll. Runs on the
|
// noise/risk of doing this for the whole subnet on every poll (and, for
|
||||||
// CT122 host via SSH (see docs/device-discovery.md); can take up to ~15s
|
// Fingerbank, avoids sending every device's MAC to a third party by
|
||||||
// (SSDP listen window + bounded port scan).
|
// default). Runs on the CT122 host via SSH (see docs/device-discovery.md);
|
||||||
|
// can take up to ~15s (SSDP listen window + bounded port scan).
|
||||||
app.post<{ Params: { ip: string } }>(
|
app.post<{ Params: { ip: string } }>(
|
||||||
"/api/devices/:ip/deep-check",
|
"/api/devices/:ip/deep-check",
|
||||||
{ preHandler: requireAuth },
|
{ preHandler: requireAuth },
|
||||||
@@ -83,7 +92,26 @@ export function registerDeviceRoutes(
|
|||||||
if (!IP_RE.test(req.params.ip)) return reply.code(400).send({ error: "invalid IP address" });
|
if (!IP_RE.test(req.params.ip)) return reply.code(400).send({ error: "invalid IP address" });
|
||||||
try {
|
try {
|
||||||
const result = await deepCheckDevice(deepCheckConfig, req.params.ip);
|
const result = await deepCheckDevice(deepCheckConfig, req.params.ip);
|
||||||
return result;
|
|
||||||
|
// Fingerbank is optional enrichment -- a missing key, rate limit, or
|
||||||
|
// network hiccup shouldn't sink the local findings that already
|
||||||
|
// succeeded via SSH.
|
||||||
|
let fingerbank = null;
|
||||||
|
if (fingerbankApiKey) {
|
||||||
|
const mac = getDeviceMac(db, req.params.ip);
|
||||||
|
if (mac) {
|
||||||
|
try {
|
||||||
|
fingerbank = await identifyDevice(fingerbankApiKey, {
|
||||||
|
mac,
|
||||||
|
upnpServer: result.ssdp?.server ?? null,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
req.log.error({ err, ip: req.params.ip }, "fingerbank lookup failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...result, fingerbank };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
req.log.error({ err, ip: req.params.ip }, "deep check failed");
|
req.log.error({ err, ip: req.params.ip }, "deep check failed");
|
||||||
return reply.code(502).send({ error: err instanceof Error ? err.message : "deep check failed" });
|
return reply.code(502).send({ error: err instanceof Error ? err.message : "deep check failed" });
|
||||||
|
|||||||
@@ -79,18 +79,26 @@ export function clearDeviceLabel(mac: string): Promise<{ ok: true }> {
|
|||||||
return request(`/api/devices/${encodeURIComponent(mac)}/label`, { method: "DELETE" });
|
return request(`/api/devices/${encodeURIComponent(mac)}/label`, { method: "DELETE" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FingerbankResult {
|
||||||
|
deviceName: string | null;
|
||||||
|
score: number | null;
|
||||||
|
version: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DeepCheckResult {
|
export interface DeepCheckResult {
|
||||||
ip: string;
|
ip: string;
|
||||||
mdnsHostname: string | null;
|
mdnsHostname: string | null;
|
||||||
reverseDns: string | null;
|
reverseDns: string | null;
|
||||||
ssdp: {
|
ssdp: {
|
||||||
location: string;
|
location: string;
|
||||||
|
server: string | null;
|
||||||
friendlyName: string | null;
|
friendlyName: string | null;
|
||||||
manufacturer: string | null;
|
manufacturer: string | null;
|
||||||
modelName: string | null;
|
modelName: string | null;
|
||||||
} | null;
|
} | null;
|
||||||
openPorts: number[];
|
openPorts: number[];
|
||||||
http: { port: number; title: string | null; server: string | null }[];
|
http: { port: number; title: string | null; server: string | null }[];
|
||||||
|
fingerbank: FingerbankResult | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deepCheckDevice(ip: string): Promise<DeepCheckResult> {
|
export function deepCheckDevice(ip: string): Promise<DeepCheckResult> {
|
||||||
|
|||||||
@@ -90,9 +90,23 @@ function NameCell({ device, onDeviceChanged }: { device: Device; onDeviceChanged
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bands per Fingerbank's own docs: below 30 "very little confidence", 31-50
|
||||||
|
// moderate, 51-75 high, 76+ very high. Shown alongside the result so a
|
||||||
|
// manufacturer-only guess isn't mistaken for a confirmed identification.
|
||||||
|
function confidenceLabel(score: number): string {
|
||||||
|
if (score < 30) return "low confidence";
|
||||||
|
if (score <= 50) return "moderate confidence";
|
||||||
|
if (score <= 75) return "high confidence";
|
||||||
|
return "very high confidence";
|
||||||
|
}
|
||||||
|
|
||||||
function DeepCheckResultPanel({ result }: { result: DeepCheckResult }) {
|
function DeepCheckResultPanel({ result }: { result: DeepCheckResult }) {
|
||||||
const nothingFound =
|
const nothingFound =
|
||||||
!result.mdnsHostname && !result.reverseDns && !result.ssdp && result.openPorts.length === 0;
|
!result.mdnsHostname &&
|
||||||
|
!result.reverseDns &&
|
||||||
|
!result.ssdp &&
|
||||||
|
result.openPorts.length === 0 &&
|
||||||
|
!result.fingerbank?.deviceName;
|
||||||
|
|
||||||
if (nothingFound) {
|
if (nothingFound) {
|
||||||
return <p className="deep-check-empty">No additional information found for {result.ip}.</p>;
|
return <p className="deep-check-empty">No additional information found for {result.ip}.</p>;
|
||||||
@@ -100,6 +114,18 @@ function DeepCheckResultPanel({ result }: { result: DeepCheckResult }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<dl className="deep-check-results">
|
<dl className="deep-check-results">
|
||||||
|
{result.fingerbank?.deviceName && (
|
||||||
|
<>
|
||||||
|
<dt>Fingerbank ID</dt>
|
||||||
|
<dd>
|
||||||
|
{result.fingerbank.deviceName}
|
||||||
|
{result.fingerbank.version && ` (${result.fingerbank.version})`}
|
||||||
|
{result.fingerbank.score !== null && (
|
||||||
|
<span className="name-hint"> — {confidenceLabel(result.fingerbank.score)}</span>
|
||||||
|
)}
|
||||||
|
</dd>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{result.mdnsHostname && (
|
{result.mdnsHostname && (
|
||||||
<>
|
<>
|
||||||
<dt>mDNS hostname</dt>
|
<dt>mDNS hostname</dt>
|
||||||
|
|||||||
@@ -79,6 +79,31 @@ Requires `avahi-utils` and `miniupnpc` installed on the CT122 host (`apt-get ins
|
|||||||
avahi-utils miniupnpc` — a one-time host package install, not part of any deploy
|
avahi-utils miniupnpc` — a one-time host package install, not part of any deploy
|
||||||
script, so re-provisioning CT122 from scratch would need to redo this step).
|
script, so re-provisioning CT122 from scratch would need to redo this step).
|
||||||
|
|
||||||
|
### Fingerbank enrichment (optional)
|
||||||
|
|
||||||
|
When `FINGERBANK_API_KEY` is set, the deep-check route (`apps/api/src/routes/devices.ts`)
|
||||||
|
also queries [Fingerbank](https://fingerbank.org)'s `/api/v2/combinations/interrogate`
|
||||||
|
with the device's MAC plus, if found, the SSDP `SERVER` header as an `upnp_user_agents`
|
||||||
|
signal (`apps/api/src/discovery/fingerbank.ts`). Unlike the other deep-check steps this
|
||||||
|
runs directly from the API container — no multicast/raw-socket access needed, just a
|
||||||
|
normal outbound HTTPS call — so no host-level or SSH changes were needed for this part.
|
||||||
|
|
||||||
|
**Honest limitation, confirmed by testing against a real device**: without a DHCP
|
||||||
|
fingerprint (which requires being the DHCP server — we're not, and have no way to
|
||||||
|
intercept that traffic from CT122), MAC-only or MAC+UPnP-signal queries often can't get
|
||||||
|
past manufacturer-level identification. A Nintendo device on this LAN queried as
|
||||||
|
`{"mac": "..."}` returned `device_name: "Hardware Manufacturer/Nintendo"` at
|
||||||
|
`score: 29` ("very little confidence" per Fingerbank's own bands) — no more specific
|
||||||
|
than the free OUI lookup already gives for free. The UI shows the confidence band
|
||||||
|
alongside the result (`confidenceLabel()` in `DeviceTable.tsx`) precisely so a
|
||||||
|
manufacturer-only guess at low confidence isn't mistaken for a confirmed ID. Still worth
|
||||||
|
having as opt-in enrichment — some devices *do* expose richer signals (a real DHCP
|
||||||
|
fingerprint, a distinctive UPnP string) that push the score meaningfully higher — but
|
||||||
|
don't expect it to reliably answer "what specific model is this" on its own.
|
||||||
|
|
||||||
|
Optional and gated: missing key, a Fingerbank API error, or no match all degrade
|
||||||
|
gracefully — the rest of deep-check's local findings (mDNS/SSDP/ports) are unaffected.
|
||||||
|
|
||||||
## Deploying/updating
|
## Deploying/updating
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ fi
|
|||||||
MDNS=$(timeout 2 avahi-resolve -a "$IP" 2>/dev/null | awk '{print $2}' || true)
|
MDNS=$(timeout 2 avahi-resolve -a "$IP" 2>/dev/null | awk '{print $2}' || true)
|
||||||
RDNS=$(timeout 2 getent hosts "$IP" 2>/dev/null | awk '{print $2}' | head -1 || true)
|
RDNS=$(timeout 2 getent hosts "$IP" 2>/dev/null | awk '{print $2}' | head -1 || true)
|
||||||
|
|
||||||
SSDP_LOCATION=$(timeout 4 python3 - "$IP" <<'PYEOF' || true
|
# Prints LOCATION and SERVER headers (one per line) from the first ssdp:all
|
||||||
|
# response the target IP sends -- SERVER is a useful extra fingerprinting
|
||||||
|
# signal (e.g. "Linux/3.10 UPnP/1.0 MiniDLNA/1.1") beyond just the device
|
||||||
|
# descriptor XML, fed to Fingerbank as an upnp_user_agents hint.
|
||||||
|
SSDP_RAW=$(timeout 4 python3 - "$IP" <<'PYEOF' || true
|
||||||
import socket, sys, time
|
import socket, sys, time
|
||||||
target = sys.argv[1]
|
target = sys.argv[1]
|
||||||
msg = "\r\n".join([
|
msg = "\r\n".join([
|
||||||
@@ -44,14 +48,21 @@ while time.time() - start < 3:
|
|||||||
try:
|
try:
|
||||||
data, addr = sock.recvfrom(4096)
|
data, addr = sock.recvfrom(4096)
|
||||||
if addr[0] == target:
|
if addr[0] == target:
|
||||||
|
location = server = ""
|
||||||
for line in data.decode(errors="replace").split("\r\n"):
|
for line in data.decode(errors="replace").split("\r\n"):
|
||||||
if line.lower().startswith("location:"):
|
if line.lower().startswith("location:"):
|
||||||
print(line.split(":", 1)[1].strip())
|
location = line.split(":", 1)[1].strip()
|
||||||
sys.exit(0)
|
elif line.lower().startswith("server:"):
|
||||||
|
server = line.split(":", 1)[1].strip()
|
||||||
|
print(location)
|
||||||
|
print(server)
|
||||||
|
sys.exit(0)
|
||||||
except socket.timeout:
|
except socket.timeout:
|
||||||
break
|
break
|
||||||
PYEOF
|
PYEOF
|
||||||
)
|
)
|
||||||
|
SSDP_LOCATION=$(echo "$SSDP_RAW" | sed -n '1p')
|
||||||
|
SSDP_SERVER=$(echo "$SSDP_RAW" | sed -n '2p')
|
||||||
|
|
||||||
SSDP_FRIENDLY="" SSDP_MANUFACTURER="" SSDP_MODEL=""
|
SSDP_FRIENDLY="" SSDP_MANUFACTURER="" SSDP_MODEL=""
|
||||||
if [[ -n "$SSDP_LOCATION" ]]; then
|
if [[ -n "$SSDP_LOCATION" ]]; then
|
||||||
@@ -85,11 +96,11 @@ for p in "${open_ports[@]}"; do
|
|||||||
http_titles+=("$p|$title|$server")
|
http_titles+=("$p|$title|$server")
|
||||||
done
|
done
|
||||||
|
|
||||||
python3 - "$IP" "$MDNS" "$RDNS" "$SSDP_LOCATION" "$SSDP_FRIENDLY" "$SSDP_MANUFACTURER" "$SSDP_MODEL" \
|
python3 - "$IP" "$MDNS" "$RDNS" "$SSDP_LOCATION" "$SSDP_SERVER" "$SSDP_FRIENDLY" "$SSDP_MANUFACTURER" "$SSDP_MODEL" \
|
||||||
"$(IFS=,; echo "${open_ports[*]}")" "$(printf '%s\n' "${http_titles[@]}")" <<'PYEOF'
|
"$(IFS=,; echo "${open_ports[*]}")" "$(printf '%s\n' "${http_titles[@]}")" <<'PYEOF'
|
||||||
import json, sys
|
import json, sys
|
||||||
|
|
||||||
ip, mdns, rdns, ssdp_loc, ssdp_friendly, ssdp_mfr, ssdp_model, ports_csv, http_raw = sys.argv[1:10]
|
ip, mdns, rdns, ssdp_loc, ssdp_server, ssdp_friendly, ssdp_mfr, ssdp_model, ports_csv, http_raw = sys.argv[1:11]
|
||||||
|
|
||||||
http = []
|
http = []
|
||||||
for line in http_raw.splitlines():
|
for line in http_raw.splitlines():
|
||||||
@@ -108,6 +119,7 @@ result = {
|
|||||||
"reverseDns": rdns or None,
|
"reverseDns": rdns or None,
|
||||||
"ssdp": {
|
"ssdp": {
|
||||||
"location": ssdp_loc or None,
|
"location": ssdp_loc or None,
|
||||||
|
"server": ssdp_server or None,
|
||||||
"friendlyName": ssdp_friendly or None,
|
"friendlyName": ssdp_friendly or None,
|
||||||
"manufacturer": ssdp_mfr or None,
|
"manufacturer": ssdp_mfr or None,
|
||||||
"modelName": ssdp_model or None,
|
"modelName": ssdp_model or None,
|
||||||
|
|||||||
Reference in New Issue
Block a user