Ferndiagnose: Allowlist/Port/Modus im Backup mitsichern (Token bleibt pro Server)
Neuer Schalter "Ferndiagnose-Einstellungen mitsichern" (Einstellungen, Default aus,
spiegelt "Download-Liste mitsichern"). Damit reisen die wiederkehrenden, server-
unabhaengigen MCP-Einstellungen im Backup-Export/Import mit, statt sie auf jedem der
5-6 Server neu einzutippen.
Mitgesichert wird NUR was identisch ueber alle Server ist und kein Geheimnis/keine
Identitaet traegt: Allowlist, Port, Freigabemodus (lokal/Netzwerk). Bewusst NICHT
mitgesichert:
- Token: wird pro Server automatisch frisch erzeugt (nie vom Nutzer getippt → kein
Tipp-Aufwand gespart). Der Backup-Schluessel ist eine fest verdrahtete Konstante →
ein geleaktes .mdd duerfte sonst Lesezugriff auf ALLE Server geben. Backstop: ohne
Token ist ein frisch importierter Server inert (checkAuth weist alles ab), bis der
Nutzer einmal "Aktivieren" drueckt und ein Token erzeugt — keine stille Freigabe.
- Oeffentliche Adresse + Name: pro Server verschieden (sonst zeigte der erzeugte
Verbindungscode des Zielservers auf den falschen Host).
Mechanik:
- mcpRemote als eigene Top-Level-Sektion im BackupPayload (NICHT in settings —
normalizeSettings ist ein Whitelist-Rebuild und wuerde unbekannte Keys verwerfen).
Form {allowlist, port, hostMode} erzwingt die Policy strukturell (kein Token-Feld).
Kein version-Bump (Import ist lenient, additiv vor-/rueckwaertskompatibel).
- Export sammelt aus getDebugServerRuntimeStatus()+getDebugAllowlist(), nur wenn der
Schalter an ist; liest bewusst KEIN Token, KEINE Remote-Meta.
- Import wendet die Sektion NUR im Settings-only-Zweig an (writeDebugServerConfig +
restartDebugServer, damit der laufende Server die Allowlist sofort uebernimmt). Im
Full-Backup-Zweig nicht — die App relauncht dort, der Boot liest die Dateien neu.
- Sicherheitsguard: Netzwerk-Modus mit LEERER Allowlist bindet local (127.0.0.1) statt
0.0.0.0 — nie stille Freigabe aus korrupten Daten.
- backupIncludeMcp in AppSettings + defaultSettings + normalizeSettings (sonst wuerde
der Schalter beim Speichern verworfen).
Tests (backup-mcp.test.ts, 11): Export-Gating + Policy-Guard (Sektion traegt nur
allowlist/port/hostMode), resolveMcpRemoteRestore-Matrix (hostMode-Mapping, Port-
Grenzen, Allowlist-Filter, Netzwerk-ohne-Allowlist→local, null bei fehlend), und der
load-bearing Live-Round-Trip: Export → Resolve → Apply → restartDebugServer →
getDebugAllowlist()/Status spiegeln die importierten Werte (beweist dass der Restart
feuert), Token unangetastet, kein debug_remote.json. Volle Suite 917 gruen, tsc=6.
This commit is contained in:
@@ -45,7 +45,7 @@ import { runInstallWithResume } from "./update-install-flow";
|
||||
import { rotateDebugToken, startDebugServer, stopDebugServer, restartDebugServer, getDebugServerRuntimeStatus, getActiveDebugToken, getDebugAllowlist, writeDebugServerConfig, clearDebugToken } from "./debug-server";
|
||||
import { encodeConnectionCode, loadRemoteMeta, saveRemoteMeta } from "./connection-code";
|
||||
import { encryptBackup, decryptBackup } from "./backup-crypto";
|
||||
import { buildBackupPayload, planBackupImport } from "./backup-payload";
|
||||
import { buildBackupPayload, planBackupImport, resolveMcpRemoteRestore, BackupMcpRemote } from "./backup-payload";
|
||||
import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "./audit-log";
|
||||
import { initAccountRotationLog, shutdownAccountRotationLog } from "./account-rotation-log";
|
||||
import { initConversionLog, shutdownConversionLog } from "./conversion-trace";
|
||||
@@ -372,6 +372,20 @@ export class AppController {
|
||||
return this.getRemoteDiagnostics();
|
||||
}
|
||||
|
||||
private restoreMcpRemoteFromBackup(section: unknown): void {
|
||||
const restore = resolveMcpRemoteRestore(section);
|
||||
if (!restore) {
|
||||
return;
|
||||
}
|
||||
writeDebugServerConfig({ host: restore.host, port: restore.port, allowlist: restore.allowlist });
|
||||
void restartDebugServer().catch(() => {});
|
||||
this.audit("INFO", "Ferndiagnose-Einstellungen aus Backup wiederhergestellt", {
|
||||
port: restore.port ?? null,
|
||||
allowlistCount: restore.allowlist?.length ?? 0,
|
||||
host: restore.host ?? "unveraendert"
|
||||
});
|
||||
}
|
||||
|
||||
public getDebugSetupCheck(): DebugSetupCheckResult {
|
||||
return getDebugSetupCheck(this.storagePaths.baseDir);
|
||||
}
|
||||
@@ -724,13 +738,22 @@ public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
|
||||
}
|
||||
|
||||
public exportBackup(): Buffer {
|
||||
const includeDownloads = Boolean(this.settings.backupIncludeDownloads);
|
||||
let mcpRemote: BackupMcpRemote | undefined;
|
||||
if (Boolean(this.settings.backupIncludeMcp)) {
|
||||
const status = getDebugServerRuntimeStatus();
|
||||
mcpRemote = {
|
||||
allowlist: getDebugAllowlist(),
|
||||
port: status.port,
|
||||
hostMode: status.host === "0.0.0.0" ? "network" : "local"
|
||||
};
|
||||
}
|
||||
const payloadObj = buildBackupPayload({
|
||||
settings: { ...this.settings },
|
||||
appVersion: APP_VERSION,
|
||||
exportedAt: new Date().toISOString(),
|
||||
session: this.manager.getSession(),
|
||||
history: loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode, this.historyLimits())
|
||||
history: loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode, this.historyLimits()),
|
||||
mcpRemote
|
||||
});
|
||||
this.audit("INFO", "Backup exportiert", {
|
||||
kind: payloadObj.kind,
|
||||
@@ -804,6 +827,7 @@ public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
|
||||
this.settings = restoredSettings;
|
||||
saveSettings(this.storagePaths, this.settings);
|
||||
this.manager.setSettings(this.settings, { suppressRetroactiveCleanup: true });
|
||||
this.restoreMcpRemoteFromBackup(parsed.mcpRemote);
|
||||
this.audit("INFO", "Backup importiert (nur Einstellungen)", {
|
||||
accountSummary: buildAccountSummary(this.settings)
|
||||
});
|
||||
|
||||
@@ -2,6 +2,12 @@ import type { AppSettings, SessionState, HistoryEntry } from "../shared/types";
|
||||
|
||||
export type BackupKind = "full" | "settings-only";
|
||||
|
||||
export interface BackupMcpRemote {
|
||||
allowlist: string[];
|
||||
port: number;
|
||||
hostMode: "local" | "network";
|
||||
}
|
||||
|
||||
export interface BackupPayload {
|
||||
version: 2;
|
||||
kind: BackupKind;
|
||||
@@ -10,6 +16,7 @@ export interface BackupPayload {
|
||||
settings: AppSettings;
|
||||
session?: SessionState;
|
||||
history?: HistoryEntry[];
|
||||
mcpRemote?: BackupMcpRemote;
|
||||
}
|
||||
|
||||
export interface BuildBackupInput {
|
||||
@@ -19,6 +26,7 @@ export interface BuildBackupInput {
|
||||
/** Only bundled when includeDownloads is true. */
|
||||
session: SessionState;
|
||||
history: HistoryEntry[];
|
||||
mcpRemote?: BackupMcpRemote;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,9 +48,39 @@ export function buildBackupPayload(input: BuildBackupInput): BackupPayload {
|
||||
base.session = input.session;
|
||||
base.history = input.history;
|
||||
}
|
||||
if (Boolean(input.settings.backupIncludeMcp) && input.mcpRemote) {
|
||||
base.mcpRemote = input.mcpRemote;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
export interface McpRemoteRestore {
|
||||
host?: "127.0.0.1" | "0.0.0.0";
|
||||
port?: number;
|
||||
allowlist?: string[];
|
||||
}
|
||||
|
||||
export function resolveMcpRemoteRestore(section: unknown): McpRemoteRestore | 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 };
|
||||
}
|
||||
|
||||
export interface ImportPlan {
|
||||
valid: boolean;
|
||||
/** Restore the download list (session + history) and relaunch. */
|
||||
|
||||
@@ -109,6 +109,7 @@ export function defaultSettings(): AppSettings {
|
||||
hideExtractedItems: true,
|
||||
confirmDeleteSelection: true,
|
||||
backupIncludeDownloads: false,
|
||||
backupIncludeMcp: false,
|
||||
notifyUrl: "",
|
||||
notifyMention: "",
|
||||
notifyOnPackageCompleted: false,
|
||||
|
||||
@@ -461,6 +461,7 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
||||
hideExtractedItems: settings.hideExtractedItems !== undefined ? Boolean(settings.hideExtractedItems) : defaults.hideExtractedItems,
|
||||
confirmDeleteSelection: settings.confirmDeleteSelection !== undefined ? Boolean(settings.confirmDeleteSelection) : defaults.confirmDeleteSelection,
|
||||
backupIncludeDownloads: settings.backupIncludeDownloads !== undefined ? Boolean(settings.backupIncludeDownloads) : defaults.backupIncludeDownloads,
|
||||
backupIncludeMcp: settings.backupIncludeMcp !== undefined ? Boolean(settings.backupIncludeMcp) : defaults.backupIncludeMcp,
|
||||
notifyUrl: asText(settings.notifyUrl) || defaults.notifyUrl,
|
||||
notifyMention: asText(settings.notifyMention) || defaults.notifyMention,
|
||||
notifyOnPackageCompleted: settings.notifyOnPackageCompleted !== undefined ? Boolean(settings.notifyOnPackageCompleted) : defaults.notifyOnPackageCompleted,
|
||||
|
||||
@@ -856,7 +856,7 @@ const emptySnapshot = (): UiSnapshot => ({
|
||||
autoReconnect: false, reconnectWaitSeconds: 45, completedCleanupPolicy: "never",
|
||||
maxParallel: 4, maxParallelExtract: 2, extractCpuPriority: "high", retryLimit: 0, speedLimitEnabled: false, speedLimitKbps: 0, speedLimitMode: "global",
|
||||
updateRepo: "", autoUpdateCheck: true, clipboardWatch: false, minimizeToTray: false,
|
||||
theme: "dark", collapseNewPackages: true, historyRetentionMode: "permanent", historyMaxEntries: 500, historyMaxAgeDays: 0, autoSortPackagesByProgress: true, autoSkipExtracted: false, hideExtractedItems: true, confirmDeleteSelection: true, backupIncludeDownloads: false,
|
||||
theme: "dark", collapseNewPackages: true, historyRetentionMode: "permanent", historyMaxEntries: 500, historyMaxAgeDays: 0, autoSortPackagesByProgress: true, autoSkipExtracted: false, hideExtractedItems: true, confirmDeleteSelection: true, backupIncludeDownloads: false, backupIncludeMcp: false,
|
||||
notifyUrl: "", notifyMention: "", notifyOnPackageCompleted: false, notifyOnPackageFailed: false, notifyOnRunFinished: false,
|
||||
accountListShowDetailedDebridLinkKeys: false,
|
||||
bandwidthSchedules: [], totalDownloadedAllTime: 0, totalCompletedFilesAllTime: 0, totalRuntimeAllTimeMs: 0,
|
||||
@@ -5442,6 +5442,8 @@ export function App(): ReactElement {
|
||||
<div className="setting-hint">Sicherheitsabfrage vor dem Entfernen ausgewählter Einträge.</div>
|
||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.backupIncludeDownloads} onChange={(e) => setBool("backupIncludeDownloads", e.target.checked)} /> Download-Liste mitsichern</label>
|
||||
<div className="setting-hint">Sicherung enthält auch die Download-Liste; Standard: nur Einstellungen.</div>
|
||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.backupIncludeMcp} onChange={(e) => setBool("backupIncludeMcp", e.target.checked)} /> Ferndiagnose-Einstellungen mitsichern</label>
|
||||
<div className="setting-hint">Allowlist, Port und Freigabemodus (lokal/Netzwerk) reisen mit. Verbindungs-Token und eigene Adresse bleiben pro Server – nach dem Import einmal „Aktivieren" drücken.</div>
|
||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.theme === "light"} onChange={(e) => {
|
||||
const next = e.target.checked ? "light" : "dark";
|
||||
settingsDraftRevisionRef.current += 1;
|
||||
|
||||
@@ -134,6 +134,7 @@ export interface AppSettings {
|
||||
hideExtractedItems: boolean;
|
||||
confirmDeleteSelection: boolean;
|
||||
backupIncludeDownloads: boolean;
|
||||
backupIncludeMcp: boolean;
|
||||
notifyUrl: string;
|
||||
notifyMention: string;
|
||||
notifyOnPackageCompleted: boolean;
|
||||
|
||||
Reference in New Issue
Block a user