generated from Dicken/dickendock
round49: masscan/httpx-Timeout-Erkennung und Nebenlaeufigkeits-Begrenzung, httpx verwirft offene Ports nicht mehr, FritzBox-Namens-Downgrade auf Platzhalter verhindert
This commit is contained in:
@@ -394,8 +394,31 @@ function mergeFritzboxHosts(hosts: FritzBoxHost[]): FritzBoxHost[] {
|
|||||||
// Vorschläge für abweichende Hostnamen/IPs bereits bekannter Geräte
|
// Vorschläge für abweichende Hostnamen/IPs bereits bekannter Geräte
|
||||||
// einsammeln (siehe upsertDeviceFromScan) - werden NICHT automatisch
|
// einsammeln (siehe upsertDeviceFromScan) - werden NICHT automatisch
|
||||||
// übernommen, nur zur Bestätigung zurückgegeben.
|
// übernommen, nur zur Bestätigung zurückgegeben.
|
||||||
const deviceChanges = results.flatMap((r) =>
|
//
|
||||||
r.changes.map((c) => ({ deviceId: r.device.id, deviceHostname: r.device.hostname, ...c }))
|
// WICHTIG: Ein Vorschlag, der einen bereits bekannten ECHTEN Namen
|
||||||
|
// (z. B. "cam-reolink-einfahrt") auf den generischen
|
||||||
|
// FritzBox-Platzhalter "PC-192-168-1-39" zurückstufen würde, wird
|
||||||
|
// hier ausgefiltert und gar nicht erst vorgeschlagen. Das war die
|
||||||
|
// Ursache der Namens-Regression: mergeFritzboxHosts() bevorzugt den
|
||||||
|
// echten Namen nur, wenn IN DEMSELBEN Scan-Durchlauf mehrere
|
||||||
|
// TR-064-Einträge für dieselbe MAC vorliegen - hat die FritzBox in
|
||||||
|
// einem Durchlauf für ein Gerät NUR den Platzhalter gemeldet (z. B.
|
||||||
|
// kurz nach einem Neustart/DHCP-Lease-Renewal, bevor sie den
|
||||||
|
// mDNS/NetBIOS-Namen kennt), gab es bisher keine zweite Quelle zum
|
||||||
|
// Vergleich - der Platzhalter wurde als ganz normale "Namensänderung"
|
||||||
|
// vorgeschlagen und konnte (z. B. über "Alle bestätigen") den guten
|
||||||
|
// Namen dauerhaft überschreiben.
|
||||||
|
const deviceChanges = results.flatMap((r, i) => {
|
||||||
|
const scannedIp = mergedHosts[i].ip;
|
||||||
|
return r.changes
|
||||||
|
.filter((c) => {
|
||||||
|
if (c.field !== "hostname") return true;
|
||||||
|
const suggestedIsPlaceholder = isGenericFritzboxHostname(c.suggested, scannedIp);
|
||||||
|
const currentIsPlaceholder = isGenericFritzboxHostname(c.current, scannedIp);
|
||||||
|
return !(suggestedIsPlaceholder && !currentIsPlaceholder);
|
||||||
|
})
|
||||||
|
.map((c) => ({ deviceId: r.device.id, deviceHostname: r.device.hostname, ...c }));
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Geräte, die die FritzBox früher gemeldet hatte, diesmal aber nicht
|
// Geräte, die die FritzBox früher gemeldet hatte, diesmal aber nicht
|
||||||
|
|||||||
@@ -23,6 +23,43 @@ export function getHttpxLastError(): string | null {
|
|||||||
return httpxLastError;
|
return httpxLastError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einfacher Zähl-Semaphore (siehe auch masscan.ts), hier mit Kapazität 3:
|
||||||
|
* Beim Sammel-Scan laufen bis zu 10 Geräte-Scans gleichzeitig (siehe
|
||||||
|
* routes/scan.ts CONCURRENT_DEVICES), jeder startet einen eigenen
|
||||||
|
* httpx-Prozess mit intern 50 Threads - ohne Deckel also bis zu 500
|
||||||
|
* gleichzeitige HTTP-Anfragen ALLEIN durch httpx, zusätzlich zum eigenen
|
||||||
|
* TCP-Verbindungs-Semaphore (ports.ts). Unter dieser Last liefen einzelne
|
||||||
|
* httpx-Läufe in den 5s-Timeout pro Port und damit leer, obwohl der Dienst
|
||||||
|
* eigentlich erreichbar war - der Port verschwand dann komplett aus dem
|
||||||
|
* Ergebnis (siehe networkScanner.ts). Kapazität 3 begrenzt das auf maximal
|
||||||
|
* 150 gleichzeitige httpx-Anfragen prozessweit.
|
||||||
|
*/
|
||||||
|
class Semaphore {
|
||||||
|
private available: number;
|
||||||
|
private readonly queue: (() => void)[] = [];
|
||||||
|
|
||||||
|
constructor(count: number) {
|
||||||
|
this.available = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
acquire(): Promise<void> {
|
||||||
|
if (this.available > 0) {
|
||||||
|
this.available--;
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return new Promise((resolve) => this.queue.push(resolve));
|
||||||
|
}
|
||||||
|
|
||||||
|
release(): void {
|
||||||
|
const next = this.queue.shift();
|
||||||
|
if (next) next();
|
||||||
|
else this.available++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const httpxSemaphore = new Semaphore(3);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nutzt httpx (ProjectDiscovery, falls im Container installiert - siehe
|
* Nutzt httpx (ProjectDiscovery, falls im Container installiert - siehe
|
||||||
* Dockerfile) um von einer Liste offener Ports (z. B. das Ergebnis von
|
* Dockerfile) um von einer Liste offener Ports (z. B. das Ergebnis von
|
||||||
@@ -35,23 +72,29 @@ export function getHttpxLastError(): string | null {
|
|||||||
* Liefert null (nicht einen leeren Array!), wenn httpx fehlt oder
|
* Liefert null (nicht einen leeren Array!), wenn httpx fehlt oder
|
||||||
* fehlschlägt - der Aufrufer erkennt daran "kein httpx verfügbar" und fällt
|
* fehlschlägt - der Aufrufer erkennt daran "kein httpx verfügbar" und fällt
|
||||||
* auf die eingebaute Methode zurück, statt fälschlich "keine Webdienste
|
* auf die eingebaute Methode zurück, statt fälschlich "keine Webdienste
|
||||||
* gefunden" anzunehmen.
|
* gefunden" anzunehmen. Wichtig: httpx meldet für einen Port, den es nicht
|
||||||
|
* innerhalb des Timeouts erreicht, GAR NICHTS - das ist von außen nicht von
|
||||||
|
* "das ist wirklich kein Webdienst" zu unterscheiden. networkScanner.ts
|
||||||
|
* behandelt daher von httpx nicht bestätigte, aber offene Ports weiterhin
|
||||||
|
* mit einer eigenen Nachprüfung, statt sie stillschweigend zu verwerfen.
|
||||||
*/
|
*/
|
||||||
export function probeWithHttpx(ip: string, ports: number[]): Promise<Map<number, HttpxResult> | null> {
|
export async function probeWithHttpx(ip: string, ports: number[]): Promise<Map<number, HttpxResult> | null> {
|
||||||
if (httpxConfirmedUnavailable || ports.length === 0) {
|
if (httpxConfirmedUnavailable || ports.length === 0) {
|
||||||
return Promise.resolve(ports.length === 0 ? new Map() : null);
|
return ports.length === 0 ? new Map() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
await httpxSemaphore.acquire();
|
||||||
|
try {
|
||||||
|
return await new Promise((resolve) => {
|
||||||
const child = spawn(
|
const child = spawn(
|
||||||
"httpx",
|
"httpx",
|
||||||
[
|
[
|
||||||
"-silent",
|
"-silent",
|
||||||
"-json",
|
"-json",
|
||||||
"-timeout",
|
"-timeout",
|
||||||
"5",
|
"8",
|
||||||
"-retries",
|
"-retries",
|
||||||
"0",
|
"1",
|
||||||
"-no-color",
|
"-no-color",
|
||||||
"-threads",
|
"-threads",
|
||||||
"50",
|
"50",
|
||||||
@@ -86,14 +129,27 @@ export function probeWithHttpx(ip: string, ports: number[]): Promise<Map<number,
|
|||||||
stderr += chunk.toString("utf-8");
|
stderr += chunk.toString("utf-8");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Grober oberer Zeitrahmen für den GESAMTEN httpx-Lauf (alle Ports
|
||||||
|
// zusammen, dank -threads 50 weitgehend parallel) - unabhängig vom
|
||||||
|
// Pro-Port-Timeout oben. 60s statt vorheriger 30s, damit ein Gerät mit
|
||||||
|
// vielen offenen Ports (nach einem vollständigen 1-65535-Portscan)
|
||||||
|
// nicht schon bei normaler Auslastung abgebrochen wird.
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
httpxLastError = "Zeitüberschreitung (30s) - httpx hat nicht rechtzeitig geantwortet";
|
httpxLastError = `Zeitüberschreitung (60s) - httpx wurde beendet, bevor alle ${ports.length} Port(s) geprüft waren`;
|
||||||
child.kill();
|
child.kill();
|
||||||
finish(null);
|
finish(null);
|
||||||
}, 30_000);
|
}, 60_000);
|
||||||
|
|
||||||
child.on("close", (code) => {
|
child.on("close", (code) => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
// War der Lauf schon durch den Timeout oben als fehlgeschlagen
|
||||||
|
// markiert (settled=true), NICHT mehr überschreiben - sonst geht die
|
||||||
|
// konkrete "Zeitüberschreitung"-Meldung unter der generischen
|
||||||
|
// Exit-Code-Meldung verloren, die "close" hier sonst noch setzen
|
||||||
|
// würde (der Kill selbst löst "close" mit code=null erst danach
|
||||||
|
// aus).
|
||||||
|
if (settled) return;
|
||||||
|
|
||||||
if (code !== 0 && stdout.trim().length === 0) {
|
if (code !== 0 && stdout.trim().length === 0) {
|
||||||
httpxLastError = (stderr.trim() || `httpx beendet mit Exit-Code ${code}`).slice(0, 400);
|
httpxLastError = (stderr.trim() || `httpx beendet mit Exit-Code ${code}`).slice(0, 400);
|
||||||
if (/not found|no such file/i.test(stderr)) httpxConfirmedUnavailable = true;
|
if (/not found|no such file/i.test(stderr)) httpxConfirmedUnavailable = true;
|
||||||
@@ -138,4 +194,7 @@ export function probeWithHttpx(ip: string, ports: number[]): Promise<Map<number,
|
|||||||
}
|
}
|
||||||
child.stdin.end();
|
child.stdin.end();
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
httpxSemaphore.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,70 @@ import { execFile } from "node:child_process";
|
|||||||
|
|
||||||
let masscanConfirmedUnavailable = false;
|
let masscanConfirmedUnavailable = false;
|
||||||
|
|
||||||
// Klartext-Grund des letzten Fehlschlags (stderr/Fehlermeldung von masscan
|
// Klartext-Grund des letzten Fehlschlags - wird im Scan-Log angezeigt, damit
|
||||||
// selbst) - wird im Scan-Log angezeigt, damit man sieht WARUM masscan nicht
|
// man sieht WARUM masscan nicht greift, statt nur "nicht genutzt/verfügbar"
|
||||||
// greift (z. B. fehlende Rechte trotz cap_add, kein Interface gefunden),
|
// ohne jede Erklärung. WICHTIG: node's execFile hängt bei einem per Timeout
|
||||||
// statt nur "nicht genutzt/verfügbar" ohne jede Erklärung.
|
// gekillten Prozess die (oft nur unvollständige) stderr-Ausgabe direkt in
|
||||||
|
// error.message - das sieht bei masscan wie eine ganz normale
|
||||||
|
// Fortschrittsanzeige aus ("Starting masscan...", "rate: ..., found=0"),
|
||||||
|
// ist aber KEINE Fehlermeldung von masscan selbst, sondern der letzte Stand
|
||||||
|
// VOR dem Abbruch. Deshalb wird unten explizit geprüft, ob der Prozess per
|
||||||
|
// Timeout gekillt wurde (error.killed/error.signal), und das als eigener,
|
||||||
|
// klar benannter Grund ausgegeben - statt die Restausgabe fälschlich als
|
||||||
|
// "das ist der Fehler" zu präsentieren.
|
||||||
let masscanLastError: string | null = null;
|
let masscanLastError: string | null = null;
|
||||||
|
|
||||||
export function getMasscanLastError(): string | null {
|
export function getMasscanLastError(): string | null {
|
||||||
return masscanLastError;
|
return masscanLastError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einfacher Zähl-Semaphore (Kopie des Patterns aus ports.ts), hier mit
|
||||||
|
* Kapazität 1: masscan verschickt SYN-Pakete per Rohsocket direkt über das
|
||||||
|
* Netzwerk-Interface des Containers - laufen mehrere masscan-Prozesse
|
||||||
|
* GLEICHZEITIG (z. B. weil der Sammel-Scan bis zu 10 Geräte parallel
|
||||||
|
* abarbeitet, siehe routes/scan.ts CONCURRENT_DEVICES), konkurrieren sie um
|
||||||
|
* dasselbe Interface. Das führt zu Paketverlust und dazu, dass einzelne
|
||||||
|
* Läufe den 90-Sekunden-Timeout reißen und fälschlich als "nicht
|
||||||
|
* verfügbar" erscheinen, obwohl masscan grundsätzlich funktioniert -
|
||||||
|
* genau das beobachtete "mal geht's, mal nicht". Mit Kapazität 1 laufen
|
||||||
|
* masscan-Aufrufe aus mehreren parallelen Geräte-Scans strikt nacheinander,
|
||||||
|
* der Rest des jeweiligen Geräte-Scans (DNS, httpx, Softwareerkennung)
|
||||||
|
* bleibt weiterhin parallel.
|
||||||
|
*/
|
||||||
|
class Semaphore {
|
||||||
|
private available: number;
|
||||||
|
private readonly queue: (() => void)[] = [];
|
||||||
|
|
||||||
|
constructor(count: number) {
|
||||||
|
this.available = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
acquire(): Promise<void> {
|
||||||
|
if (this.available > 0) {
|
||||||
|
this.available--;
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return new Promise((resolve) => this.queue.push(resolve));
|
||||||
|
}
|
||||||
|
|
||||||
|
release(): void {
|
||||||
|
const next = this.queue.shift();
|
||||||
|
if (next) next();
|
||||||
|
else this.available++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const masscanSemaphore = new Semaphore(1);
|
||||||
|
|
||||||
|
// masscan braucht bei rate=2000 für den vollen Portbereich (1-65535) rein
|
||||||
|
// rechnerisch ca. 33s Sendezeit + einige Sekunden Nachlaufzeit für späte
|
||||||
|
// Antworten. 60s waren in der Praxis (Docker-in-LXC, ggf. wartende
|
||||||
|
// Konkurrenz-Scans durch den neuen Semaphore oben) zu knapp bemessen und
|
||||||
|
// haben laufende, aber noch nicht fertige Scans abgewürgt. 90s geben Luft,
|
||||||
|
// ohne einen wirklich hängenden Prozess ewig weiterlaufen zu lassen.
|
||||||
|
const MASSCAN_TIMEOUT_MS = 90_000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nutzt masscan (falls im Container installiert, siehe Dockerfile) für den
|
* Nutzt masscan (falls im Container installiert, siehe Dockerfile) für den
|
||||||
* vollständigen Portscan (1-65535) - masscan verschickt SYN-Pakete
|
* vollständigen Portscan (1-65535) - masscan verschickt SYN-Pakete
|
||||||
@@ -19,10 +73,11 @@ export function getMasscanLastError(): string | null {
|
|||||||
* und ist dadurch um ein Vielfaches schneller als das eingebaute
|
* und ist dadurch um ein Vielfaches schneller als das eingebaute
|
||||||
* Batch-TCP-Connect-Scanning (scanPortsInBatches in ports.ts).
|
* Batch-TCP-Connect-Scanning (scanPortsInBatches in ports.ts).
|
||||||
*
|
*
|
||||||
* Fällt automatisch (und leise) auf null zurück, wenn masscan fehlt, keine
|
* Fällt automatisch auf null zurück, wenn masscan fehlt, keine Rechte hat
|
||||||
* Rechte hat (braucht CAP_NET_RAW, siehe docker-compose.yml) oder aus
|
* (braucht CAP_NET_RAW, siehe docker-compose.yml) oder aus anderem Grund
|
||||||
* anderem Grund fehlschlägt - der Aufrufer nutzt dann stattdessen
|
* fehlschlägt - der Aufrufer nutzt dann stattdessen scanPortsInBatches. Kein
|
||||||
* scanPortsInBatches. Kein hartes Erfordernis, nur eine Beschleunigung.
|
* hartes Erfordernis, nur eine Beschleunigung. Der genaue Grund eines
|
||||||
|
* Fehlschlags steht danach in getMasscanLastError().
|
||||||
*/
|
*/
|
||||||
export async function scanWithMasscan(
|
export async function scanWithMasscan(
|
||||||
host: string,
|
host: string,
|
||||||
@@ -31,32 +86,47 @@ export async function scanWithMasscan(
|
|||||||
): Promise<number[] | null> {
|
): Promise<number[] | null> {
|
||||||
if (masscanConfirmedUnavailable) return null;
|
if (masscanConfirmedUnavailable) return null;
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
await masscanSemaphore.acquire();
|
||||||
execFile(
|
try {
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
const child = execFile(
|
||||||
"masscan",
|
"masscan",
|
||||||
[host, "-p", portRange, "--rate", String(rate), "-oL", "-"],
|
[host, "-p", portRange, "--rate", String(rate), "-oL", "-"],
|
||||||
{ timeout: 60_000 },
|
{ timeout: MASSCAN_TIMEOUT_MS },
|
||||||
(error, stdout, stderr) => {
|
(error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
// Klartext-Fehler für den Aufrufer/das Scan-Log merken - stderr
|
const wasTimeout = error.killed === true;
|
||||||
// enthält bei masscan i. d. R. den eigentlichen Grund (z. B.
|
const lastOutput = (stderr?.trim() || stdout?.trim() || "").slice(0, 300);
|
||||||
// "FAIL:.. Permission denied", "could not detect interface"),
|
|
||||||
// error.message oft nur den generischen Exit-Code.
|
if (wasTimeout) {
|
||||||
const rawReason = (stderr?.trim() || error.message).slice(0, 400);
|
// Explizit als Zeitüberschreitung benennen - die mitgelieferte
|
||||||
masscanLastError = rawReason;
|
// stderr-Ausgabe ist nur der letzte Stand VOR dem Abbruch
|
||||||
|
// (masscans eigene Fortschrittsanzeige), keine Fehlermeldung.
|
||||||
|
masscanLastError = `Zeitüberschreitung nach ${MASSCAN_TIMEOUT_MS / 1000}s - masscan wurde per ${error.signal ?? "Signal"} beendet, bevor der Scan fertig war. Letzter Stand vor Abbruch: "${lastOutput || "(keine Ausgabe)"}"`;
|
||||||
|
} else {
|
||||||
|
// Echter Fehlschlag (Exit-Code ≠ 0, nicht durch Timeout) -
|
||||||
|
// stderr enthält bei masscan i. d. R. den eigentlichen Grund
|
||||||
|
// (z. B. "FAIL:.. Permission denied", "could not detect
|
||||||
|
// interface"), error.message oft nur den generischen Exit-Code.
|
||||||
|
const exitCode = error.code ?? "unbekannt";
|
||||||
|
masscanLastError = `Exit-Code ${exitCode}: ${(stderr?.trim() || error.message).slice(0, 350)}`;
|
||||||
|
}
|
||||||
|
|
||||||
// ENOENT (Programm fehlt) oder fehlende Rechte (EPERM/"PCAP" o. ä.
|
// ENOENT (Programm fehlt) oder fehlende Rechte (EPERM/"PCAP" o. ä.
|
||||||
// in stderr) - beides bedeutet: masscan für diese Installation
|
// in stderr) - beides bedeutet: masscan für diese Installation
|
||||||
// dauerhaft nicht nutzbar, künftig direkt auf den Fallback gehen
|
// dauerhaft nicht nutzbar, künftig direkt auf den Fallback gehen
|
||||||
// statt bei jedem Scan erneut den (dann meist schnell fehlschlagenden)
|
// statt bei jedem Scan erneut den (dann meist schnell
|
||||||
// Versuch zu machen.
|
// fehlschlagenden) Versuch zu machen. Ein Timeout zählt
|
||||||
|
// ausdrücklich NICHT dazu - das kann situationsabhängig sein
|
||||||
|
// (Netzlast) und beim nächsten Mal klappen.
|
||||||
const msg = `${error.message} ${stderr ?? ""}`.toLowerCase();
|
const msg = `${error.message} ${stderr ?? ""}`.toLowerCase();
|
||||||
if (
|
if (
|
||||||
msg.includes("enoent") ||
|
!wasTimeout &&
|
||||||
|
(msg.includes("enoent") ||
|
||||||
msg.includes("not found") ||
|
msg.includes("not found") ||
|
||||||
msg.includes("permission") ||
|
msg.includes("permission") ||
|
||||||
msg.includes("operation not permitted") ||
|
msg.includes("operation not permitted") ||
|
||||||
msg.includes("pcap")
|
msg.includes("pcap"))
|
||||||
) {
|
) {
|
||||||
masscanConfirmedUnavailable = true;
|
masscanConfirmedUnavailable = true;
|
||||||
}
|
}
|
||||||
@@ -78,7 +148,11 @@ export async function scanWithMasscan(
|
|||||||
resolve(ports);
|
resolve(ports);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
void child;
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
masscanSemaphore.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isMasscanConfirmedUnavailable(): boolean {
|
export function isMasscanConfirmedUnavailable(): boolean {
|
||||||
|
|||||||
@@ -182,22 +182,32 @@ export async function scanDeviceServices(
|
|||||||
(p) => !AVM_MANAGEMENT_PORTS.has(p) || priorityPorts.includes(p)
|
(p) => !AVM_MANAGEMENT_PORTS.has(p) || priorityPorts.includes(p)
|
||||||
);
|
);
|
||||||
|
|
||||||
// httpx (falls installiert, siehe Dockerfile) filtert aus den offenen
|
// httpx (falls installiert, siehe Dockerfile) prüft die offenen Ports
|
||||||
// Ports NUR die tatsächlich HTTP/HTTPS-sprechenden heraus - deutlich
|
// vorab in einem einzigen, hochparallelen Durchlauf und liefert für die
|
||||||
// schneller UND präziser als unser eigenes Protokoll-Raten (erst HTTP
|
// tatsächlich HTTP/HTTPS-sprechenden gleich Schema/Titel/Server-Header
|
||||||
// versuchen, bei Fehlschlag HTTPS versuchen) für JEDEN einzelnen offenen
|
// mit - das spart probePort() unten den sonst nötigen Protokoll-Rate-
|
||||||
// Port einzeln. Ports wie SSH/SMB/NFS, die zwar offen sind aber gar kein
|
// Versuch (erst HTTP, bei Fehlschlag HTTPS) für JEDEN Port einzeln.
|
||||||
// HTTP sprechen, werden so gar nicht erst an die aufwendigere
|
// httpxResults === null heißt "httpx nicht verfügbar" - dann bleibt es
|
||||||
// Detail-Pipeline (probeHttp, Softwareerkennung, Icon-Abgleich)
|
// beim bisherigen Verhalten (jeder offene Port wird von probePort()
|
||||||
// weitergereicht. httpxResults === null heißt "httpx nicht verfügbar" -
|
// geprüft, Protokoll wird pro Port erraten). Ein von httpx NICHT
|
||||||
// dann bleibt es beim bisherigen Verhalten (jeder offene Port wird
|
// bestätigter Port wird (seit der Regression mit stillschweigend
|
||||||
// geprüft, Protokoll wird pro Port erraten).
|
// verschwundenen Diensten unter Last) trotzdem an probePort() weiter-
|
||||||
|
// gereicht statt verworfen - siehe Kommentar unten.
|
||||||
const httpxResults = openPorts.length > 0 ? await probeWithHttpx(device.ip, openPorts) : new Map();
|
const httpxResults = openPorts.length > 0 ? await probeWithHttpx(device.ip, openPorts) : new Map();
|
||||||
const usingHttpx = httpxResults !== null;
|
const usingHttpx = httpxResults !== null;
|
||||||
const httpxError = !usingHttpx ? getHttpxLastError() : null;
|
const httpxError = !usingHttpx ? getHttpxLastError() : null;
|
||||||
if (usingHttpx) {
|
// WICHTIG: openPorts wird NICHT mehr auf die von httpx bestätigten Ports
|
||||||
openPorts = Array.from(httpxResults!.keys());
|
// eingeschränkt. httpx meldet für einen Port, den es nicht innerhalb
|
||||||
}
|
// seines Timeouts erreicht (z. B. unter Last durch mehrere gleichzeitige
|
||||||
|
// Geräte-Scans, siehe httpx.ts), schlicht GAR NICHTS - das ist von hier
|
||||||
|
// aus nicht von "spricht wirklich kein HTTP" zu unterscheiden. Frühere
|
||||||
|
// Version hat in diesem Fall den kompletten Port verworfen und damit
|
||||||
|
// echte, aber unter Last verpasste Webdienste stillschweigend als "kein
|
||||||
|
// Dienst gefunden" gemeldet. Jetzt bleiben alle offenen Ports erhalten;
|
||||||
|
// httpx liefert nur noch (wo vorhanden) das bereits bekannte Schema als
|
||||||
|
// Abkürzung für probePort() unten - fehlt ein Port in httpxResults, prüft
|
||||||
|
// probePort() ihn ganz normal selbst (identisch zum Verhalten ganz ohne
|
||||||
|
// httpx), statt ihn zu überspringen.
|
||||||
|
|
||||||
const found: DiscoveredService[] = [];
|
const found: DiscoveredService[] = [];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user