feat: harden remote diagnostics authentication
Bind the diagnostics flow to bearer-only authentication and reject query token attempts with controlled responses. Compare bearer token bytes with timing-safe equality, remove wildcard CORS, and mark diagnostics/support responses as no-store. Move trace configuration mutation behind POST, add method failure handling, add a small per-IP/loopback in-memory request limit, and keep generated setup and support-manifest URLs token-free while pointing support access at a local bridge/tunnel flow. Sanitize backup remote diagnostics on export and restore so legacy token, endpoint, host mode, and port values are not persisted; restores only keep the allowlist and force local binding. Tests cover bearer accept/reject, query rejection, GET mutation rejection, no-store/CORS behavior, loopback default binding, rate limiting, token-free hints, and backup sanitation.
This commit is contained in:
+36
-34
@@ -3,10 +3,10 @@ import type { AppSettings, SessionState, HistoryEntry } from "../shared/types";
|
||||
export type BackupKind = "full" | "settings-only";
|
||||
|
||||
export interface BackupRemoteDiagnostics {
|
||||
allowlist: string[];
|
||||
port: number;
|
||||
hostMode: "local" | "network";
|
||||
}
|
||||
allowlist?: string[];
|
||||
port?: number;
|
||||
hostMode?: "local" | "network";
|
||||
}
|
||||
|
||||
export interface BackupPayload {
|
||||
version: 2;
|
||||
@@ -35,7 +35,7 @@ export interface BuildBackupInput {
|
||||
* bundled solely when settings.backupIncludeDownloads is true. An explicit kind
|
||||
* marker makes the import side unambiguous and survives hand-edited files.
|
||||
*/
|
||||
export function buildBackupPayload(input: BuildBackupInput): BackupPayload {
|
||||
export function buildBackupPayload(input: BuildBackupInput): BackupPayload {
|
||||
const includeDownloads = Boolean(input.settings.backupIncludeDownloads);
|
||||
const base: BackupPayload = {
|
||||
version: 2,
|
||||
@@ -49,37 +49,39 @@ export function buildBackupPayload(input: BuildBackupInput): BackupPayload {
|
||||
base.history = input.history;
|
||||
}
|
||||
if (Boolean(input.settings.backupIncludeRemoteDiagnostics) && input.remoteDiagnostics) {
|
||||
base.remoteDiagnostics = input.remoteDiagnostics;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
base.remoteDiagnostics = sanitizeBackupRemoteDiagnostics(input.remoteDiagnostics);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
export interface RemoteDiagnosticsRestore {
|
||||
host?: "127.0.0.1" | "0.0.0.0";
|
||||
port?: number;
|
||||
allowlist?: string[];
|
||||
}
|
||||
|
||||
host?: "127.0.0.1";
|
||||
port?: number;
|
||||
allowlist?: string[];
|
||||
}
|
||||
|
||||
function sanitizeAllowlist(value: unknown): string[] {
|
||||
return Array.isArray(value)
|
||||
? value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim())
|
||||
: [];
|
||||
}
|
||||
|
||||
function sanitizeBackupRemoteDiagnostics(section: BackupRemoteDiagnostics): BackupRemoteDiagnostics {
|
||||
return {
|
||||
allowlist: sanitizeAllowlist(section.allowlist)
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveRemoteDiagnosticsRestore(section: unknown): RemoteDiagnosticsRestore | null {
|
||||
if (!section || typeof section !== "object") {
|
||||
return null;
|
||||
}
|
||||
const s = section as { allowlist?: unknown; port?: unknown; hostMode?: unknown };
|
||||
const allowlist = Array.isArray(s.allowlist)
|
||||
? s.allowlist.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim())
|
||||
: undefined;
|
||||
const port = (typeof s.port === "number" && Number.isInteger(s.port) && s.port >= 1024 && s.port <= 65535) ? s.port : undefined;
|
||||
let host: "127.0.0.1" | "0.0.0.0" | undefined;
|
||||
if (s.hostMode === "network") {
|
||||
host = allowlist && allowlist.length > 0 ? "0.0.0.0" : "127.0.0.1";
|
||||
} else if (s.hostMode === "local") {
|
||||
host = "127.0.0.1";
|
||||
}
|
||||
if (host === undefined && port === undefined && allowlist === undefined) {
|
||||
return null;
|
||||
}
|
||||
return { host, port, allowlist };
|
||||
}
|
||||
if (!section || typeof section !== "object") {
|
||||
return null;
|
||||
}
|
||||
const s = section as { allowlist?: unknown };
|
||||
return {
|
||||
host: "127.0.0.1",
|
||||
allowlist: sanitizeAllowlist(s.allowlist)
|
||||
};
|
||||
}
|
||||
|
||||
export interface ImportPlan {
|
||||
valid: boolean;
|
||||
|
||||
+200
-112
@@ -21,16 +21,18 @@ import { getWindowsHostDiagnostics } from "./windows-host-diagnostics";
|
||||
import type { DownloadManager } from "./download-manager";
|
||||
import type { DownloadItem, PackageEntry, UiSnapshot } from "../shared/types";
|
||||
|
||||
const DEFAULT_PORT = 9868;
|
||||
const DEFAULT_HOST = "127.0.0.1";
|
||||
const MAX_LOG_LINES = 10000;
|
||||
const DEFAULT_PORT = 9868;
|
||||
const DEFAULT_HOST = "127.0.0.1";
|
||||
const MAX_LOG_LINES = 10000;
|
||||
const SUPPORT_MANIFEST_FILE = "debug_support_manifest.json";
|
||||
|
||||
type DebugEndpointDescriptor = {
|
||||
method: "GET";
|
||||
path: string;
|
||||
queryExample?: string;
|
||||
description: string;
|
||||
const RATE_LIMIT_WINDOW_MS = 60_000;
|
||||
const RATE_LIMIT_MAX_REQUESTS = 120;
|
||||
|
||||
type DebugEndpointDescriptor = {
|
||||
method: "GET" | "POST";
|
||||
path: string;
|
||||
queryExample?: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
const DEBUG_ENDPOINTS: DebugEndpointDescriptor[] = [
|
||||
@@ -48,8 +50,9 @@ const DEBUG_ENDPOINTS: DebugEndpointDescriptor[] = [
|
||||
{ method: "GET", path: "/logs/conversion", queryExample: "lines=100&grep=keyword", description: "Reads the per-item link conversion/unrestrict lifecycle log (token, API getLink, web, account rotation, aborts with timings)." },
|
||||
{ method: "GET", path: "/logs/package", queryExample: "package=Release&lines=100&grep=keyword", description: "Reads the package log for a specific package name or id." },
|
||||
{ method: "GET", path: "/logs/item", queryExample: "item=episode.part2.rar&lines=100&grep=keyword", description: "Reads the item log for a specific file name or item id." },
|
||||
{ method: "GET", path: "/errors", queryExample: "level=ERROR&limit=100", description: "Returns the in-memory ring of the most recent WARN/ERROR log lines." },
|
||||
{ method: "GET", path: "/trace/config", queryExample: "enable=1¬e=support&durationMinutes=120", description: "Reads or updates the support trace configuration." },
|
||||
{ method: "GET", path: "/errors", queryExample: "level=ERROR&limit=100", description: "Returns the in-memory ring of the most recent WARN/ERROR log lines." },
|
||||
{ method: "GET", path: "/trace/config", description: "Reads the support trace configuration." },
|
||||
{ method: "POST", path: "/trace/config", queryExample: "enable=1¬e=support&durationMinutes=120", description: "Updates the support trace configuration." },
|
||||
{ method: "GET", path: "/settings", description: "Returns a redacted settings snapshot without raw secrets." },
|
||||
{ method: "GET", path: "/accounts", description: "Returns a redacted account/provider configuration summary." },
|
||||
{ method: "GET", path: "/providers", description: "Live provider runtime state: per-account/key cooldowns (until/remaining/reason/category), in-flight depth, Mega rotation cursor, empty-response streaks. The 'why is it cooling down right now' view." },
|
||||
@@ -67,9 +70,10 @@ let server: http.Server | null = null;
|
||||
let manager: DownloadManager | null = null;
|
||||
let authToken = "";
|
||||
let bindHost = DEFAULT_HOST;
|
||||
let bindPort = DEFAULT_PORT;
|
||||
let runtimeBaseDir = "";
|
||||
let allowlist: string[] = [];
|
||||
let bindPort = DEFAULT_PORT;
|
||||
let runtimeBaseDir = "";
|
||||
let allowlist: string[] = [];
|
||||
let requestLimits = new Map<string, { startedAt: number; count: number }>();
|
||||
|
||||
export interface DebugServerRuntimeStatus {
|
||||
running: boolean;
|
||||
@@ -241,27 +245,40 @@ function isClientAllowed(clientIp: string): boolean {
|
||||
return evaluateClientAllowed(clientIp, allowlist);
|
||||
}
|
||||
|
||||
function checkAuth(req: http.IncomingMessage): boolean {
|
||||
if (!authToken) {
|
||||
return false;
|
||||
}
|
||||
const header = req.headers.authorization || "";
|
||||
if (header === `Bearer ${authToken}`) {
|
||||
return true;
|
||||
}
|
||||
const url = new URL(req.url || "/", "http://localhost");
|
||||
return url.searchParams.get("token") === authToken;
|
||||
}
|
||||
|
||||
function jsonResponse(res: http.ServerResponse, status: number, data: unknown): void {
|
||||
const body = JSON.stringify(data, null, 2);
|
||||
res.writeHead(status, {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Cache-Control": "no-cache"
|
||||
});
|
||||
res.end(body);
|
||||
}
|
||||
function checkAuth(req: http.IncomingMessage): boolean {
|
||||
if (!authToken) {
|
||||
return false;
|
||||
}
|
||||
const header = typeof req.headers.authorization === "string" ? req.headers.authorization : "";
|
||||
const match = /^Bearer\s+(.+)$/i.exec(header);
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
const supplied = Buffer.from(match[1] || "", "utf8");
|
||||
const expected = Buffer.from(authToken, "utf8");
|
||||
if (supplied.length !== expected.length) {
|
||||
return false;
|
||||
}
|
||||
return crypto.timingSafeEqual(supplied, expected);
|
||||
}
|
||||
|
||||
function noStoreHeaders(extra: Record<string, string> = {}): Record<string, string> {
|
||||
return {
|
||||
"Cache-Control": "no-store",
|
||||
"Pragma": "no-cache",
|
||||
"Expires": "0",
|
||||
...extra
|
||||
};
|
||||
}
|
||||
|
||||
function jsonResponse(res: http.ServerResponse, status: number, data: unknown, headers: Record<string, string> = {}): void {
|
||||
const body = JSON.stringify(data, null, 2);
|
||||
res.writeHead(status, {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
...noStoreHeaders(headers)
|
||||
});
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
function binaryResponse(
|
||||
res: http.ServerResponse,
|
||||
@@ -270,15 +287,37 @@ function binaryResponse(
|
||||
contentType: string,
|
||||
fileName?: string
|
||||
): void {
|
||||
res.writeHead(status, {
|
||||
"Content-Type": contentType,
|
||||
"Content-Length": String(body.length),
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Cache-Control": "no-cache",
|
||||
...(fileName ? { "Content-Disposition": `attachment; filename="${fileName}"` } : {})
|
||||
});
|
||||
res.end(body);
|
||||
}
|
||||
res.writeHead(status, {
|
||||
"Content-Type": contentType,
|
||||
"Content-Length": String(body.length),
|
||||
...noStoreHeaders(),
|
||||
...(fileName ? { "Content-Disposition": `attachment; filename="${fileName}"` } : {})
|
||||
});
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
function rateLimitKey(clientIp: string): string {
|
||||
const normalized = normalizeIp(clientIp);
|
||||
return isLoopbackIp(normalized) || normalized === "" ? "loopback" : normalized;
|
||||
}
|
||||
|
||||
function checkRateLimit(clientIp: string): { allowed: boolean; retryAfterSeconds: number } {
|
||||
const now = Date.now();
|
||||
const key = rateLimitKey(clientIp);
|
||||
const current = requestLimits.get(key);
|
||||
if (!current || now - current.startedAt >= RATE_LIMIT_WINDOW_MS) {
|
||||
requestLimits.set(key, { startedAt: now, count: 1 });
|
||||
return { allowed: true, retryAfterSeconds: 0 };
|
||||
}
|
||||
current.count += 1;
|
||||
if (current.count <= RATE_LIMIT_MAX_REQUESTS) {
|
||||
return { allowed: true, retryAfterSeconds: 0 };
|
||||
}
|
||||
return {
|
||||
allowed: false,
|
||||
retryAfterSeconds: Math.max(1, Math.ceil((RATE_LIMIT_WINDOW_MS - (now - current.startedAt)) / 1000))
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeLinesParam(rawValue: string | null, fallback: number): number {
|
||||
const parsed = Number(rawValue || String(fallback));
|
||||
@@ -351,11 +390,10 @@ function buildSupportManifest(baseDir: string): Record<string, unknown> {
|
||||
purpose: "Machine-readable manifest for support tooling and remote troubleshooting.",
|
||||
auth: {
|
||||
required: true,
|
||||
methods: [
|
||||
"Authorization: Bearer <token>",
|
||||
"?token=<token>"
|
||||
],
|
||||
tokenFile: path.join(baseDir, "debug_token.txt")
|
||||
methods: [
|
||||
"Authorization: Bearer <token>"
|
||||
],
|
||||
tokenFile: path.join(baseDir, "debug_token.txt")
|
||||
},
|
||||
runtimeFiles: {
|
||||
hostFile: path.join(baseDir, "debug_host.txt"),
|
||||
@@ -374,19 +412,18 @@ function buildSupportManifest(baseDir: string): Record<string, unknown> {
|
||||
historyFile: path.join(baseDir, "rd_history.json")
|
||||
},
|
||||
debugServer: {
|
||||
enabled: Boolean(authToken),
|
||||
host: bindHost,
|
||||
port: bindPort,
|
||||
localBaseUrl: `http://127.0.0.1:${bindPort}`,
|
||||
remoteBaseUrlTemplate: `http://<SERVER_IP_OR_DNS>:${bindPort}`
|
||||
enabled: Boolean(authToken),
|
||||
host: bindHost,
|
||||
port: bindPort,
|
||||
localBaseUrl: `http://127.0.0.1:${bindPort}`,
|
||||
remoteBridgeBaseUrlTemplate: `http://127.0.0.1:${bindPort}`
|
||||
},
|
||||
setupCheckEndpoint: "/debug/setup",
|
||||
selfCheckEndpoint: "/self-check",
|
||||
remoteAccessRequirements: [
|
||||
"A reachable server IP or DNS name.",
|
||||
"The configured diagnostics port.",
|
||||
"The token stored in debug_token.txt.",
|
||||
"A network route and firewall rule that permit access to the configured port."
|
||||
"Use a trusted local bridge or tunnel to forward support traffic to the loopback diagnostics port.",
|
||||
"Send Authorization: Bearer with the token stored in debug_token.txt.",
|
||||
"Do not expose the diagnostics port directly on LAN or WAN."
|
||||
],
|
||||
endpoints: DEBUG_ENDPOINTS.map((endpoint) => ({
|
||||
...endpoint,
|
||||
@@ -499,7 +536,7 @@ function getItemLogPathForQuery(snapshot: UiSnapshot, query: string): { item: Do
|
||||
return { item: null, logPath: directPath };
|
||||
}
|
||||
|
||||
function buildStatusPayload(snapshot: UiSnapshot): Record<string, unknown> {
|
||||
function buildStatusPayload(snapshot: UiSnapshot): Record<string, unknown> {
|
||||
const items = Object.values(snapshot.session.items);
|
||||
const packages = Object.values(snapshot.session.packages);
|
||||
|
||||
@@ -527,33 +564,54 @@ function buildStatusPayload(snapshot: UiSnapshot): Record<string, unknown> {
|
||||
packages: packages.map((pkg) => summarizePackage(snapshot, pkg, false)),
|
||||
activeItems,
|
||||
failedItems: failedItems.length > 0 ? failedItems : undefined
|
||||
};
|
||||
}
|
||||
|
||||
function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
|
||||
const url = new URL(req.url || "/", "http://localhost");
|
||||
const pathname = url.pathname;
|
||||
const traceConfig = getTraceConfig();
|
||||
if (traceConfig.enabled && traceConfig.logDebugRequests) {
|
||||
logTraceEvent("INFO", "debug-http", "Request", {
|
||||
method: req.method || "GET",
|
||||
url: sanitizeRequestUrlForTrace(req.url || "/"),
|
||||
clientIp: extractDebugClientIp(req)
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
res.writeHead(204, {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Headers": "Authorization",
|
||||
"Access-Control-Allow-Methods": "GET,OPTIONS"
|
||||
});
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const peerIp = getPeerIp(req);
|
||||
if (!isClientAllowed(peerIp)) {
|
||||
};
|
||||
}
|
||||
|
||||
function hasTraceMutationParams(url: URL): boolean {
|
||||
return ["enable", "includeMainLog", "includeAudit", "logDebugRequests", "note", "durationMinutes"]
|
||||
.some((key) => url.searchParams.has(key));
|
||||
}
|
||||
|
||||
function allowedMethods(pathname: string, url: URL): string[] {
|
||||
if (pathname === "/trace/config") {
|
||||
return hasTraceMutationParams(url) ? ["POST"] : ["GET", "POST"];
|
||||
}
|
||||
return ["GET"];
|
||||
}
|
||||
|
||||
function rejectMethod(res: http.ServerResponse, methods: string[]): void {
|
||||
jsonResponse(res, 405, {
|
||||
error: "Method Not Allowed",
|
||||
code: "method_not_allowed",
|
||||
allowedMethods: methods
|
||||
}, {
|
||||
"Allow": methods.join(", ")
|
||||
});
|
||||
}
|
||||
|
||||
function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
|
||||
const url = new URL(req.url || "/", "http://localhost");
|
||||
const pathname = url.pathname;
|
||||
const method = (req.method || "GET").toUpperCase();
|
||||
const traceConfig = getTraceConfig();
|
||||
if (traceConfig.enabled && traceConfig.logDebugRequests) {
|
||||
logTraceEvent("INFO", "debug-http", "Request", {
|
||||
method,
|
||||
url: sanitizeRequestUrlForTrace(req.url || "/"),
|
||||
clientIp: extractDebugClientIp(req)
|
||||
});
|
||||
}
|
||||
|
||||
if (url.searchParams.has("token")) {
|
||||
jsonResponse(res, 400, {
|
||||
error: "Bad Request",
|
||||
code: "query_token_rejected"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const peerIp = getPeerIp(req);
|
||||
if (!isClientAllowed(peerIp)) {
|
||||
if (traceConfig.enabled && traceConfig.logDebugRequests) {
|
||||
logTraceEvent("WARN", "debug-http", "Durch Allowlist blockiert", {
|
||||
peerIp,
|
||||
@@ -561,21 +619,41 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
||||
url: sanitizeRequestUrlForTrace(req.url || "/")
|
||||
});
|
||||
}
|
||||
jsonResponse(res, 403, { error: "Forbidden", reason: "Client-IP nicht in Allowlist", clientIp: peerIp });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkAuth(req)) {
|
||||
if (traceConfig.enabled && traceConfig.logDebugRequests) {
|
||||
logTraceEvent("WARN", "debug-http", "Unauthorized request", {
|
||||
method: req.method || "GET",
|
||||
url: sanitizeRequestUrlForTrace(req.url || "/"),
|
||||
clientIp: extractDebugClientIp(req)
|
||||
});
|
||||
}
|
||||
jsonResponse(res, 401, { error: "Unauthorized" });
|
||||
return;
|
||||
}
|
||||
jsonResponse(res, 403, { error: "Forbidden", reason: "Client-IP nicht in Allowlist", clientIp: peerIp });
|
||||
return;
|
||||
}
|
||||
|
||||
const limit = checkRateLimit(peerIp);
|
||||
if (!limit.allowed) {
|
||||
jsonResponse(res, 429, {
|
||||
error: "Too Many Requests",
|
||||
code: "rate_limited",
|
||||
retryAfterSeconds: limit.retryAfterSeconds
|
||||
}, {
|
||||
"Retry-After": String(limit.retryAfterSeconds)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkAuth(req)) {
|
||||
if (traceConfig.enabled && traceConfig.logDebugRequests) {
|
||||
logTraceEvent("WARN", "debug-http", "Unauthorized request", {
|
||||
method,
|
||||
url: sanitizeRequestUrlForTrace(req.url || "/"),
|
||||
clientIp: extractDebugClientIp(req)
|
||||
});
|
||||
}
|
||||
jsonResponse(res, 401, { error: "Unauthorized", code: "unauthorized" }, {
|
||||
"WWW-Authenticate": "Bearer"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const methods = allowedMethods(pathname, url);
|
||||
if (!methods.includes(method)) {
|
||||
rejectMethod(res, methods);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === "/health") {
|
||||
jsonResponse(res, 200, {
|
||||
@@ -719,8 +797,16 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === "/trace/config") {
|
||||
const patch: Record<string, unknown> = {};
|
||||
if (pathname === "/trace/config") {
|
||||
if (method === "GET") {
|
||||
jsonResponse(res, 200, {
|
||||
path: getTraceConfigPath(),
|
||||
logPath: getTraceLogPath(),
|
||||
config: getTraceConfig()
|
||||
});
|
||||
return;
|
||||
}
|
||||
const patch: Record<string, unknown> = {};
|
||||
const enabled = toBooleanQuery(url.searchParams.get("enable"));
|
||||
const includeMainLog = toBooleanQuery(url.searchParams.get("includeMainLog"));
|
||||
const includeAudit = toBooleanQuery(url.searchParams.get("includeAudit"));
|
||||
@@ -1055,9 +1141,10 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
||||
});
|
||||
}
|
||||
|
||||
function openServerSocket(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
authToken = loadToken(runtimeBaseDir);
|
||||
function openServerSocket(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
requestLimits = new Map();
|
||||
authToken = loadToken(runtimeBaseDir);
|
||||
bindPort = getPort(runtimeBaseDir);
|
||||
bindHost = getHost(runtimeBaseDir);
|
||||
allowlist = loadAllowlist(runtimeBaseDir);
|
||||
@@ -1168,14 +1255,15 @@ export function clearDebugToken(): void {
|
||||
writeSupportManifest(runtimeBaseDir);
|
||||
}
|
||||
|
||||
export function stopDebugServer(): void {
|
||||
if (server) {
|
||||
export function stopDebugServer(): void {
|
||||
if (server) {
|
||||
server.close();
|
||||
try {
|
||||
server.closeAllConnections?.();
|
||||
} catch {
|
||||
}
|
||||
server = null;
|
||||
logger.info("Debug-Server gestoppt");
|
||||
}
|
||||
}
|
||||
server = null;
|
||||
requestLimits = new Map();
|
||||
logger.info("Debug-Server gestoppt");
|
||||
}
|
||||
}
|
||||
|
||||
+20
-20
@@ -348,14 +348,14 @@ export function getDebugSetupCheck(baseDir: string): DebugSetupCheckResult {
|
||||
};
|
||||
const supportBundle = getSupportBundleEstimate(baseDir, logSummary);
|
||||
|
||||
if (!token) {
|
||||
warnings.push("debug_token.txt fehlt oder ist leer. Der Debug-Server startet dann nicht.");
|
||||
}
|
||||
if (localOnly) {
|
||||
warnings.push("Der Debug-Server ist aktuell nur lokal erreichbar. Für Remote-Support debug_host.txt auf 0.0.0.0 setzen.");
|
||||
} else {
|
||||
notes.push("Der Debug-Server ist für Remote-Zugriff konfiguriert. Firewall oder Provider-Regeln müssen separat offen sein.");
|
||||
}
|
||||
if (!token) {
|
||||
warnings.push("debug_token.txt fehlt oder ist leer. Der Debug-Server startet dann nicht.");
|
||||
}
|
||||
if (localOnly) {
|
||||
notes.push("Der Debug-Server ist lokal gebunden. Remote-Support nutzt eine vertrauenswürdige Bridge oder einen Tunnel auf diese lokale Adresse.");
|
||||
} else {
|
||||
warnings.push("Der Debug-Server ist nicht lokal gebunden. Für Support sicherheitshalber auf 127.0.0.1 zurückstellen und eine Bridge oder einen Tunnel nutzen.");
|
||||
}
|
||||
if (!fs.existsSync(supportManifestPath)) {
|
||||
warnings.push("debug_support_manifest.json fehlt. App einmal neu starten, damit das Support-Manifest neu geschrieben wird.");
|
||||
}
|
||||
@@ -421,15 +421,15 @@ export function getDebugSetupCheck(baseDir: string): DebugSetupCheckResult {
|
||||
supportBundle,
|
||||
warnings,
|
||||
notes,
|
||||
localUrls: {
|
||||
health: `http://127.0.0.1:${port}/health?token=${token || "<TOKEN>"}`,
|
||||
meta: `http://127.0.0.1:${port}/meta?token=${token || "<TOKEN>"}`,
|
||||
diagnostics: `http://127.0.0.1:${port}/diagnostics?token=${token || "<TOKEN>"}`
|
||||
},
|
||||
remoteUrlTemplates: {
|
||||
health: `http://<SERVER_IP_OR_DNS>:${port}/health?token=${token || "<TOKEN>"}`,
|
||||
meta: `http://<SERVER_IP_OR_DNS>:${port}/meta?token=${token || "<TOKEN>"}`,
|
||||
diagnostics: `http://<SERVER_IP_OR_DNS>:${port}/diagnostics?token=${token || "<TOKEN>"}`
|
||||
}
|
||||
};
|
||||
}
|
||||
localUrls: {
|
||||
health: `http://127.0.0.1:${port}/health`,
|
||||
meta: `http://127.0.0.1:${port}/meta`,
|
||||
diagnostics: `http://127.0.0.1:${port}/diagnostics`
|
||||
},
|
||||
remoteUrlTemplates: {
|
||||
health: `http://127.0.0.1:${port}/health`,
|
||||
meta: `http://127.0.0.1:${port}/meta`,
|
||||
diagnostics: `http://127.0.0.1:${port}/diagnostics`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user