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.
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
#!/usr/bin/env node
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { z } from "zod";
|
||||
import { decodeConnectionCode } from "./code.mjs";
|
||||
import { debugGet } from "./http.mjs";
|
||||
|
||||
function loadServerMap() {
|
||||
const map = new Map();
|
||||
const raw = process.env.RDDIAG_SERVERS;
|
||||
if (raw) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
for (const [name, code] of Object.entries(parsed || {})) {
|
||||
map.set(String(name), String(code));
|
||||
}
|
||||
} catch {
|
||||
process.stderr.write("rd-diagnostics-mcp: RDDIAG_SERVERS ist kein gueltiges JSON, wird ignoriert\n");
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
const SERVER_MAP = loadServerMap();
|
||||
const DEFAULT_CODE = process.env.RDDIAG_CODE ? String(process.env.RDDIAG_CODE) : "";
|
||||
|
||||
function listAvailableServers() {
|
||||
const names = [...SERVER_MAP.keys()];
|
||||
if (DEFAULT_CODE) names.push("(RDDIAG_CODE-Default)");
|
||||
return names;
|
||||
}
|
||||
|
||||
function resolveTarget(args) {
|
||||
let code = "";
|
||||
if (args && args.code) {
|
||||
code = String(args.code);
|
||||
} else if (args && args.server) {
|
||||
const found = SERVER_MAP.get(String(args.server));
|
||||
if (!found) {
|
||||
throw new Error(
|
||||
`Server "${args.server}" nicht konfiguriert. Bekannt: ${listAvailableServers().join(", ") || "(keine)"}`
|
||||
);
|
||||
}
|
||||
code = found;
|
||||
} else if (DEFAULT_CODE) {
|
||||
code = DEFAULT_CODE;
|
||||
} else if (SERVER_MAP.size === 1) {
|
||||
code = [...SERVER_MAP.values()][0];
|
||||
} else {
|
||||
throw new Error(
|
||||
`Kein Verbindungscode. Uebergib "code" oder "server", oder setze RDDIAG_CODE/RDDIAG_SERVERS. Bekannt: ${listAvailableServers().join(", ") || "(keine)"}`
|
||||
);
|
||||
}
|
||||
return decodeConnectionCode(code);
|
||||
}
|
||||
|
||||
function targetLabel(target) {
|
||||
return target.name ? `${target.name} (${target.host}:${target.port})` : `${target.host}:${target.port}`;
|
||||
}
|
||||
|
||||
function buildQuery(params) {
|
||||
const usable = Object.entries(params || {}).filter(
|
||||
([, v]) => v !== undefined && v !== null && String(v).length > 0
|
||||
);
|
||||
if (usable.length === 0) return "";
|
||||
const sp = new URLSearchParams();
|
||||
for (const [k, v] of usable) sp.set(k, String(v));
|
||||
return "?" + sp.toString();
|
||||
}
|
||||
|
||||
function prettyBody(body) {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(body), null, 2);
|
||||
} catch {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
function connectionHint(err) {
|
||||
const m = String((err && err.code) || err && err.message || "");
|
||||
if (/ECONNREFUSED/.test(m)) return "Debug-Server nicht erreichbar — auf dem Server aktiviert? Port/Firewall offen?";
|
||||
if (/ENOTFOUND|EAI_AGAIN/.test(m)) return "Host nicht aufloesbar — stimmt die Adresse im Verbindungscode?";
|
||||
if (/ETIMEDOUT|Zeitueberschreitung/.test(m)) return "Zeitueberschreitung — Server/Netz langsam oder Port geblockt.";
|
||||
if (/ECONNRESET|EPIPE/.test(m)) return "Verbindung abgebrochen — falscher Port/Scheme (http vs https)?";
|
||||
if (/Fingerprint/.test(m)) return "TLS-Fingerprint passt nicht — Code stammt evtl. von einem anderen Server.";
|
||||
return "";
|
||||
}
|
||||
|
||||
async function requestTool(args, path, params, opts = {}) {
|
||||
let target;
|
||||
try {
|
||||
target = resolveTarget(args);
|
||||
} catch (err) {
|
||||
return { content: [{ type: "text", text: `# Verbindungsfehler\n${err.message}` }], isError: true };
|
||||
}
|
||||
const fullPath = path + buildQuery(params);
|
||||
const label = targetLabel(target);
|
||||
try {
|
||||
const res = await debugGet(target, fullPath, { timeoutMs: opts.timeoutMs || 20000 });
|
||||
const isError = res.status < 200 || res.status >= 300;
|
||||
let extra = "";
|
||||
if (res.status === 401) extra = "\n(401 = Token im Verbindungscode ist abgelaufen/rotiert. Neuen Code anfordern.)";
|
||||
if (res.status === 503) extra = "\n(503 = Download-Manager nicht bereit. App laeuft, aber noch nicht initialisiert?)";
|
||||
const head = `# ${label} ${fullPath} → HTTP ${res.status}${extra}`;
|
||||
return { content: [{ type: "text", text: head + "\n" + prettyBody(res.body) }], isError };
|
||||
} catch (err) {
|
||||
const hint = connectionHint(err);
|
||||
const text = `# ${label} ${fullPath} → FEHLER\n${err.message}${hint ? "\n→ " + hint : ""}`;
|
||||
return { content: [{ type: "text", text }], isError: true };
|
||||
}
|
||||
}
|
||||
|
||||
const CODE_FIELD = {
|
||||
code: z.string().optional().describe("Verbindungscode (rddiag:v1:...). Optional, wenn server/RDDIAG_CODE gesetzt ist."),
|
||||
server: z.string().optional().describe("Name eines via RDDIAG_SERVERS konfigurierten Servers statt eines vollen Codes.")
|
||||
};
|
||||
|
||||
const server = new McpServer({ name: "rd-diagnostics-mcp", version: "1.0.0" });
|
||||
|
||||
server.registerTool(
|
||||
"rd_servers",
|
||||
{
|
||||
title: "Konfigurierte Server",
|
||||
description: "Listet die in dieser Bridge konfigurierten Server (RDDIAG_SERVERS / RDDIAG_CODE). Verbindet sich nicht.",
|
||||
inputSchema: {}
|
||||
},
|
||||
async () => {
|
||||
const names = [...SERVER_MAP.keys()];
|
||||
const lines = [];
|
||||
lines.push(`Konfigurierte Server: ${names.length}`);
|
||||
for (const n of names) lines.push(`- ${n}`);
|
||||
lines.push(`Default (RDDIAG_CODE): ${DEFAULT_CODE ? "gesetzt" : "nicht gesetzt"}`);
|
||||
lines.push("");
|
||||
lines.push("Tools akzeptieren entweder code:<rddiag:v1:...> oder server:<name>.");
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"rd_ping",
|
||||
{
|
||||
title: "Erreichbarkeit pruefen",
|
||||
description: "Schneller Health-Check (GET /health): App-Version, Uptime, Speicher. Zuerst aufrufen, um Erreichbarkeit + Token zu pruefen.",
|
||||
inputSchema: { ...CODE_FIELD }
|
||||
},
|
||||
async (args) => requestTool(args, "/health", {}, { timeoutMs: 10000 })
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"rd_diagnostics",
|
||||
{
|
||||
title: "Gesamtdiagnose",
|
||||
description: "Aggregierter Zustand (GET /diagnostics): Meta, Status, Settings, Stats, Accounts, History, Host + die wichtigsten Logs. Der 'alles auf einen Blick'-Endpunkt.",
|
||||
inputSchema: {
|
||||
...CODE_FIELD,
|
||||
lines: z.number().int().positive().optional().describe("Anzahl Log-Zeilen pro Log (Default 150)."),
|
||||
grep: z.string().optional().describe("Filter fuer Log-Zeilen."),
|
||||
package: z.string().optional().describe("Optional auf ein Paket fokussieren.")
|
||||
}
|
||||
},
|
||||
async (args) => requestTool(args, "/diagnostics", { lines: args.lines, grep: args.grep, package: args.package }, { timeoutMs: 30000 })
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"rd_status",
|
||||
{
|
||||
title: "Live-Status",
|
||||
description: "Laufzeit-Status (GET /status): aktive Downloads, Queue, Provider-Zustand.",
|
||||
inputSchema: { ...CODE_FIELD }
|
||||
},
|
||||
async (args) => requestTool(args, "/status", {})
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"rd_items",
|
||||
{
|
||||
title: "Download-Items",
|
||||
description: "Einzelne Download-Items (GET /items), optional gefiltert nach Status/Paket.",
|
||||
inputSchema: {
|
||||
...CODE_FIELD,
|
||||
status: z.string().optional().describe("Status-Filter (z.B. downloading, error, done)."),
|
||||
package: z.string().optional().describe("Paket-Filter.")
|
||||
}
|
||||
},
|
||||
async (args) => requestTool(args, "/items", { status: args.status, package: args.package })
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"rd_packages",
|
||||
{
|
||||
title: "Pakete",
|
||||
description: "Pakete (GET /packages), optional mit enthaltenen Items.",
|
||||
inputSchema: {
|
||||
...CODE_FIELD,
|
||||
package: z.string().optional().describe("Bestimmtes Paket."),
|
||||
includeItems: z.boolean().optional().describe("Items mitliefern.")
|
||||
}
|
||||
},
|
||||
async (args) => requestTool(args, "/packages", { package: args.package, includeItems: args.includeItems ? "1" : "" })
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"rd_errors",
|
||||
{
|
||||
title: "Letzte Fehler",
|
||||
description: "Fehler-Ring (GET /errors): die letzten Fehler mit Level/Quelle. 'Was ist schiefgelaufen'.",
|
||||
inputSchema: {
|
||||
...CODE_FIELD,
|
||||
level: z.string().optional().describe("Level-Filter (ERROR, WARN, ...)."),
|
||||
limit: z.number().int().positive().optional().describe("Anzahl (Default 100).")
|
||||
}
|
||||
},
|
||||
async (args) => requestTool(args, "/errors", { level: args.level, limit: args.limit })
|
||||
);
|
||||
|
||||
const LOG_PATHS = {
|
||||
main: "/logs/main",
|
||||
audit: "/logs/audit",
|
||||
rename: "/logs/rename",
|
||||
trace: "/logs/trace",
|
||||
session: "/logs/session",
|
||||
package: "/logs/package",
|
||||
item: "/logs/item"
|
||||
};
|
||||
|
||||
server.registerTool(
|
||||
"rd_logs",
|
||||
{
|
||||
title: "Log lesen",
|
||||
description: "Liest das Ende eines Logs (GET /logs/<name>). name: main|audit|rename|trace|session|package|item. Fuer package/item zusaetzlich package/item angeben.",
|
||||
inputSchema: {
|
||||
...CODE_FIELD,
|
||||
name: z.enum(["main", "audit", "rename", "trace", "session", "package", "item"]).describe("Welches Log."),
|
||||
lines: z.number().int().positive().optional().describe("Anzahl Zeilen vom Ende (Default 100)."),
|
||||
grep: z.string().optional().describe("Filter."),
|
||||
package: z.string().optional().describe("Nur fuer name=package."),
|
||||
item: z.string().optional().describe("Nur fuer name=item.")
|
||||
}
|
||||
},
|
||||
async (args) => {
|
||||
const path = LOG_PATHS[args.name];
|
||||
return requestTool(args, path, { lines: args.lines, grep: args.grep, package: args.package, item: args.item });
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"rd_history",
|
||||
{
|
||||
title: "Verlauf",
|
||||
description: "Abgeschlossener Verlauf (GET /history), optional nach Status/Suchbegriff.",
|
||||
inputSchema: {
|
||||
...CODE_FIELD,
|
||||
limit: z.number().int().positive().optional().describe("Anzahl (Default 50)."),
|
||||
status: z.string().optional().describe("Status-Filter."),
|
||||
grep: z.string().optional().describe("Suchbegriff.")
|
||||
}
|
||||
},
|
||||
async (args) => requestTool(args, "/history", { limit: args.limit, status: args.status, grep: args.grep })
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"rd_accounts",
|
||||
{
|
||||
title: "Accounts",
|
||||
description: "Debrid-Accounts (GET /accounts, Token redigiert): Gueltigkeit, Premium, Cooldown/Rotation.",
|
||||
inputSchema: { ...CODE_FIELD }
|
||||
},
|
||||
async (args) => requestTool(args, "/accounts", {})
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"rd_host",
|
||||
{
|
||||
title: "Host-Diagnose",
|
||||
description: "Windows-Host-Diagnose (GET /host/diagnostics): Laufwerke, Speicher, Pfade.",
|
||||
inputSchema: { ...CODE_FIELD }
|
||||
},
|
||||
async (args) => requestTool(args, "/host/diagnostics", {})
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"rd_self_check",
|
||||
{
|
||||
title: "Self-Check",
|
||||
description: "Setup/Self-Check (GET /self-check): erkennt Konfigurations-/Pfadprobleme.",
|
||||
inputSchema: { ...CODE_FIELD }
|
||||
},
|
||||
async (args) => requestTool(args, "/self-check", {})
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"rd_get",
|
||||
{
|
||||
title: "Roh-Endpunkt (Escape-Hatch)",
|
||||
description: "Beliebigen Debug-Server-Pfad lesen (GET <path>), wenn kein spezialisiertes Tool passt. Pfad inkl. fuehrendem / und optionalem Query-String, z.B. /meta oder /stats.",
|
||||
inputSchema: {
|
||||
...CODE_FIELD,
|
||||
path: z.string().describe("Pfad mit fuehrendem /, optional ?query. Nur GET, read-only.")
|
||||
}
|
||||
},
|
||||
async (args) => {
|
||||
const p = String(args.path || "");
|
||||
if (!p.startsWith("/")) {
|
||||
return { content: [{ type: "text", text: "# Fehler\npath muss mit / beginnen" }], isError: true };
|
||||
}
|
||||
return requestTool(args, p, {}, { timeoutMs: 30000 });
|
||||
}
|
||||
);
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
process.stderr.write(
|
||||
`rd-diagnostics-mcp bereit. Server: ${listAvailableServers().join(", ") || "(keine vorkonfiguriert; code pro Aufruf uebergeben)"}\n`
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
process.stderr.write(`rd-diagnostics-mcp Startfehler: ${err && err.stack ? err.stack : err}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
export interface DecodedConnectionCode {
|
||||
host: string;
|
||||
port: number;
|
||||
token: string;
|
||||
scheme: string;
|
||||
name: string;
|
||||
fingerprint: string;
|
||||
}
|
||||
|
||||
export interface EncodeConnectionCodeInput {
|
||||
host: string;
|
||||
port: number;
|
||||
token: string;
|
||||
name?: string;
|
||||
fingerprint?: string;
|
||||
scheme?: string;
|
||||
}
|
||||
|
||||
export function encodeConnectionCode(input: EncodeConnectionCodeInput): string;
|
||||
export function decodeConnectionCode(code: string): DecodedConnectionCode;
|
||||
@@ -0,0 +1,56 @@
|
||||
const PREFIX = "rddiag:v1:";
|
||||
|
||||
function base64urlEncode(str) {
|
||||
return Buffer.from(str, "utf8")
|
||||
.toString("base64")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
}
|
||||
|
||||
function base64urlDecode(str) {
|
||||
const pad = str.length % 4 === 0 ? "" : "=".repeat(4 - (str.length % 4));
|
||||
const b64 = str.replace(/-/g, "+").replace(/_/g, "/") + pad;
|
||||
return Buffer.from(b64, "base64").toString("utf8");
|
||||
}
|
||||
|
||||
export function encodeConnectionCode({ host, port, token, name, fingerprint, scheme }) {
|
||||
if (!host || typeof host !== "string") throw new Error("host fehlt");
|
||||
const p = Number(port);
|
||||
if (!Number.isInteger(p) || p < 1 || p > 65535) throw new Error("port ungueltig");
|
||||
if (!token || typeof token !== "string") throw new Error("token fehlt");
|
||||
const payload = { v: 1, h: host, p, t: token };
|
||||
if (name) payload.n = String(name);
|
||||
if (fingerprint) payload.fp = String(fingerprint);
|
||||
if (scheme && scheme !== "http") payload.s = String(scheme);
|
||||
return PREFIX + base64urlEncode(JSON.stringify(payload));
|
||||
}
|
||||
|
||||
export function decodeConnectionCode(code) {
|
||||
const raw = String(code || "").trim();
|
||||
if (!raw.startsWith(PREFIX)) {
|
||||
throw new Error(`Verbindungscode muss mit "${PREFIX}" beginnen`);
|
||||
}
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(base64urlDecode(raw.slice(PREFIX.length)));
|
||||
} catch {
|
||||
throw new Error("Verbindungscode ist beschaedigt (kein gueltiges base64url/JSON)");
|
||||
}
|
||||
if (!json || typeof json !== "object") throw new Error("Verbindungscode-Inhalt ungueltig");
|
||||
const host = String(json.h || "").trim();
|
||||
const port = Number(json.p);
|
||||
const token = String(json.t || "");
|
||||
if (!host) throw new Error("Verbindungscode ohne Host");
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Verbindungscode mit ungueltigem Port");
|
||||
if (!token) throw new Error("Verbindungscode ohne Token");
|
||||
const scheme = json.s === "https" ? "https" : "http";
|
||||
return {
|
||||
host,
|
||||
port,
|
||||
token,
|
||||
scheme,
|
||||
name: json.n ? String(json.n) : "",
|
||||
fingerprint: json.fp ? String(json.fp) : ""
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env node
|
||||
import { encodeConnectionCode } from "./code.mjs";
|
||||
|
||||
function arg(name, fallback) {
|
||||
const i = process.argv.indexOf("--" + name);
|
||||
if (i >= 0 && i + 1 < process.argv.length) return process.argv[i + 1];
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const host = arg("host");
|
||||
const port = arg("port");
|
||||
const token = arg("token");
|
||||
const name = arg("name");
|
||||
const scheme = arg("scheme");
|
||||
const fingerprint = arg("fp");
|
||||
|
||||
if (!host || !port || !token) {
|
||||
process.stderr.write("Usage: node src/gen-code.mjs --host <h> --port <p> --token <t> [--name <n>] [--scheme https] [--fp <sha256>]\n");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
process.stdout.write(encodeConnectionCode({ host, port, token, name, scheme, fingerprint }) + "\n");
|
||||
@@ -0,0 +1,63 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user