real-debrid-downloader/tools/rd-diagnostics-mcp/src/http.mjs
Sucukdeluxe c8beaf96ed Ferndiagnose (MCP): Verbindungscode + abgesicherter Fernzugriff + stdio-Bridge
Neue Funktion, um Diagnose eines laufenden Servers aus der Ferne zu ermoeglichen:
Hilfe -> Remote-Support -> "Ferndiagnose (MCP)". Erzeugt einen Verbindungscode,
den der Assistent nutzt, um Status, Logs, Fehler und Accounts read-only zu lesen.

App-Seite:
- Live (re)startbarer Debug-Server ohne App-Neustart (restartDebugServer wartet auf
  'close' + closeAllConnections, behandelt EADDRINUSE).
- IP-Allowlist (debug_allowlist.txt, exakte IP + CIDR), erzwungen VOR der Auth.
  Fail-closed: Netzwerk-Bind (0.0.0.0) ohne Allowlist akzeptiert nur Loopback.
- One-Click Aktivieren/Aktualisieren/Deaktivieren + Token-Rotation (alter Code sofort
  ungueltig). Sichtbarkeit waehlbar: "Nur lokal" (Tunnel-Empfehlung) vs "Im Netzwerk".
- Verbindungscode rddiag:v1:base64url({v,h,p,t,n?,fp?,s?}); oeffentlicher Host frei
  waehlbar, Netzwerk-IPs als Schnellauswahl.
- Neue IPC: get/enable/disable/rotate Remote-Diagnostics; Controller-Methoden; Typen.

Bridge (tools/rd-diagnostics-mcp, standalone, KEINE App-Dependency):
- stdio MCP-Server (@modelcontextprotocol/sdk) mit 14 Tools, proxyt die bestehende
  HTTP-Debug-API. Multi-Server ueber code/server/RDDIAG_CODE/RDDIAG_SERVERS.
- TLS-Fingerprint-Pinning auf secureConnect (vor Token-Versand), falls https genutzt.
- test/harness.mjs: faehrt einen Fake-Debug-Server hoch und treibt die Bridge als
  echten stdio-Child per JSON-RPC -> voller Protokollpfad gruen.

Sicherheit (Audit): keine persistenten Secrets in den Logs der Debug-API (Passwoerter
redigiert, keine Debrid-Keys/aufgeloesten Download-URLs geloggt; settings/accounts
redigiert). Empfohlener Transport: Loopback + privater Tunnel; Direkt-Bind nur mit
Allowlist in vertrauenswuerdigen Netzen.

Tests: connection-code-Cross-Check (App-Encoder <-> Bridge-Decoder), Allowlist-Matrix
(Loopback, exakt, CIDR, fail-closed, Live-Restart). Volle Suite 905 gruen, tsc=6.
2026-06-19 17:05:28 +02:00

64 lines
1.9 KiB
JavaScript

import http from "node:http";
import https from "node:https";
function normalizeFp(fp) {
return String(fp || "").replace(/:/g, "").toLowerCase();
}
export function debugGet(target, path, { timeoutMs = 20000 } = {}) {
const scheme = target.scheme === "https" ? "https" : "http";
const lib = scheme === "https" ? https : http;
const rel = path.startsWith("/") ? path : "/" + path;
const url = new URL(rel, `${scheme}://${target.host}:${target.port}`);
const pinning = scheme === "https" && !!target.fingerprint;
return new Promise((resolve, reject) => {
const options = {
method: "GET",
headers: {
Authorization: `Bearer ${target.token}`,
Accept: "application/json"
},
timeout: timeoutMs
};
if (scheme === "https") {
options.rejectUnauthorized = !target.fingerprint;
}
const req = lib.request(url, options, (res) => {
let data = "";
res.setEncoding("utf8");
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
resolve({ status: res.statusCode || 0, body: data, headers: res.headers });
});
});
req.on("timeout", () => {
req.destroy(new Error(`Zeitueberschreitung nach ${timeoutMs}ms`));
});
req.on("error", (err) => {
reject(err);
});
if (pinning) {
req.on("socket", (socket) => {
socket.on("secureConnect", () => {
const cert = typeof socket.getPeerCertificate === "function" ? socket.getPeerCertificate() : null;
const got = normalizeFp(cert && cert.fingerprint256);
const want = normalizeFp(target.fingerprint);
if (!got || got !== want) {
req.destroy(new Error(`TLS-Fingerprint stimmt nicht (erwartet ${want || "?"}, erhalten ${got || "?"})`));
return;
}
req.end();
});
});
} else {
req.end();
}
});
}