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.
64 lines
1.9 KiB
JavaScript
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();
|
|
}
|
|
});
|
|
}
|