Sanitize account check IPC statuses
Centralize DebridAccountStatus sanitizing for direct account-check responses. The shared sanitizer collects stored and submitted credential variants, including raw, trimmed, URL-encoded, URL-decoded, full credential lines, and login:secret forms, then redacts credential-like query params, key-value echoes, authorization, cookie, API-key, token, password, secret, session, backup passphrase, and archive password text before any status DTO reaches the renderer. Apply the sanitizer to bulk checkDebridAccounts results, single checkAccountCredentials results, and account command credential checks before returning, throwing, or persisting statuses. Reuse the same sanitizer for renderer snapshots so status redaction stays on one path. Replace the Debrid-Link key popup copy action with a truthful non-secret masked-identity copy action and cover the regression so no renderer path copies key.token or reports a secret-copy success without a secret readback.
This commit is contained in:
@@ -0,0 +1,113 @@
|
|||||||
|
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||||
|
import { parseMegaDebridAccounts } from "../shared/mega-debrid-accounts";
|
||||||
|
import type { AppSettings, DebridAccountStatus } from "../shared/types";
|
||||||
|
|
||||||
|
const REDACTED = "[geschützt]";
|
||||||
|
|
||||||
|
function safeDecode(value: string): string {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(value);
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRedaction(values: Set<string>, value: unknown): void {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const raw = value;
|
||||||
|
const trimmed = value.trim();
|
||||||
|
for (const candidate of [raw, trimmed]) {
|
||||||
|
if (!candidate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
values.add(candidate);
|
||||||
|
const encoded = encodeURIComponent(candidate);
|
||||||
|
values.add(encoded);
|
||||||
|
values.add(encoded.replace(/%20/g, "+"));
|
||||||
|
const decoded = safeDecode(candidate);
|
||||||
|
if (decoded) {
|
||||||
|
values.add(decoded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInputString(input: unknown, key: string): string | undefined {
|
||||||
|
if (!input || typeof input !== "object") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const value = (input as Record<string, unknown>)[key];
|
||||||
|
return typeof value === "string" ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectAccountStatusRedactionValues(settings?: AppSettings, input?: unknown): string[] {
|
||||||
|
const values = new Set<string>();
|
||||||
|
if (settings) {
|
||||||
|
addRedaction(values, settings.token);
|
||||||
|
addRedaction(values, settings.megaPassword);
|
||||||
|
addRedaction(values, settings.megaCredentials);
|
||||||
|
addRedaction(values, settings.megaDebridApiCredentials);
|
||||||
|
addRedaction(values, settings.megaDebridWebCredentials);
|
||||||
|
addRedaction(values, settings.bestToken);
|
||||||
|
addRedaction(values, settings.allDebridToken);
|
||||||
|
addRedaction(values, settings.ddownloadPassword);
|
||||||
|
addRedaction(values, settings.oneFichierApiKey);
|
||||||
|
addRedaction(values, settings.debridLinkApiKeys);
|
||||||
|
addRedaction(values, settings.linkSnappyPassword);
|
||||||
|
addRedaction(values, settings.archivePasswordList);
|
||||||
|
addRedaction(values, settings.notifyUrl);
|
||||||
|
for (const raw of [settings.megaCredentials, settings.megaDebridApiCredentials, settings.megaDebridWebCredentials]) {
|
||||||
|
for (const account of parseMegaDebridAccounts(raw, settings.megaPassword)) {
|
||||||
|
addRedaction(values, account.password);
|
||||||
|
addRedaction(values, `${account.login}:${account.password}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const key of parseDebridLinkApiKeys(settings.debridLinkApiKeys)) {
|
||||||
|
addRedaction(values, key.token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const inputIdentity = getInputString(input, "identity");
|
||||||
|
const inputSecret = getInputString(input, "secret");
|
||||||
|
addRedaction(values, inputSecret);
|
||||||
|
if (inputIdentity && inputSecret) {
|
||||||
|
addRedaction(values, `${inputIdentity.trim()}:${inputSecret.trim()}`);
|
||||||
|
}
|
||||||
|
return [...values].filter(Boolean).sort((left, right) => right.length - left.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeAccountStatusText(value: string, redactions: readonly string[]): string {
|
||||||
|
let result = value;
|
||||||
|
result = result.replace(/\b(?:Authorization|Proxy-Authorization)\s*:\s*[^\r\n]+/gi, (match) => {
|
||||||
|
const name = match.slice(0, match.indexOf(":"));
|
||||||
|
return `${name}: ${REDACTED}`;
|
||||||
|
});
|
||||||
|
result = result.replace(/\b(?:Cookie|Set-Cookie)\s*:\s*[^\r\n]+/gi, (match) => {
|
||||||
|
const name = match.slice(0, match.indexOf(":"));
|
||||||
|
return `${name}: ${REDACTED}`;
|
||||||
|
});
|
||||||
|
result = result.replace(/\b(?:X-Api-Key|Api-Key|X-Auth-Token|X-Access-Token|Access-Token|Private-Token)\s*:\s*[^\r\n]+/gi, (match) => {
|
||||||
|
const name = match.slice(0, match.indexOf(":"));
|
||||||
|
return `${name}: ${REDACTED}`;
|
||||||
|
});
|
||||||
|
result = result.replace(/((?:[?&;\s,]|^)(?:password|pass|pwd|token|api[_-]?key|apikey|access[_-]?token|private[_-]?token|secret|session|cookie|(?:backup|archive)?[_ -]?passphrase|archive[_ -]?password)\s*[:=]\s*)[^&\s"')]+/gi, `$1${REDACTED}`);
|
||||||
|
for (const secret of redactions) {
|
||||||
|
result = result.split(secret).join(REDACTED);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeDebridAccountStatus(status: DebridAccountStatus, redactions: readonly string[]): DebridAccountStatus {
|
||||||
|
return {
|
||||||
|
...status,
|
||||||
|
accountId: sanitizeAccountStatusText(status.accountId, redactions),
|
||||||
|
label: sanitizeAccountStatusText(status.label, redactions),
|
||||||
|
maskedLogin: sanitizeAccountStatusText(status.maskedLogin, redactions),
|
||||||
|
email: status.email ? sanitizeAccountStatusText(status.email, redactions) : undefined,
|
||||||
|
message: sanitizeAccountStatusText(status.message, redactions)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeDebridAccountStatuses(statuses: readonly DebridAccountStatus[], redactions: readonly string[]): DebridAccountStatus[] {
|
||||||
|
return statuses.map((status) => sanitizeDebridAccountStatus(status, redactions));
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ import { parseMegaDebridAccounts } from "../shared/mega-debrid-accounts";
|
|||||||
import { getMegaDebridAccountsForMode } from "../shared/mega-debrid-accounts";
|
import { getMegaDebridAccountsForMode } from "../shared/mega-debrid-accounts";
|
||||||
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||||
import { applyAccountCommand } from "./account-commands";
|
import { applyAccountCommand } from "./account-commands";
|
||||||
|
import { collectAccountStatusRedactionValues, sanitizeDebridAccountStatus, sanitizeDebridAccountStatuses } from "./account-status-sanitizer";
|
||||||
import { createRendererState } from "./renderer-state";
|
import { createRendererState } from "./renderer-state";
|
||||||
import { parseCollectorInput } from "./link-parser";
|
import { parseCollectorInput } from "./link-parser";
|
||||||
import { configureLogger, getLogFilePath, logger } from "./logger";
|
import { configureLogger, getLogFilePath, logger } from "./logger";
|
||||||
@@ -484,6 +485,7 @@ export class AppController {
|
|||||||
public async executeAccountCommand(command: AccountCommand): Promise<AccountCommandResult> {
|
public async executeAccountCommand(command: AccountCommand): Promise<AccountCommandResult> {
|
||||||
const applied = applyAccountCommand(this.settings, command);
|
const applied = applyAccountCommand(this.settings, command);
|
||||||
let checkedStatus: DebridAccountStatus | null = null;
|
let checkedStatus: DebridAccountStatus | null = null;
|
||||||
|
const redactions = collectAccountStatusRedactionValues(applied.settings, command);
|
||||||
if (command.action !== "delete" && applied.response.accountId && (command.kind === "megadebrid-api" || command.kind === "megadebrid-web")) {
|
if (command.action !== "delete" && applied.response.accountId && (command.kind === "megadebrid-api" || command.kind === "megadebrid-web")) {
|
||||||
const mode = command.kind === "megadebrid-web" ? "web" : "api";
|
const mode = command.kind === "megadebrid-web" ? "web" : "api";
|
||||||
const account = getMegaDebridAccountsForMode(applied.settings, mode).find((entry) => entry.id === applied.response.accountId);
|
const account = getMegaDebridAccountsForMode(applied.settings, mode).find((entry) => entry.id === applied.response.accountId);
|
||||||
@@ -495,10 +497,11 @@ export class AppController {
|
|||||||
if (!key) throw new Error("Account-Payload ist ungültig");
|
if (!key) throw new Error("Account-Payload ist ungültig");
|
||||||
checkedStatus = await checkDebridLinkKey(key);
|
checkedStatus = await checkDebridLinkKey(key);
|
||||||
}
|
}
|
||||||
|
if (checkedStatus) {
|
||||||
|
checkedStatus = sanitizeDebridAccountStatus(checkedStatus, redactions);
|
||||||
|
}
|
||||||
if (checkedStatus && !checkedStatus.valid) {
|
if (checkedStatus && !checkedStatus.valid) {
|
||||||
const submittedSecret = "secret" in command ? command.secret : undefined;
|
throw new Error(checkedStatus.message || "Zugangsdaten ungültig");
|
||||||
const safeMessage = submittedSecret ? checkedStatus.message.split(submittedSecret).join("[geschützt]") : checkedStatus.message;
|
|
||||||
throw new Error(safeMessage || "Zugangsdaten ungültig");
|
|
||||||
}
|
}
|
||||||
this.updateSettings(applied.settings);
|
this.updateSettings(applied.settings);
|
||||||
if (checkedStatus) this.manager.applyDebridAccountStatuses([checkedStatus]);
|
if (checkedStatus) this.manager.applyDebridAccountStatuses([checkedStatus]);
|
||||||
@@ -508,6 +511,7 @@ export class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async checkAccountCredentials(input: AccountCredentialCheckInput): Promise<DebridAccountStatus> {
|
public async checkAccountCredentials(input: AccountCredentialCheckInput): Promise<DebridAccountStatus> {
|
||||||
|
const redactions = collectAccountStatusRedactionValues(this.settings, input);
|
||||||
if (input.kind === "megadebrid-api" || input.kind === "megadebrid-web") {
|
if (input.kind === "megadebrid-api" || input.kind === "megadebrid-web") {
|
||||||
const mode = input.kind === "megadebrid-web" ? "web" : "api";
|
const mode = input.kind === "megadebrid-web" ? "web" : "api";
|
||||||
const account = input.identity?.trim() && input.secret
|
const account = input.identity?.trim() && input.secret
|
||||||
@@ -515,14 +519,14 @@ export class AppController {
|
|||||||
: getMegaDebridAccountsForMode(this.settings, mode).find((entry) => entry.id === input.accountId);
|
: getMegaDebridAccountsForMode(this.settings, mode).find((entry) => entry.id === input.accountId);
|
||||||
if (!account) throw new Error("Account-Payload ist ungültig");
|
if (!account) throw new Error("Account-Payload ist ungültig");
|
||||||
const status = await checkMegaDebridAccount(account);
|
const status = await checkMegaDebridAccount(account);
|
||||||
return { ...status, message: input.secret ? status.message.split(input.secret).join("[geschützt]") : status.message };
|
return sanitizeDebridAccountStatus(status, redactions);
|
||||||
}
|
}
|
||||||
const key = input.secret?.trim()
|
const key = input.secret?.trim()
|
||||||
? parseDebridLinkApiKeys(input.secret)[0]
|
? parseDebridLinkApiKeys(input.secret)[0]
|
||||||
: parseDebridLinkApiKeys(this.settings.debridLinkApiKeys).find((entry) => entry.id === input.accountId);
|
: parseDebridLinkApiKeys(this.settings.debridLinkApiKeys).find((entry) => entry.id === input.accountId);
|
||||||
if (!key) throw new Error("Account-Payload ist ungültig");
|
if (!key) throw new Error("Account-Payload ist ungültig");
|
||||||
const status = await checkDebridLinkKey(key);
|
const status = await checkDebridLinkKey(key);
|
||||||
return { ...status, message: input.secret ? status.message.split(input.secret).join("[geschützt]") : status.message };
|
return sanitizeDebridAccountStatus(status, redactions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public resetProviderDailyUsage(provider: DebridProvider): AppSettings {
|
public resetProviderDailyUsage(provider: DebridProvider): AppSettings {
|
||||||
@@ -586,7 +590,10 @@ export class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
|
public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
|
||||||
const statuses = await checkAllDebridAccounts(this.settings);
|
const statuses = sanitizeDebridAccountStatuses(
|
||||||
|
await checkAllDebridAccounts(this.settings),
|
||||||
|
collectAccountStatusRedactionValues(this.settings)
|
||||||
|
);
|
||||||
this.manager.applyDebridAccountStatuses(statuses);
|
this.manager.applyDebridAccountStatuses(statuses);
|
||||||
this.audit("INFO", "Debrid-Accounts geprueft", {
|
this.audit("INFO", "Debrid-Accounts geprueft", {
|
||||||
total: statuses.length,
|
total: statuses.length,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||||
import { getMegaDebridAccountsForMode, getMegaDebridDisabledAccountIdsForMode, parseMegaDebridAccounts } from "../shared/mega-debrid-accounts";
|
import { getMegaDebridAccountsForMode, getMegaDebridDisabledAccountIdsForMode } from "../shared/mega-debrid-accounts";
|
||||||
import type { AppSettings, DebridAccountStatus, DebridProvider, RendererAccount, RendererAccountKind, RendererSettings } from "../shared/types";
|
import type { AppSettings, DebridAccountStatus, DebridProvider, RendererAccount, RendererAccountKind, RendererSettings } from "../shared/types";
|
||||||
|
import { collectAccountStatusRedactionValues, sanitizeDebridAccountStatus } from "./account-status-sanitizer";
|
||||||
|
|
||||||
function maskValue(value: string, keepStart = 3, keepEnd = 3): string {
|
function maskValue(value: string, keepStart = 3, keepEnd = 3): string {
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
@@ -13,45 +14,11 @@ function maskValue(value: string, keepStart = 3, keepEnd = 3): string {
|
|||||||
return `${trimmed.slice(0, keepStart)}${"*".repeat(Math.max(4, trimmed.length - keepStart - keepEnd))}${trimmed.slice(-keepEnd)}`;
|
return `${trimmed.slice(0, keepStart)}${"*".repeat(Math.max(4, trimmed.length - keepStart - keepEnd))}${trimmed.slice(-keepEnd)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSecrets(settings: AppSettings): string[] {
|
function safeStatus(status: DebridAccountStatus | undefined, redactions: readonly string[]): DebridAccountStatus | null {
|
||||||
const megaSecrets = [settings.megaCredentials, settings.megaDebridApiCredentials, settings.megaDebridWebCredentials]
|
|
||||||
.flatMap((credentials) => parseMegaDebridAccounts(credentials, settings.megaPassword))
|
|
||||||
.map((account) => account.password);
|
|
||||||
const debridLinkSecrets = parseDebridLinkApiKeys(settings.debridLinkApiKeys).map((key) => key.token);
|
|
||||||
return [...new Set([
|
|
||||||
settings.token,
|
|
||||||
settings.megaPassword,
|
|
||||||
settings.megaCredentials,
|
|
||||||
settings.megaDebridApiCredentials,
|
|
||||||
settings.megaDebridWebCredentials,
|
|
||||||
...megaSecrets,
|
|
||||||
settings.bestToken,
|
|
||||||
settings.allDebridToken,
|
|
||||||
settings.ddownloadPassword,
|
|
||||||
settings.oneFichierApiKey,
|
|
||||||
settings.debridLinkApiKeys,
|
|
||||||
...debridLinkSecrets,
|
|
||||||
settings.linkSnappyPassword,
|
|
||||||
settings.archivePasswordList,
|
|
||||||
settings.notifyUrl
|
|
||||||
].map((value) => value.trim()).filter(Boolean))].sort((left, right) => right.length - left.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
function redact(value: string, secrets: readonly string[]): string {
|
|
||||||
return secrets.reduce((result, secret) => result.split(secret).join("[geschützt]"), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeStatus(status: DebridAccountStatus | undefined, secrets: readonly string[]): DebridAccountStatus | null {
|
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return sanitizeDebridAccountStatus(status, redactions);
|
||||||
...status,
|
|
||||||
label: redact(status.label, secrets),
|
|
||||||
maskedLogin: redact(status.maskedLogin, secrets),
|
|
||||||
email: status.email ? redact(status.email, secrets) : undefined,
|
|
||||||
message: redact(status.message, secrets)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function providerEnabled(settings: AppSettings, provider: DebridProvider): boolean {
|
function providerEnabled(settings: AppSettings, provider: DebridProvider): boolean {
|
||||||
@@ -83,7 +50,7 @@ function singleAccount(
|
|||||||
|
|
||||||
export function createRendererAccounts(settings: AppSettings): RendererAccount[] {
|
export function createRendererAccounts(settings: AppSettings): RendererAccount[] {
|
||||||
const accounts: RendererAccount[] = [];
|
const accounts: RendererAccount[] = [];
|
||||||
const secrets = getSecrets(settings);
|
const redactions = collectAccountStatusRedactionValues(settings);
|
||||||
if (settings.realDebridUseWebLogin || settings.token.trim()) {
|
if (settings.realDebridUseWebLogin || settings.token.trim()) {
|
||||||
accounts.push(singleAccount(
|
accounts.push(singleAccount(
|
||||||
settings,
|
settings,
|
||||||
@@ -111,7 +78,7 @@ export function createRendererAccounts(settings: AppSettings): RendererAccount[]
|
|||||||
dailyLimitBytes: settings.megaDebridAccountDailyLimitBytes[account.id] || 0,
|
dailyLimitBytes: settings.megaDebridAccountDailyLimitBytes[account.id] || 0,
|
||||||
dailyUsageBytes: settings.megaDebridAccountDailyUsageBytes[account.id] || 0,
|
dailyUsageBytes: settings.megaDebridAccountDailyUsageBytes[account.id] || 0,
|
||||||
totalUsageBytes: settings.megaDebridAccountTotalUsageBytes[account.id] || 0,
|
totalUsageBytes: settings.megaDebridAccountTotalUsageBytes[account.id] || 0,
|
||||||
status: safeStatus(settings.debridAccountStatuses[account.id], secrets)
|
status: safeStatus(settings.debridAccountStatuses[account.id], redactions)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,7 +120,7 @@ export function createRendererAccounts(settings: AppSettings): RendererAccount[]
|
|||||||
dailyLimitBytes: settings.debridLinkApiKeyDailyLimitBytes[key.id] || 0,
|
dailyLimitBytes: settings.debridLinkApiKeyDailyLimitBytes[key.id] || 0,
|
||||||
dailyUsageBytes: settings.debridLinkApiKeyDailyUsageBytes[key.id] || 0,
|
dailyUsageBytes: settings.debridLinkApiKeyDailyUsageBytes[key.id] || 0,
|
||||||
totalUsageBytes: settings.debridLinkApiKeyTotalUsageBytes[key.id] || 0,
|
totalUsageBytes: settings.debridLinkApiKeyTotalUsageBytes[key.id] || 0,
|
||||||
status: safeStatus(settings.debridAccountStatuses[key.id], secrets)
|
status: safeStatus(settings.debridAccountStatuses[key.id], redactions)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (settings.linkSnappyLogin.trim() && settings.linkSnappyPassword) {
|
if (settings.linkSnappyLogin.trim() && settings.linkSnappyPassword) {
|
||||||
@@ -164,6 +131,7 @@ export function createRendererAccounts(settings: AppSettings): RendererAccount[]
|
|||||||
|
|
||||||
export function createRendererSettings(settings: AppSettings): RendererSettings {
|
export function createRendererSettings(settings: AppSettings): RendererSettings {
|
||||||
const configuredProviders = [...new Set(createRendererAccounts(settings).map((account) => account.provider))];
|
const configuredProviders = [...new Set(createRendererAccounts(settings).map((account) => account.provider))];
|
||||||
|
const redactions = collectAccountStatusRedactionValues(settings);
|
||||||
return {
|
return {
|
||||||
language: settings.language,
|
language: settings.language,
|
||||||
realDebridUseWebLogin: settings.realDebridUseWebLogin,
|
realDebridUseWebLogin: settings.realDebridUseWebLogin,
|
||||||
@@ -250,7 +218,7 @@ export function createRendererSettings(settings: AppSettings): RendererSettings
|
|||||||
megaDebridAccountDailyLimitBytes: { ...settings.megaDebridAccountDailyLimitBytes },
|
megaDebridAccountDailyLimitBytes: { ...settings.megaDebridAccountDailyLimitBytes },
|
||||||
megaDebridAccountDailyUsageBytes: { ...settings.megaDebridAccountDailyUsageBytes },
|
megaDebridAccountDailyUsageBytes: { ...settings.megaDebridAccountDailyUsageBytes },
|
||||||
megaDebridAccountTotalUsageBytes: { ...settings.megaDebridAccountTotalUsageBytes },
|
megaDebridAccountTotalUsageBytes: { ...settings.megaDebridAccountTotalUsageBytes },
|
||||||
debridAccountStatuses: Object.fromEntries(Object.entries(settings.debridAccountStatuses).map(([id, status]) => [id, safeStatus(status, getSecrets(settings))])),
|
debridAccountStatuses: Object.fromEntries(Object.entries(settings.debridAccountStatuses).map(([id, status]) => [id, safeStatus(status, redactions)])),
|
||||||
providerDailyUsageDay: settings.providerDailyUsageDay,
|
providerDailyUsageDay: settings.providerDailyUsageDay,
|
||||||
scheduledStartEpochMs: settings.scheduledStartEpochMs
|
scheduledStartEpochMs: settings.scheduledStartEpochMs
|
||||||
} as RendererSettings;
|
} as RendererSettings;
|
||||||
|
|||||||
@@ -6141,13 +6141,13 @@ export function App(): ReactElement {
|
|||||||
<>
|
<>
|
||||||
<span className="col-key">{ki + 1}</span>
|
<span className="col-key">{ki + 1}</span>
|
||||||
<button
|
<button
|
||||||
aria-label={`${key.label} kopieren`}
|
aria-label={`${key.label} maskierte Kennung kopieren`}
|
||||||
className="col-masked link-popup-click"
|
className="col-masked link-popup-click"
|
||||||
type="button"
|
type="button"
|
||||||
title={`${key.masked}\nKlicken zum Kopieren`}
|
title={`${key.masked}\nMaskierte Kennung kopieren`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void navigator.clipboard.writeText(key.token)
|
void navigator.clipboard.writeText(key.masked)
|
||||||
.then(() => showToast(`${key.label} kopiert`, 1800))
|
.then(() => showToast("Maskierte Kennung kopiert", 1800))
|
||||||
.catch(() => showToast("Kopieren fehlgeschlagen", 2200));
|
.catch(() => showToast("Kopieren fehlgeschlagen", 2200));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { AppController } from "../src/main/app-controller";
|
||||||
|
import { defaultSettings } from "../src/main/constants";
|
||||||
|
import type { AppSettings, DebridAccountStatus } from "../src/shared/types";
|
||||||
|
|
||||||
|
vi.mock("electron", () => ({
|
||||||
|
app: { getPath: () => "C:\\MDD\\Test" },
|
||||||
|
BrowserWindow: class {},
|
||||||
|
clipboard: {},
|
||||||
|
dialog: {},
|
||||||
|
ipcMain: { handle: vi.fn(), on: vi.fn() },
|
||||||
|
Menu: { buildFromTemplate: vi.fn(), setApplicationMenu: vi.fn() },
|
||||||
|
safeStorage: { isEncryptionAvailable: () => false, encryptString: vi.fn(), decryptString: vi.fn() },
|
||||||
|
shell: {},
|
||||||
|
Tray: class {}
|
||||||
|
}));
|
||||||
|
|
||||||
|
function mockFetchOnce(status: number, body: unknown): void {
|
||||||
|
const text = typeof body === "string" ? body : JSON.stringify(body);
|
||||||
|
vi.stubGlobal("fetch", vi.fn(async () => ({
|
||||||
|
ok: status >= 200 && status < 300,
|
||||||
|
status,
|
||||||
|
text: async () => text
|
||||||
|
})) as unknown as typeof fetch);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createController(settings: AppSettings): AppController {
|
||||||
|
const controller = Object.create(AppController.prototype) as {
|
||||||
|
settings: AppSettings;
|
||||||
|
manager: { applyDebridAccountStatuses: ReturnType<typeof vi.fn> };
|
||||||
|
audit: ReturnType<typeof vi.fn>;
|
||||||
|
};
|
||||||
|
controller.settings = settings;
|
||||||
|
controller.manager = { applyDebridAccountStatuses: vi.fn() };
|
||||||
|
controller.audit = vi.fn();
|
||||||
|
return controller as unknown as AppController;
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectNoSecret(payload: unknown, secrets: readonly string[]): void {
|
||||||
|
const serialized = JSON.stringify(payload);
|
||||||
|
for (const secret of secrets) {
|
||||||
|
expect(serialized).not.toContain(secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("account-check IPC result sanitizing", () => {
|
||||||
|
it("sanitizes bulk check statuses before returning or persisting them", async () => {
|
||||||
|
const login = "bulk-status@example.test";
|
||||||
|
const secret = "bulk raw secret+ä?=1";
|
||||||
|
const encodedSecret = encodeURIComponent(secret);
|
||||||
|
const credentialLine = `${login}:${secret}`;
|
||||||
|
const settings = {
|
||||||
|
...defaultSettings(),
|
||||||
|
megaCredentials: credentialLine,
|
||||||
|
megaDebridApiCredentials: credentialLine,
|
||||||
|
megaDebridApiEnabled: true
|
||||||
|
};
|
||||||
|
const providerText = `Denied https://www.mega-debrid.eu/api.php?action=connectUser&login=${encodeURIComponent(login)}&password=${encodedSecret} Authorization: Bearer ${encodedSecret} raw ${secret} line ${credentialLine}`;
|
||||||
|
mockFetchOnce(200, { response_code: "error", response_text: providerText });
|
||||||
|
const controller = createController(settings);
|
||||||
|
|
||||||
|
const statuses = await controller.checkDebridAccounts();
|
||||||
|
|
||||||
|
expectNoSecret(statuses, [secret, encodedSecret, credentialLine]);
|
||||||
|
expectNoSecret((controller as unknown as { manager: { applyDebridAccountStatuses: ReturnType<typeof vi.fn> } }).manager.applyDebridAccountStatuses.mock.calls, [secret, encodedSecret, credentialLine]);
|
||||||
|
expect(statuses[0]?.message).toContain("[geschützt]");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sanitizes single credential checks with raw, encoded, header and query echoes", async () => {
|
||||||
|
const secret = "single raw token+ö?=2";
|
||||||
|
const encodedSecret = encodeURIComponent(secret);
|
||||||
|
const echoedHeaderSecret = "provider-header-token-95K";
|
||||||
|
const echoedPassphrase = "provider-backup-passphrase-54A";
|
||||||
|
const providerText = `Rejected https://debrid-link.com/api/v2/account/infos?access_token=${encodedSecret}&apiKey=${secret} Authorization: Bearer ${encodedSecret} Cookie: sid=${secret} X-Api-Key: ${echoedHeaderSecret}
|
||||||
|
Backup-Passphrase=${echoedPassphrase}`;
|
||||||
|
mockFetchOnce(200, { success: false, error: providerText });
|
||||||
|
const controller = createController(defaultSettings());
|
||||||
|
|
||||||
|
const status = await controller.checkAccountCredentials({
|
||||||
|
kind: "debridlink-api",
|
||||||
|
secret
|
||||||
|
});
|
||||||
|
|
||||||
|
expectNoSecret(status, [secret, encodedSecret, echoedHeaderSecret, echoedPassphrase]);
|
||||||
|
expect((status as DebridAccountStatus).message).toContain("[geschützt]");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,6 +13,9 @@ describe("desktop shell", () => {
|
|||||||
|
|
||||||
expect(source).not.toMatch(/<span[^>]*className="[^"]*link-popup-click/);
|
expect(source).not.toMatch(/<span[^>]*className="[^"]*link-popup-click/);
|
||||||
expect(source.match(/<button[^>]*className="[^"]*link-popup-click[^>]*type="button"/g)).toHaveLength(3);
|
expect(source.match(/<button[^>]*className="[^"]*link-popup-click[^>]*type="button"/g)).toHaveLength(3);
|
||||||
|
expect(source).not.toContain("navigator.clipboard.writeText(key.token)");
|
||||||
|
expect(source).toContain("navigator.clipboard.writeText(key.masked)");
|
||||||
|
expect(source).toContain("Maskierte Kennung kopiert");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("confirms before removing a collector tab", () => {
|
it("confirms before removing a collector tab", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user