feat: erzwinge festen Proxy für API-Anfragen
This commit is contained in:
@@ -3,6 +3,7 @@ import { AllDebridHostInfo } from "../shared/types";
|
||||
import { UnrestrictedLink } from "./realdebrid";
|
||||
import { filenameFromUrl, sleep } from "./utils";
|
||||
import { ALLDEBRID_LOGIN_HOSTS, applyRemoteLoginSecurity, createRemoteLoginWebPreferences } from "./browser-security";
|
||||
import { configureElectronProxySession } from "./network-proxy";
|
||||
|
||||
const ALLDEBRID_BASE_URL = "https://alldebrid.com";
|
||||
const ALLDEBRID_LOGIN_URL = `${ALLDEBRID_BASE_URL}/register/?from=de`;
|
||||
@@ -211,9 +212,10 @@ export class AllDebridWebFallback {
|
||||
window.focus();
|
||||
}
|
||||
|
||||
public async getHostInfo(host: string): Promise<AllDebridHostInfo> {
|
||||
const currentSession = session.fromPartition(this.getPartition());
|
||||
const response = await currentSession.fetch(ALLDEBRID_STATUS_URL, {
|
||||
public async getHostInfo(host: string): Promise<AllDebridHostInfo> {
|
||||
const currentSession = session.fromPartition(this.getPartition());
|
||||
await configureElectronProxySession(currentSession);
|
||||
const response = await currentSession.fetch(ALLDEBRID_STATUS_URL, {
|
||||
headers: {
|
||||
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
Referer: ALLDEBRID_SERVICE_REFERER,
|
||||
@@ -285,9 +287,10 @@ export class AllDebridWebFallback {
|
||||
return run;
|
||||
}
|
||||
|
||||
private async ensureLoginWindow(): Promise<BrowserWindow> {
|
||||
const partition = this.getPartition();
|
||||
const existing = this.loginWindow;
|
||||
private async ensureLoginWindow(): Promise<BrowserWindow> {
|
||||
const partition = this.getPartition();
|
||||
await configureElectronProxySession(session.fromPartition(partition));
|
||||
const existing = this.loginWindow;
|
||||
if (existing && !existing.isDestroyed() && this.loginWindowPartition === partition) {
|
||||
return existing;
|
||||
}
|
||||
@@ -327,9 +330,10 @@ export class AllDebridWebFallback {
|
||||
body: URLSearchParams,
|
||||
referer: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<{ response: Response; text: string }> {
|
||||
const currentSession = session.fromPartition(this.getPartition());
|
||||
const response = await currentSession.fetch(url, {
|
||||
): Promise<{ response: Response; text: string }> {
|
||||
const currentSession = session.fromPartition(this.getPartition());
|
||||
await configureElectronProxySession(currentSession);
|
||||
const response = await currentSession.fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json, text/javascript, */*; q=0.01",
|
||||
|
||||
@@ -88,6 +88,7 @@ import { NotificationOutbox } from "./notification-outbox";
|
||||
import { sendNotification } from "./notify";
|
||||
import { DownloadHealthMonitor } from "./download-health-monitor";
|
||||
import { shouldDeferAutoResumeToDailyStart } from "./daily-start-scheduler";
|
||||
import { configureNetworkProxy, shutdownNetworkProxy } from "./network-proxy";
|
||||
|
||||
function sanitizeSettingsPatch(partial: Partial<AppSettings>): Partial<AppSettings> {
|
||||
const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined);
|
||||
@@ -168,6 +169,7 @@ export class AppController {
|
||||
this.logDirectory
|
||||
);
|
||||
}
|
||||
this.applyNetworkProxyConfiguration();
|
||||
this.initializeLogStorage();
|
||||
this.runHistoryLifecycleCleanup("Start", () => resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode));
|
||||
const loadResult = loadSessionWithStatus(this.storagePaths);
|
||||
@@ -613,6 +615,7 @@ export class AppController {
|
||||
this.overlayLiveUsageCounters(restoredSettings);
|
||||
saveSettings(this.storagePaths, restoredSettings);
|
||||
this.settings = restoredSettings;
|
||||
this.applyNetworkProxyConfiguration();
|
||||
runtimeApplied = true;
|
||||
this.manager.setSettings(this.settings, { settingsOnlyImport: true });
|
||||
this.manager.persistNowSync();
|
||||
@@ -622,6 +625,7 @@ export class AppController {
|
||||
this.rollbackImportPersistence(rollback, "fehlgeschlagenem Settings-Import");
|
||||
}
|
||||
this.settings = previousSettings;
|
||||
this.applyNetworkProxyConfiguration();
|
||||
if (runtimeApplied) {
|
||||
try {
|
||||
this.manager.setSettings(previousSettings, { settingsOnlyImport: true });
|
||||
@@ -688,6 +692,7 @@ export class AppController {
|
||||
throw error;
|
||||
}
|
||||
this.settings = nextSettings;
|
||||
this.applyNetworkProxyConfiguration();
|
||||
this.manager.setSettings(this.settings);
|
||||
this.audit("INFO", "Einstellungen aktualisiert", {
|
||||
changedKeys: Object.keys(sanitizedPatch),
|
||||
@@ -1492,6 +1497,7 @@ export class AppController {
|
||||
this.manager.skipShutdownPersist = true;
|
||||
this.manager.blockAllPersistence = true;
|
||||
this.settings = restoredSettings;
|
||||
this.applyNetworkProxyConfiguration();
|
||||
runtimeApplied = true;
|
||||
this.manager.setSettings(this.settings);
|
||||
this.manager.stop();
|
||||
@@ -1505,6 +1511,7 @@ export class AppController {
|
||||
this.manager.skipShutdownPersist = previousSkipShutdownPersist;
|
||||
this.manager.blockAllPersistence = previousBlockAllPersistence;
|
||||
this.settings = previousSettings;
|
||||
this.applyNetworkProxyConfiguration();
|
||||
if (runtimeApplied) {
|
||||
try {
|
||||
this.manager.setSettings(previousSettings, { settingsOnlyImport: true });
|
||||
@@ -1617,6 +1624,7 @@ export class AppController {
|
||||
this.pendingRealDebridWebAccountIds.clear();
|
||||
this.allDebridWebFallback.dispose();
|
||||
this.bestDebridWebFallback.dispose();
|
||||
await shutdownNetworkProxy();
|
||||
if (this.settings.historyRetentionMode === "session") {
|
||||
this.runHistoryLifecycleCleanup("Beenden", () => clearHistory(this.storagePaths));
|
||||
}
|
||||
@@ -1629,6 +1637,15 @@ export class AppController {
|
||||
logger.info("App beendet");
|
||||
}
|
||||
|
||||
private applyNetworkProxyConfiguration(): void {
|
||||
const state = configureNetworkProxy(this.settings);
|
||||
if (state.status === "active") {
|
||||
logger.info(`Proxy-only aktiv: fester API-Proxy ${state.selectedIndex}/${state.proxyCount}`);
|
||||
} else if (state.status === "blocked") {
|
||||
logger.warn(`Proxy-only blockiert Netzwerkanfragen: ${state.reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async waitForShutdownTask(task: Promise<unknown>, deadlineAt: number): Promise<void> {
|
||||
const remainingMs = Math.max(0, deadlineAt - Date.now());
|
||||
if (remainingMs <= 0) {
|
||||
|
||||
@@ -2,7 +2,8 @@ import fs from "node:fs";
|
||||
import { session, type Session } from "electron";
|
||||
import { UnrestrictedLink } from "./realdebrid";
|
||||
import { filenameFromUrl, sleep } from "./utils";
|
||||
import { logger } from "./logger";
|
||||
import { logger } from "./logger";
|
||||
import { configureElectronProxySession } from "./network-proxy";
|
||||
|
||||
const BESTDEBRID_BASE_URL = "https://bestdebrid.com";
|
||||
const BESTDEBRID_DOWNLOADER_URL = `${BESTDEBRID_BASE_URL}/en/downloader/`;
|
||||
@@ -178,8 +179,9 @@ export class BestDebridWebFallback {
|
||||
throw new Error("BestDebrid: Cookie-Datei enthält keinen Login-Cookie. Bitte nach dem Login erneut exportieren.");
|
||||
}
|
||||
|
||||
const currentSession = session.fromPartition(this.getPartition());
|
||||
await this.clearPartitionState(currentSession);
|
||||
const currentSession = session.fromPartition(this.getPartition());
|
||||
await configureElectronProxySession(currentSession);
|
||||
await this.clearPartitionState(currentSession);
|
||||
|
||||
for (const cookie of bestDebridCookies) {
|
||||
const url = `https://${cookie.domain.replace(/^\./, "")}${cookie.path}`;
|
||||
@@ -244,9 +246,10 @@ export class BestDebridWebFallback {
|
||||
}
|
||||
|
||||
private async generate(link: string, signal?: AbortSignal): Promise<{ kind: "success"; value: UnrestrictedLink } | { kind: "login_required" }> {
|
||||
throwIfAborted(signal);
|
||||
const currentSession = session.fromPartition(this.getPartition());
|
||||
const response = await currentSession.fetch(BESTDEBRID_GENERATE_URL, {
|
||||
throwIfAborted(signal);
|
||||
const currentSession = session.fromPartition(this.getPartition());
|
||||
await configureElectronProxySession(currentSession);
|
||||
const response = await currentSession.fetch(BESTDEBRID_GENERATE_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json, text/javascript, */*; q=0.01",
|
||||
|
||||
@@ -108,6 +108,7 @@ export function defaultSettings(): AppSettings {
|
||||
speedLimitMode: "global",
|
||||
proxyDownloadEnabled: false,
|
||||
proxyListPath: "",
|
||||
proxyApiProxyIndex: 1,
|
||||
proxyConnectionsPerDownload: 16,
|
||||
updateRepo: DEFAULT_UPDATE_REPO,
|
||||
autoUpdateCheck: true,
|
||||
|
||||
@@ -11001,7 +11001,7 @@ export class DownloadManager extends EventEmitter {
|
||||
if (!proxyAttempted && attempt === 1 && existingBytes === 0 && this.settings.proxyDownloadEnabled) {
|
||||
proxyAttempted = true;
|
||||
if (this.settings.speedLimitEnabled) {
|
||||
logAttemptEvent("INFO", "Proxy-Segmentierung wegen aktivem Geschwindigkeitslimit übersprungen", {
|
||||
logAttemptEvent("INFO", "Proxy-Segmentierung wegen aktivem Geschwindigkeitslimit übersprungen; fester Proxy wird verwendet", {
|
||||
attempt
|
||||
});
|
||||
} else {
|
||||
@@ -11104,10 +11104,10 @@ export class DownloadManager extends EventEmitter {
|
||||
item.downloadedBytes = 0;
|
||||
item.progressPercent = 0;
|
||||
item.speedBps = 0;
|
||||
item.fullStatus = "Direktdownload wird gestartet";
|
||||
item.fullStatus = "Einzel-Proxy-Download wird gestartet";
|
||||
item.updatedAt = nowMs();
|
||||
this.emitState();
|
||||
logAttemptEvent("WARN", "Proxy-Segmentdownload nicht verwendet, Direktdownload folgt", {
|
||||
logAttemptEvent("WARN", "Proxy-Segmentdownload nicht verwendet, fester Einzel-Proxy folgt", {
|
||||
attempt,
|
||||
reason: proxyReasonLabels[proxyResult.reason]
|
||||
});
|
||||
|
||||
+9
-1
@@ -32,6 +32,7 @@ import {
|
||||
validateCollectorPersistenceState,
|
||||
validateCollectorTextPreparationRequest
|
||||
} from "../shared/collector";
|
||||
import { getProxyAuthentication } from "./network-proxy";
|
||||
|
||||
forceDarkNativeTheme(nativeTheme);
|
||||
|
||||
@@ -1271,7 +1272,14 @@ function formatRendererErrorReport(rawReport: unknown): string {
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
app.on("child-process-gone", (_event, details) => {
|
||||
app.on("login", (event, _webContents, _requestDetails, authInfo, callback) => {
|
||||
const credentials = getProxyAuthentication(authInfo);
|
||||
if (!credentials) return;
|
||||
event.preventDefault();
|
||||
callback(credentials.username, credentials.password);
|
||||
});
|
||||
|
||||
app.on("child-process-gone", (_event, details) => {
|
||||
const killed = details.reason !== "clean-exit" && details.reason !== "killed";
|
||||
const line = `Subprozess beendet: type=${details.type} reason=${details.reason} exitCode=${details.exitCode ?? "?"}${details.name ? ` name=${details.name}` : ""}${details.serviceName ? ` service=${details.serviceName}` : ""}`;
|
||||
if (killed) {
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import type { Session } from "electron";
|
||||
import { getGlobalDispatcher, ProxyAgent, setGlobalDispatcher, type Dispatcher } from "undici";
|
||||
import type { AppSettings } from "../shared/types";
|
||||
import { selectFixedProxy, type ProxyEndpoint } from "./proxy-segmented-download";
|
||||
|
||||
export type NetworkProxyState =
|
||||
| { status: "disabled" }
|
||||
| { status: "active"; selectedIndex: number; proxyCount: number }
|
||||
| { status: "blocked"; reason: "proxy_file_unavailable" | "no_valid_proxies" | "proxy_index_unavailable" };
|
||||
|
||||
interface ActiveProxyConfiguration {
|
||||
status: "active";
|
||||
proxy: ProxyEndpoint;
|
||||
selectedIndex: number;
|
||||
proxyCount: number;
|
||||
fingerprint: string;
|
||||
}
|
||||
|
||||
interface BlockedProxyConfiguration {
|
||||
status: "blocked";
|
||||
reason: "proxy_file_unavailable" | "no_valid_proxies" | "proxy_index_unavailable";
|
||||
fingerprint: string;
|
||||
}
|
||||
|
||||
type ProxyConfiguration = { status: "disabled"; fingerprint: "disabled" } | ActiveProxyConfiguration | BlockedProxyConfiguration;
|
||||
|
||||
const originalDispatcher = getGlobalDispatcher();
|
||||
const configuredSessions = new WeakMap<object, string>();
|
||||
const knownSessions = new Set<Session>();
|
||||
let currentConfiguration: ProxyConfiguration = { status: "disabled", fingerprint: "disabled" };
|
||||
let installedDispatcher: Dispatcher | null = null;
|
||||
|
||||
function blockedDispatcher(reason: string): Dispatcher {
|
||||
return {
|
||||
dispatch: (_options, handler) => {
|
||||
queueMicrotask(() => handler.onError?.(new Error(`proxy_only_blocked:${reason}`)));
|
||||
return true;
|
||||
},
|
||||
close: async () => {},
|
||||
destroy: async () => {}
|
||||
} as Dispatcher;
|
||||
}
|
||||
|
||||
function closeInstalledDispatcher(dispatcher: Dispatcher | null): void {
|
||||
if (!dispatcher || dispatcher === originalDispatcher) return;
|
||||
void dispatcher.close().catch(() => {});
|
||||
}
|
||||
|
||||
function installDispatcher(dispatcher: Dispatcher | null): void {
|
||||
const previous = installedDispatcher;
|
||||
installedDispatcher = dispatcher;
|
||||
setGlobalDispatcher(dispatcher || originalDispatcher);
|
||||
if (previous && previous !== dispatcher) closeInstalledDispatcher(previous);
|
||||
}
|
||||
|
||||
function refreshKnownElectronSessions(): void {
|
||||
for (const currentSession of knownSessions) {
|
||||
void configureElectronProxySession(currentSession).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
export function configureNetworkProxy(settings: Pick<AppSettings, "proxyDownloadEnabled" | "proxyListPath" | "proxyApiProxyIndex">): NetworkProxyState {
|
||||
if (!settings.proxyDownloadEnabled) {
|
||||
if (currentConfiguration.status !== "disabled") {
|
||||
currentConfiguration = { status: "disabled", fingerprint: "disabled" };
|
||||
installDispatcher(null);
|
||||
refreshKnownElectronSessions();
|
||||
}
|
||||
return { status: "disabled" };
|
||||
}
|
||||
|
||||
const selected = selectFixedProxy(settings.proxyListPath, settings.proxyApiProxyIndex);
|
||||
if (selected.status !== "ok") {
|
||||
const fingerprint = `blocked:${selected.status}`;
|
||||
if (currentConfiguration.fingerprint !== fingerprint) {
|
||||
currentConfiguration = { status: "blocked", reason: selected.status, fingerprint };
|
||||
installDispatcher(blockedDispatcher(selected.status));
|
||||
refreshKnownElectronSessions();
|
||||
}
|
||||
return { status: "blocked", reason: selected.status };
|
||||
}
|
||||
|
||||
const fingerprint = `${selected.proxy.url}\0${selected.proxy.authorization}\0${selected.selectedIndex}`;
|
||||
if (currentConfiguration.fingerprint !== fingerprint) {
|
||||
const dispatcher = new ProxyAgent({
|
||||
uri: selected.proxy.url,
|
||||
...(selected.proxy.authorization ? { token: selected.proxy.authorization } : {})
|
||||
});
|
||||
currentConfiguration = {
|
||||
status: "active",
|
||||
proxy: selected.proxy,
|
||||
selectedIndex: selected.selectedIndex,
|
||||
proxyCount: selected.proxyCount,
|
||||
fingerprint
|
||||
};
|
||||
installDispatcher(dispatcher);
|
||||
refreshKnownElectronSessions();
|
||||
}
|
||||
return { status: "active", selectedIndex: selected.selectedIndex, proxyCount: selected.proxyCount };
|
||||
}
|
||||
|
||||
export function getNetworkProxyState(): NetworkProxyState {
|
||||
if (currentConfiguration.status === "disabled") return { status: "disabled" };
|
||||
if (currentConfiguration.status === "blocked") {
|
||||
return { status: "blocked", reason: currentConfiguration.reason };
|
||||
}
|
||||
return {
|
||||
status: "active",
|
||||
selectedIndex: currentConfiguration.selectedIndex,
|
||||
proxyCount: currentConfiguration.proxyCount
|
||||
};
|
||||
}
|
||||
|
||||
export function getProxyAuthentication(authInfo: { isProxy: boolean; host: string; port: number }): { username: string; password: string } | null {
|
||||
if (!authInfo.isProxy || currentConfiguration.status !== "active") return null;
|
||||
const proxy = currentConfiguration.proxy;
|
||||
if (authInfo.host.toLowerCase() !== proxy.hostname.toLowerCase() || authInfo.port !== proxy.port) return null;
|
||||
if (!proxy.username && !proxy.password) return null;
|
||||
return { username: proxy.username, password: proxy.password };
|
||||
}
|
||||
|
||||
export async function configureElectronProxySession(currentSession: Session): Promise<void> {
|
||||
knownSessions.add(currentSession);
|
||||
const previousFingerprint = configuredSessions.get(currentSession);
|
||||
if (previousFingerprint === currentConfiguration.fingerprint) return;
|
||||
|
||||
try {
|
||||
if (currentConfiguration.status === "disabled") {
|
||||
if (previousFingerprint !== undefined) {
|
||||
await currentSession.setProxy({ mode: "direct" });
|
||||
await currentSession.closeAllConnections();
|
||||
}
|
||||
} else if (currentConfiguration.status === "active") {
|
||||
await currentSession.setProxy({
|
||||
mode: "fixed_servers",
|
||||
proxyRules: currentConfiguration.proxy.url.replace(/\/$/, "")
|
||||
});
|
||||
await currentSession.closeAllConnections();
|
||||
} else {
|
||||
await currentSession.setProxy({
|
||||
mode: "fixed_servers",
|
||||
proxyRules: "http://127.0.0.1:1"
|
||||
});
|
||||
await currentSession.closeAllConnections();
|
||||
}
|
||||
configuredSessions.set(currentSession, currentConfiguration.fingerprint);
|
||||
} catch {
|
||||
if (currentConfiguration.status !== "disabled") {
|
||||
await currentSession.setProxy({ mode: "fixed_servers", proxyRules: "http://127.0.0.1:1" }).catch(() => {});
|
||||
await currentSession.closeAllConnections().catch(() => {});
|
||||
}
|
||||
throw new Error("proxy_only_session_configuration_failed");
|
||||
}
|
||||
}
|
||||
|
||||
export async function shutdownNetworkProxy(): Promise<void> {
|
||||
const active = installedDispatcher;
|
||||
installedDispatcher = null;
|
||||
currentConfiguration = { status: "disabled", fingerprint: "disabled" };
|
||||
setGlobalDispatcher(originalDispatcher);
|
||||
knownSessions.clear();
|
||||
if (active && active !== originalDispatcher) {
|
||||
await active.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
@@ -15,10 +15,14 @@ const MAX_REDIRECTS = 5;
|
||||
const MAX_PROXY_FILE_BYTES = 8 * 1024 * 1024;
|
||||
const DISK_ERROR_CODES = new Set(["ENOSPC", "EDQUOT", "EACCES", "EPERM", "EROFS", "EIO", "ENODEV"]);
|
||||
|
||||
interface ProxyEndpoint {
|
||||
export interface ProxyEndpoint {
|
||||
id: number;
|
||||
url: string;
|
||||
authorization: string;
|
||||
username: string;
|
||||
password: string;
|
||||
hostname: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
interface CachedProxyFile {
|
||||
@@ -110,9 +114,11 @@ function parseProxyLine(rawLine: string, id: number): ProxyEndpoint | null {
|
||||
return null;
|
||||
}
|
||||
let authorization = "";
|
||||
let username = "";
|
||||
let password = "";
|
||||
if (parsed.username || parsed.password) {
|
||||
const username = decodeURIComponent(parsed.username);
|
||||
const password = decodeURIComponent(parsed.password);
|
||||
username = decodeURIComponent(parsed.username);
|
||||
password = decodeURIComponent(parsed.password);
|
||||
authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
||||
parsed.username = "";
|
||||
parsed.password = "";
|
||||
@@ -120,7 +126,15 @@ function parseProxyLine(rawLine: string, id: number): ProxyEndpoint | null {
|
||||
parsed.pathname = "";
|
||||
parsed.search = "";
|
||||
parsed.hash = "";
|
||||
return { id, url: parsed.toString(), authorization };
|
||||
return {
|
||||
id,
|
||||
url: parsed.toString(),
|
||||
authorization,
|
||||
username,
|
||||
password,
|
||||
hostname: parsed.hostname,
|
||||
port
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -133,6 +147,45 @@ export function parseProxyList(content: string): number {
|
||||
.reduce((count, line, index) => count + (parseProxyLine(line, index) ? 1 : 0), 0);
|
||||
}
|
||||
|
||||
function parseUniqueProxyEndpoints(content: string): ProxyEndpoint[] {
|
||||
const seen = new Set<string>();
|
||||
return content
|
||||
.replace(/^\uFEFF/, "")
|
||||
.split(/\r?\n/)
|
||||
.map((line, index) => parseProxyLine(line, index))
|
||||
.filter((proxy): proxy is ProxyEndpoint => {
|
||||
if (!proxy) return false;
|
||||
const key = `${proxy.url}\0${proxy.authorization}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export type FixedProxySelection =
|
||||
| { status: "ok"; proxy: ProxyEndpoint; selectedIndex: number; proxyCount: number }
|
||||
| { status: "proxy_file_unavailable" | "no_valid_proxies" | "proxy_index_unavailable" };
|
||||
|
||||
export function selectFixedProxy(proxyListPath: string, requestedIndex: number): FixedProxySelection {
|
||||
const filePath = String(proxyListPath || "").trim();
|
||||
if (!filePath) return { status: "proxy_file_unavailable" };
|
||||
try {
|
||||
const normalizedPath = path.resolve(filePath);
|
||||
const stat = fs.statSync(normalizedPath);
|
||||
if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_PROXY_FILE_BYTES) {
|
||||
return { status: stat.size <= 0 ? "no_valid_proxies" : "proxy_file_unavailable" };
|
||||
}
|
||||
const proxies = parseUniqueProxyEndpoints(fs.readFileSync(normalizedPath, "utf8"));
|
||||
if (proxies.length === 0) return { status: "no_valid_proxies" };
|
||||
const selectedIndex = Math.max(1, Math.floor(requestedIndex || 1));
|
||||
const proxy = proxies[selectedIndex - 1];
|
||||
if (!proxy) return { status: "proxy_index_unavailable" };
|
||||
return { status: "ok", proxy, selectedIndex, proxyCount: proxies.length };
|
||||
} catch {
|
||||
return { status: "proxy_file_unavailable" };
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProxyFile(filePath: string): Promise<
|
||||
{ status: "ok"; proxies: ProxyEndpoint[] }
|
||||
| { status: "unavailable" }
|
||||
@@ -151,18 +204,7 @@ async function loadProxyFile(filePath: string): Promise<
|
||||
: { status: "empty" };
|
||||
}
|
||||
const content = await fs.promises.readFile(normalizedPath, "utf8");
|
||||
const seen = new Set<string>();
|
||||
const proxies = content
|
||||
.replace(/^\uFEFF/, "")
|
||||
.split(/\r?\n/)
|
||||
.map((line, index) => parseProxyLine(line, index))
|
||||
.filter((proxy): proxy is ProxyEndpoint => {
|
||||
if (!proxy) return false;
|
||||
const key = `${proxy.url}\0${proxy.authorization}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
const proxies = parseUniqueProxyEndpoints(content);
|
||||
proxyFileCache.set(normalizedPath, { mtimeMs: stat.mtimeMs, size: stat.size, proxies });
|
||||
return proxies.length > 0 ? { status: "ok", proxies } : { status: "empty" };
|
||||
} catch {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { sleep } from "./utils";
|
||||
import { API_BASE_URL, REQUEST_RETRIES } from "./constants";
|
||||
import { applyRemoteLoginSecurity, createRemoteLoginWebPreferences, REALDEBRID_LOGIN_HOSTS } from "./browser-security";
|
||||
import { buildRealDebridWebGenerationScript, normalizeRealDebridWebGenerationResult } from "./realdebrid-web-page";
|
||||
import { configureElectronProxySession } from "./network-proxy";
|
||||
|
||||
const RD_BASE_URL = "https://real-debrid.com";
|
||||
const RD_LOGIN_URL = RD_BASE_URL;
|
||||
@@ -366,9 +367,10 @@ export class RealDebridWebFallback {
|
||||
return raceWithAbort(run, signal);
|
||||
}
|
||||
|
||||
private async ensureLoginWindow(): Promise<BrowserWindow> {
|
||||
const partition = this.getPartition();
|
||||
const existing = this.loginWindow;
|
||||
private async ensureLoginWindow(): Promise<BrowserWindow> {
|
||||
const partition = this.getPartition();
|
||||
await configureElectronProxySession(session.fromPartition(partition));
|
||||
const existing = this.loginWindow;
|
||||
if (existing && !existing.isDestroyed() && this.loginWindowPartition === partition) {
|
||||
return existing;
|
||||
}
|
||||
@@ -431,6 +433,7 @@ export class RealDebridWebFallback {
|
||||
this.throwIfDisposed();
|
||||
throwIfAborted(signal);
|
||||
const partition = this.getPartition();
|
||||
await configureElectronProxySession(session.fromPartition(partition));
|
||||
const existing = this.generatorWindow;
|
||||
if (existing && !existing.isDestroyed() && this.generatorWindowPartition === partition) {
|
||||
return existing;
|
||||
@@ -603,8 +606,9 @@ export class RealDebridWebFallback {
|
||||
}
|
||||
}
|
||||
|
||||
const currentSession = session.fromPartition(this.getPartition());
|
||||
const response = await currentSession.fetch(RD_APITOKEN_URL, {
|
||||
const currentSession = session.fromPartition(this.getPartition());
|
||||
await configureElectronProxySession(currentSession);
|
||||
const response = await currentSession.fetch(RD_APITOKEN_URL, {
|
||||
headers: {
|
||||
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
Referer: RD_BASE_URL + "/",
|
||||
|
||||
@@ -206,6 +206,7 @@ export function createRendererSettings(settings: AppSettings): RendererSettings
|
||||
speedLimitMode: settings.speedLimitMode,
|
||||
proxyDownloadEnabled: settings.proxyDownloadEnabled,
|
||||
proxyListPath: settings.proxyListPath,
|
||||
proxyApiProxyIndex: settings.proxyApiProxyIndex,
|
||||
proxyConnectionsPerDownload: settings.proxyConnectionsPerDownload,
|
||||
updateRepo: settings.updateRepo,
|
||||
autoUpdateCheck: settings.autoUpdateCheck,
|
||||
|
||||
@@ -614,6 +614,7 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
||||
speedLimitMode: settings.speedLimitMode,
|
||||
proxyDownloadEnabled: Boolean(settings.proxyDownloadEnabled),
|
||||
proxyListPath: asText(settings.proxyListPath).slice(0, 4096),
|
||||
proxyApiProxyIndex: clampNumber(settings.proxyApiProxyIndex, defaults.proxyApiProxyIndex, 1, 100000),
|
||||
proxyConnectionsPerDownload: clampNumber(settings.proxyConnectionsPerDownload, defaults.proxyConnectionsPerDownload, 2, 32),
|
||||
autoUpdateCheck: Boolean(settings.autoUpdateCheck),
|
||||
updateRepo: migrateUpdateRepo(asText(settings.updateRepo), defaults.updateRepo),
|
||||
|
||||
@@ -953,7 +953,7 @@ const emptySnapshot = (): UiSnapshot => ({
|
||||
removeSamplesAfterExtract: false, enableIntegrityCheck: true, autoResumeOnStart: true,
|
||||
autoReconnect: false, reconnectWaitSeconds: 45, completedCleanupPolicy: "never",
|
||||
maxParallel: 4, maxParallelExtract: 2, extractCpuPriority: "high", retryLimit: 0, speedLimitEnabled: false, speedLimitKbps: 0, speedLimitMode: "global",
|
||||
proxyDownloadEnabled: false, proxyListPath: "", proxyConnectionsPerDownload: 16,
|
||||
proxyDownloadEnabled: false, proxyListPath: "", proxyApiProxyIndex: 1, proxyConnectionsPerDownload: 16,
|
||||
updateRepo: "", autoUpdateCheck: true, clipboardWatch: false, minimizeToTray: false,
|
||||
theme: "dark", themePreference: "dark", logStorageLocation: "appdata", collapseNewPackages: true, animatePackageDisclosure: true, historyRetentionMode: "permanent", historyMaxEntries: 500, historyMaxAgeDays: 0, autoSortPackagesByProgress: false, autoSkipExtracted: false, hideExtractedItems: true, confirmDeleteSelection: true, backupIncludeDownloads: false, backupIncludeRemoteDiagnostics: false,
|
||||
notifyMention: "", notifyOnPackageCompleted: false, notifyOnPackageFailed: false, notifyOnRunFinished: false,
|
||||
@@ -6066,6 +6066,7 @@ export function App(): ReactElement {
|
||||
historyMaxAgeDays: [0, 3650, 0],
|
||||
maxParallelExtract: [1, 8, 2],
|
||||
reconnectWaitSeconds: [10, 600, 45],
|
||||
proxyApiProxyIndex: [1, 100000, 1],
|
||||
proxyConnectionsPerDownload: [2, 32, 16]
|
||||
};
|
||||
const bounds = numericLimits[fieldId as keyof RendererSettingsDraft];
|
||||
|
||||
@@ -424,15 +424,15 @@ export function buildSettingsFormViewModel({
|
||||
},
|
||||
{
|
||||
id: "speed-proxy",
|
||||
title: "Proxy-Download",
|
||||
description: "Teilt neue Downloads in Byte-Bereiche und lädt sie parallel über verschiedene HTTP-Proxys. Falls der Server keine Bereiche unterstützt oder die Proxys ausfallen, läuft der bestehende Direktdownload weiter.",
|
||||
title: "Proxy-only",
|
||||
description: "Leitet API-, Login- und Link-Anfragen über einen festen HTTP-Proxy. Downloads verwenden zusätzlich mehrere Proxys parallel. Bei aktivem Modus gibt es keinen direkten Rückfall über deine echte Verbindung.",
|
||||
fields: [
|
||||
{
|
||||
id: "proxyDownloadEnabled",
|
||||
kind: "switch",
|
||||
label: "Proxy-Download aktivieren",
|
||||
label: "Proxy-only aktivieren",
|
||||
value: settings.proxyDownloadEnabled,
|
||||
help: "Gilt für neue Dateien ab 8 MiB. Ein aktives Geschwindigkeitslimit verwendet weiterhin den Direktdownload."
|
||||
help: "Ist der feste API-Proxy nicht erreichbar oder ungültig, schlagen Netzwerkanfragen geschlossen fehl. Es wird nie ungefragt direkt verbunden."
|
||||
},
|
||||
{
|
||||
id: "proxyListPath",
|
||||
@@ -444,6 +444,16 @@ export function buildSettingsFormViewModel({
|
||||
disabled: !settings.proxyDownloadEnabled,
|
||||
help: "Unterstützt unter anderem Benutzer:Passwort@Host:Port. Zugangsdaten werden nicht protokolliert."
|
||||
},
|
||||
{
|
||||
id: "proxyApiProxyIndex",
|
||||
kind: "number",
|
||||
label: "Fester API-Proxy (gültiger Listeneintrag)",
|
||||
value: String(settings.proxyApiProxyIndex),
|
||||
min: 1,
|
||||
max: 100000,
|
||||
disabled: !settings.proxyDownloadEnabled,
|
||||
help: "Dieser 1-basierte Eintrag bleibt für API, Login und Link-Auflösung fest. Ist er offline, wird nicht automatisch ein anderer Proxy oder die direkte Verbindung verwendet."
|
||||
},
|
||||
{
|
||||
id: "proxyConnectionsPerDownload",
|
||||
kind: "number",
|
||||
@@ -452,7 +462,7 @@ export function buildSettingsFormViewModel({
|
||||
min: 2,
|
||||
max: 32,
|
||||
disabled: !settings.proxyDownloadEnabled,
|
||||
help: `Bei ${settings.maxParallel} gleichzeitigen Downloads sind bis zu ${settings.maxParallel * settings.proxyConnectionsPerDownload} Proxy-Verbindungen möglich.`
|
||||
help: `Bei ${settings.maxParallel} gleichzeitigen Downloads sind bis zu ${settings.maxParallel * settings.proxyConnectionsPerDownload} parallele Segmentverbindungen möglich. Kleine Dateien, Teil-Downloads und aktive Geschwindigkeitslimits laufen über den festen API-Proxy.`
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -142,6 +142,7 @@ export interface DailyStartSettings {
|
||||
export interface ProxyDownloadSettings {
|
||||
proxyDownloadEnabled: boolean;
|
||||
proxyListPath: string;
|
||||
proxyApiProxyIndex: number;
|
||||
proxyConnectionsPerDownload: number;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user