generated from Dicken/dickendock
round37: API-Scanner Kontrollprobe entschaerft (weniger Fehlalarme false negatives), Alle-bestaetigen-Buttons, Scan-Buttons bei Geraeten zusammengelegt
This commit is contained in:
@@ -128,21 +128,14 @@ function isApiLikeResponse(result: RawProbeResult): boolean {
|
||||
* Reine HTML-Seiten (z. B. eine 404-Fehlerseite des Frontends oder eine
|
||||
* Login-Weiterleitung ohne API-Signal) zählen nicht.
|
||||
*
|
||||
* VORAB ein Kontroll-Check gegen einen garantiert nicht existierenden Pfad:
|
||||
* manche Geräte (z. B. IP-Kameras) beantworten JEDE Anfrage einheitlich mit
|
||||
* z. B. Basic-Auth-Aufforderung oder demselben JSON-Fehlerobjekt - dann
|
||||
* würden buchstäblich ALLE Kandidaten-Pfade fälschlich als "API gefunden"
|
||||
* durchgehen (siehe Bugreport: eine Reolink-Türklingel "hatte" plötzlich
|
||||
* Home-Assistant-, Proxmox- UND Portainer-APIs). Reagiert das Kontroll-Pfad
|
||||
* genauso "positiv" wie ein echter Treffer, ist dem Gerät für diese Prüfung
|
||||
* grundsätzlich nicht zu trauen - dann lieber gar kein Ergebnis als 30 falsche.
|
||||
* KEIN pauschaler Kontroll-Check gegen einen zufälligen Pfad mehr (frühere
|
||||
* Version): viele Selfhosted-Programme (Sonarr, Portainer, Home Assistant, …)
|
||||
* fordern für JEDE unauthentifizierte Anfrage einheitlich eine Anmeldung -
|
||||
* das sah wie ein Gerät aus, "dem man nicht trauen kann", war aber normales,
|
||||
* erwartbares Verhalten und hat massenhaft echte Treffer unterdrückt.
|
||||
* Stattdessen wird JEDER einzelne Treffer für sich geprüft (siehe unten).
|
||||
*/
|
||||
export async function detectApis(baseUrl: string): Promise<DetectedApi[]> {
|
||||
const control = await fetchRaw(`${baseUrl}/__launchpad_api_probe_${Date.now()}__`);
|
||||
if (control && control.status !== 404 && control.status !== 0 && isApiLikeResponse(control)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const checks = await Promise.all(
|
||||
CANDIDATE_PATHS.map(async ({ path, type }) => {
|
||||
const result = await fetchRaw(`${baseUrl}${path}`);
|
||||
@@ -156,19 +149,28 @@ export async function detectApis(baseUrl: string): Promise<DetectedApi[]> {
|
||||
|
||||
let matches = checks.filter((c): c is DetectedApi => c !== null);
|
||||
|
||||
// Zweite Stufe gegen Catch-all-Verhalten, das die Kontrollprobe oben nicht
|
||||
// erwischt (z. B. FritzBox/Hue Bridge: 404 für "echte" Fremdpfade, aber
|
||||
// jede Anfrage UNTER/NAHE einem bekannten API-Pfad wird gleich generisch
|
||||
// beantwortet). Für jeden Treffer wird eine offensichtlich erfundene
|
||||
// Variante DESSELBEN Pfads geprüft (letztes Segment mit Zufallssuffix).
|
||||
// Antwortet die genauso "positiv", war der ursprüngliche Treffer
|
||||
// vermutlich nur ein generischer Handler, kein echter Endpunkt.
|
||||
// Gegen Catch-all-Verhalten (z. B. FritzBox/Hue Bridge: 404 für "echte"
|
||||
// Fremdpfade, aber jede Anfrage UNTER einem bekannten API-Pfad wird gleich
|
||||
// generisch beantwortet - ganz unabhängig vom genauen Unterpfad): für
|
||||
// jeden Treffer werden ZWEI erfundene Varianten geprüft - (a) derselbe
|
||||
// Pfad mit verändertem letzten Segment, (b) ein komplett zufälliger Pfad
|
||||
// im selben Verzeichnis. Antwortet EINE der beiden genauso "positiv", war
|
||||
// der ursprüngliche Treffer nur ein genereller Handler, kein echter,
|
||||
// spezifischer Endpunkt.
|
||||
const verified = await Promise.all(
|
||||
matches.map(async (m) => {
|
||||
const mutatedPath = m.path.replace(/([^/]+)$/, `$1-x${Date.now().toString(36)}`);
|
||||
const sibling = await fetchRaw(`${baseUrl}${mutatedPath}`);
|
||||
const isCatchAll =
|
||||
!!sibling && sibling.status !== 404 && sibling.status !== 0 && isApiLikeResponse(sibling);
|
||||
const mutatedLastSegment = m.path.replace(/([^/]+)$/, `$1-x${Date.now().toString(36)}`);
|
||||
const directoryPrefix = m.path.slice(0, m.path.lastIndexOf("/") + 1);
|
||||
const randomSibling = `${directoryPrefix}__lp_probe_${Date.now().toString(36)}__`;
|
||||
|
||||
const [siblingA, siblingB] = await Promise.all([
|
||||
fetchRaw(`${baseUrl}${mutatedLastSegment}`),
|
||||
directoryPrefix.length > 1 ? fetchRaw(`${baseUrl}${randomSibling}`) : Promise.resolve(null),
|
||||
]);
|
||||
|
||||
const isCatchAll = [siblingA, siblingB].some(
|
||||
(s) => !!s && s.status !== 404 && s.status !== 0 && isApiLikeResponse(s)
|
||||
);
|
||||
return isCatchAll ? null : m;
|
||||
})
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user