feat: erzwinge festen Proxy für API-Anfragen

This commit is contained in:
Sucukdeluxe
2026-08-31 13:20:03 +02:00
parent 17ac99d373
commit a562f9162a
23 changed files with 530 additions and 64 deletions
+13 -8
View File
@@ -67,7 +67,7 @@ Diese Datei hält den verifizierten technischen Arbeitsstand fest. Sie enthält
- Provider-Reihenfolge, Hoster-Overrides, Kontostatus, Tageslimits, Cooldowns und optionale Fallbacks bestimmen das Routing. - Provider-Reihenfolge, Hoster-Overrides, Kontostatus, Tageslimits, Cooldowns und optionale Fallbacks bestimmen das Routing.
- Real-Debrid und Debrid-Link unterstützen Kontopools; Auswahl und Rotation berücksichtigen Aktivität, Fairness, Limits und Fehlerklassen. - Real-Debrid und Debrid-Link unterstützen Kontopools; Auswahl und Rotation berücksichtigen Aktivität, Fairness, Limits und Fehlerklassen.
- Item-Pipeline: Vorprüfung und Recovery, Provider-Auswahl, Unrestrict mit Timeout, sichere Zielpfadreservierung, Speicherplatzreservierung, HTTP-Download mit Range-Resume, optionale Integritätsprüfung, Abschluss und Paket-Postprocessing. - Item-Pipeline: Vorprüfung und Recovery, Provider-Auswahl, Unrestrict mit Timeout, sichere Zielpfadreservierung, Speicherplatzreservierung, HTTP-Download mit Range-Resume, optionale Integritätsprüfung, Abschluss und Paket-Postprocessing.
- Optionaler Proxy-Segmentdownload teilt neue Dateien ab 8 MiB in 2 bis 32 exakte HTTP-Range-Bereiche und lädt sie parallel über verschiedene authentifizierte HTTP-CONNECT-Proxys. Bestehende Teil-Dateien und aktive Geschwindigkeitslimits bleiben beim bisherigen Direktdownload; bei fehlender Range-Unterstützung, ungültiger Liste oder Proxy-/Segmentfehlern wird automatisch auf diesen Pfad zurückgefallen. - Der optionale Proxy-only-Modus leitet sämtliche Main-Prozess-API-, Link-Auflösungs- und normalen Download-Requests über einen fest ausgewählten authentifizierten HTTP-CONNECT-Proxy. Electron-Web-Logins und deren Session-Fetches erhalten denselben festen Proxy. Neue Dateien ab 8 MiB können zusätzlich in 2 bis 32 exakten HTTP-Range-Bereichen parallel über die gesamte Proxy-Liste geladen werden.
- Download-Retries unterscheiden Netzwerk-, Range-, Hoster-, Provider-, Konto-, Quota-, Disk- und permanente Linkfehler. Stop, Pause, Shutdown und Neustart besitzen getrennte Park- und Abbruchpfade. - Download-Retries unterscheiden Netzwerk-, Range-, Hoster-, Provider-, Konto-, Quota-, Disk- und permanente Linkfehler. Stop, Pause, Shutdown und Neustart besitzen getrennte Park- und Abbruchpfade.
- Die Queue priorisiert hoch vor normal vor niedrig, beachtet globale und providerbezogene Parallelitätsgrenzen und schützt sich mit Scheduler-Generation, Heartbeat und Stall-Watchdogs gegen veraltete Tasks. - Die Queue priorisiert hoch vor normal vor niedrig, beachtet globale und providerbezogene Parallelitätsgrenzen und schützt sich mit Scheduler-Generation, Heartbeat und Stall-Watchdogs gegen veraltete Tasks.
@@ -99,11 +99,14 @@ Diese Datei hält den verifizierten technischen Arbeitsstand fest. Sie enthält
## Aktuelle unveröffentlichte Änderung ## Aktuelle unveröffentlichte Änderung
- Unter „Geschwindigkeit“ kann der Proxy-Segmentdownload aktiviert, eine externe Proxy-Textdatei ausgewählt und die Zahl der Verbindungen pro Download zwischen 2 und 32 festgelegt werden. Bei fünf parallelen Downloads und 16 Segmenten sind damit bis zu 80 Proxy-Verbindungen möglich; `maxParallel` bleibt die vorhandene globale Downloadgrenze. - Unter „Geschwindigkeit“ kann Proxy-only aktiviert, eine externe Proxy-Textdatei ausgewählt, ein fester API-Proxy über den 1-basierten gültigen Listeneintrag festgelegt und die Zahl der Segmentverbindungen pro Download zwischen 2 und 32 gewählt werden. Bei fünf parallelen Downloads und 16 Segmenten sind damit bis zu 80 Segmentverbindungen möglich; `maxParallel` bleibt die vorhandene globale Downloadgrenze.
- Der feste Proxy übernimmt API-Aufrufe, Account-Prüfungen, Updates, Benachrichtigungen, Link-Auflösung, Web-Logins und alle Einzel-Proxy-Downloads. Ist die Liste nicht lesbar, leer oder der ausgewählte Eintrag nicht vorhanden, blockiert ein Fail-closed-Dispatcher sämtliche globalen Requests. Ist der feste Proxy offline, schlägt die Anfrage fehl; er wird nicht automatisch ersetzt und es gibt bei aktivem Modus keinen direkten Rückfall über die echte Verbindung.
- Electron-Provider-Sitzungen werden vor Login, Navigation und Session-Fetch auf denselben festen Proxy gesetzt. Authentifizierungsdaten werden ausschließlich beim passenden Proxy-Authentifizierungsereignis geliefert. Bereits bekannte Sitzungen werden bei einer Einstellungsänderung neu konfiguriert und bestehende Verbindungen geschlossen.
- Unterstützt werden insbesondere `Benutzer:Passwort@Host:Port`, Proxy-URLs, `Host:Port` und `Host:Port:Benutzer:Passwort`. Die Liste wird nur im Main-Prozess gelesen und anhand von Dateigröße und Änderungszeit gecacht. Proxy-Zugangsdaten werden nicht protokolliert, nicht an den Renderer übertragen und nicht in Git übernommen; lediglich der lokale Listenpfad ist eine normale Einstellung. - Unterstützt werden insbesondere `Benutzer:Passwort@Host:Port`, Proxy-URLs, `Host:Port` und `Host:Port:Benutzer:Passwort`. Die Liste wird nur im Main-Prozess gelesen und anhand von Dateigröße und Änderungszeit gecacht. Proxy-Zugangsdaten werden nicht protokolliert, nicht an den Renderer übertragen und nicht in Git übernommen; lediglich der lokale Listenpfad ist eine normale Einstellung.
- Jeder Proxy-Download prüft zuerst `Range: bytes=0-1`, übernimmt die autoritative HTTP-Gesamtgröße, reserviert eine zielnahe temporäre Datei und schreibt jedes Segment positionsgenau. Die Zwei-Byte-Probe umgeht ein reproduziertes Real-Debrid-Verhalten, bei dem nur `bytes=0-0` trotz 206-Header die gesamte Datei streamt. Segmente versuchen bei Verbindungs-, Authentifizierungs-, Timeout- oder Range-Fehlern andere Proxys. Erst nach vollständiger Größenprüfung und `datasync` wird die Datei atomar zum Ziel umbenannt. - Jeder Proxy-Download prüft zuerst `Range: bytes=0-1`, übernimmt die autoritative HTTP-Gesamtgröße, reserviert eine zielnahe temporäre Datei und schreibt jedes Segment positionsgenau. Die Zwei-Byte-Probe umgeht ein reproduziertes Real-Debrid-Verhalten, bei dem nur `bytes=0-0` trotz 206-Header die gesamte Datei streamt. Segmente versuchen bei Verbindungs-, Authentifizierungs-, Timeout- oder Range-Fehlern andere Proxys. Erst nach vollständiger Größenprüfung und `datasync` wird die Datei atomar zum Ziel umbenannt.
- Abbruch und Segmentfehler beenden alle zugehörigen Tunnel, entfernen die temporäre Datei und rechnen nur eindeutigen Segmentfortschritt zurück. Tatsächlich empfangener Providerverkehr bleibt für Statistik und Geschwindigkeitsmessung erhalten. Ein fehlgeschlagener Proxy-Versuch startet anschließend den unveränderten Direktdownload neu. - Abbruch und Segmentfehler beenden alle zugehörigen Tunnel, entfernen die temporäre Datei und rechnen nur eindeutigen Segmentfortschritt zurück. Tatsächlich empfangener Providerverkehr bleibt für Statistik und Geschwindigkeitsmessung erhalten. Ein fehlgeschlagener Segmentversuch wechselt anschließend auf den bestehenden Einzel-Downloadpfad, der bei Proxy-only weiterhin zwingend den festen Proxy verwendet.
- Der bestehende Range-Resume-Pfad für Teil-Dateien wurde nicht ersetzt. Ein aktives Geschwindigkeitslimit überspringt die Proxy-Segmentierung, damit das konfigurierte Limit nicht umgangen wird. - Der bestehende Range-Resume-Pfad für Teil-Dateien wurde nicht ersetzt. Ein aktives Geschwindigkeitslimit überspringt die Proxy-Segmentierung, damit das konfigurierte Limit nicht umgangen wird; auch dieser Einzel-Download läuft im Proxy-only-Modus über den festen Proxy.
- Für den globalen Node-Dispatcher wird `undici` exakt in Version `6.28.0` verwendet. Der Segment-Downloader behält `https-proxy-agent` für seine voneinander unabhängigen Tunnel. Der Produktions-Audit meldet für die neue direkte Undici-Abhängigkeit keine offenen Advisories.
- Ein Start-/Beenden-Fehler nach Real-Debrid-Web-Downloads wurde behoben. Der persistente unsichtbare Web-Generator blieb nach dem Schließen des Hauptfensters geöffnet, verhinderte dadurch `window-all-closed` und hielt den Single-Instance-Lock. Weitere Starts wurden als Zweitinstanz sofort beendet, während der alte `second-instance`-Handler ohne Hauptfenster nichts tat. - Ein Start-/Beenden-Fehler nach Real-Debrid-Web-Downloads wurde behoben. Der persistente unsichtbare Web-Generator blieb nach dem Schließen des Hauptfensters geöffnet, verhinderte dadurch `window-all-closed` und hielt den Single-Instance-Lock. Weitere Starts wurden als Zweitinstanz sofort beendet, während der alte `second-instance`-Handler ohne Hauptfenster nichts tat.
- Das Hauptfenster ist unter Windows nun ausdrücklich Besitzer des App-Lebenszyklus: Nach einem natürlichen, vom Renderer bestätigten Schließen wird der kontrollierte Shutdown auch bei verbleibenden Providerfenstern gestartet. Der normale `beforeunload`-Pfad bleibt erhalten, damit noch nicht übertragene Linksammler-Änderungen synchron gesichert werden können. - Das Hauptfenster ist unter Windows nun ausdrücklich Besitzer des App-Lebenszyklus: Nach einem natürlichen, vom Renderer bestätigten Schließen wird der kontrollierte Shutdown auch bei verbleibenden Providerfenstern gestartet. Der normale `beforeunload`-Pfad bleibt erhalten, damit noch nicht übertragene Linksammler-Änderungen synchron gesichert werden können.
- `second-instance` und `activate` stellen ein vorhandenes Hauptfenster wieder her oder erzeugen ein fehlendes neu. Ein Start während eines bereits laufenden Shutdowns plant genau einen Relaunch nach dem Prozessende ein. - `second-instance` und `activate` stellen ein vorhandenes Hauptfenster wieder her oder erzeugen ein fehlendes neu. Ein Start während eines bereits laufenden Shutdowns plant genau einen Relaunch nach dem Prozessende ein.
@@ -149,12 +152,13 @@ npm exec -- tsc --noEmit
## Verifizierungen vom 31. August 2026 ## Verifizierungen vom 31. August 2026
- Proxy-Parser gegen die bereitgestellte externe Liste geprüft: 1.000 von 1.000 Einträgen gültig; dabei wurden weder Proxy-Verbindungen aufgebaut noch Listeneinträge ausgegeben. - Proxy-Parser gegen die bereitgestellte externe Liste geprüft: 1.000 von 1.000 Einträgen gültig; dabei wurden weder Proxy-Verbindungen aufgebaut noch Listeneinträge ausgegeben.
- Proxy-Segmenttests: 5 von 5 erfolgreich. Abgedeckt sind vier parallele authentifizierte CONNECT-Proxys mit exakten Byte-Bereichen und identischem Dateiergebnis, Ersatz eines abgelehnten Proxys, Direkt-Fallback ohne Restdatei bei ignoriertem Range-Header sowie Abbruch mit Fortschritts-Rückrechnung und Temp-Bereinigung. - Proxy-Segmenttests: 6 von 6 erfolgreich. Abgedeckt sind die feste Auswahl nach gültigem 1-basiertem Listeneintrag, vier parallele authentifizierte CONNECT-Proxys mit exakten Byte-Bereichen und identischem Dateiergebnis, Ersatz eines abgelehnten Segment-Proxys, sauberer Einzel-Proxy-Fallback ohne Restdatei bei ignoriertem Range-Header sowie Abbruch mit Fortschritts-Rückrechnung und Temp-Bereinigung.
- Proxy-only-Routingtests: 3 von 3 erfolgreich. Ein normaler globaler API-`fetch` verwendete ausschließlich den ausgewählten zweiten authentifizierten CONNECT-Proxy; der erste Proxy blieb unbenutzt. Bei fehlender Liste wurde die Zieladresse nicht kontaktiert. Electron-Regeln enthielten keine Zugangsdaten, während die Authentifizierung nur für exakt passenden Proxy-Host und -Port bereitgestellt wurde.
- Reale Proxy-Prüfung mit einer 930.376.313 Byte großen Real-Debrid-RAR-Datei: 16 Verbindungen, vollständiger Segmenttransfer in rund 11,7 Sekunden mit 76,09 MiB/s mittlerem Nutzdurchsatz und zwischenzeitlich rund 94 MiB/s; Gesamtlauf einschließlich `datasync` und SHA-256 dauerte 13,19 Sekunden. Der zusätzliche Prüfverkehr betrug exakt 2 Byte. - Reale Proxy-Prüfung mit einer 930.376.313 Byte großen Real-Debrid-RAR-Datei: 16 Verbindungen, vollständiger Segmenttransfer in rund 11,7 Sekunden mit 76,09 MiB/s mittlerem Nutzdurchsatz und zwischenzeitlich rund 94 MiB/s; Gesamtlauf einschließlich `datasync` und SHA-256 dauerte 13,19 Sekunden. Der zusätzliche Prüfverkehr betrug exakt 2 Byte.
- Die reale Testdatei besitzt SHA-256 `f60f70a03cc49f544aab97d66254cb39801d7bbad827d1db935ef8b25c8b544f`, eine gültige RAR-Signatur und ein fehlerfrei lesbares Inhaltsverzeichnis. 32 verteilte 64-Byte-Stichproben an Anfang und Ende aller 16 Segmente stimmten mit frischen Range-Antworten über 32 verschiedene Listeneinträge überein. - Die reale Testdatei besitzt SHA-256 `f60f70a03cc49f544aab97d66254cb39801d7bbad827d1db935ef8b25c8b544f`, eine gültige RAR-Signatur und ein fehlerfrei lesbares Inhaltsverzeichnis. 32 verteilte 64-Byte-Stichproben an Anfang und Ende aller 16 Segmente stimmten mit frischen Range-Antworten über 32 verschiedene Listeneinträge überein.
- Settings-/Storage-Regressionen decken Aktivierung, Pfad, Wertebereich 2 bis 32, Dateiauswahl und die Darstellung von 80 möglichen Tunneln bei 5 × 16 ab. - Settings-/Storage-Regressionen decken Aktivierung, Pfad, Wertebereich 2 bis 32, Dateiauswahl und die Darstellung von 80 möglichen Tunneln bei 5 × 16 ab.
- Vollständiger Client-Lauf nach der Proxy-Änderung: 139 Testdateien erfolgreich, 1 JVM-Testdatei übersprungen; 2.668 Tests erfolgreich und 4 übersprungen. Nur die zwei bekannten Symlink-Fixtures in `public-release-metadata.test.ts` scheiterten vor ihrer Produktassertion mit Windows-`EPERM`. - Vollständiger Client-Lauf nach Proxy-only: 140 Testdateien erfolgreich, 1 JVM-Testdatei übersprungen; 2.672 Tests erfolgreich und 4 übersprungen. Nur die zwei bekannten Symlink-Fixtures in `public-release-metadata.test.ts` scheiterten vor ihrer Produktassertion mit Windows-`EPERM`.
- Finaler Wiederholungslauf ohne die zwei privilegierten Symlink-Fixtures: 139 Testdateien erfolgreich, 1 JVM-Testdatei übersprungen; 2.646 Tests erfolgreich und 4 übersprungen. - Finaler Wiederholungslauf ohne die nicht ausführbare Symlink-Testdatei: 140 Testdateien erfolgreich, 1 JVM-Testdatei übersprungen; 2.650 Tests erfolgreich und 4 übersprungen.
- Nach der Proxy-Änderung erfolgreich: TypeScript, Main-Build, Renderer-Build und Self-Check. Die bekannte Vite-Warnung zum nun rund 575 KiB großen Renderer-Chunk bleibt bestehen. - Nach der Proxy-Änderung erfolgreich: TypeScript, Main-Build, Renderer-Build und Self-Check. Die bekannte Vite-Warnung zum nun rund 575 KiB großen Renderer-Chunk bleibt bestehen.
- Root-Installation mit `npm ci`: erfolgreich. - Root-Installation mit `npm ci`: erfolgreich.
- Backup-API-Installation mit `npm ci --prefix services/backup-api`: erfolgreich. - Backup-API-Installation mit `npm ci --prefix services/backup-api`: erfolgreich.
@@ -185,7 +189,8 @@ npm exec -- tsc --noEmit
- `npm audit` meldet aktuell 36 Treffer: 2 niedrig, 5 mittel, 25 hoch und 4 kritisch. Direkte Treffer betreffen unter anderem Electron 31.7.7, Vite 6.4.1, Vitest 2.1.9, concurrently 9.2.1 und electron-builder 25.1.8. Ein Upgrade braucht einen eigenen getesteten Migrationsschritt; kein automatisches `npm audit fix` ausführen. - `npm audit` meldet aktuell 36 Treffer: 2 niedrig, 5 mittel, 25 hoch und 4 kritisch. Direkte Treffer betreffen unter anderem Electron 31.7.7, Vite 6.4.1, Vitest 2.1.9, concurrently 9.2.1 und electron-builder 25.1.8. Ein Upgrade braucht einen eigenen getesteten Migrationsschritt; kein automatisches `npm audit fix` ausführen.
- Der separate Backup-API-Baum hat aktuell 0 Audit-Treffer. - Der separate Backup-API-Baum hat aktuell 0 Audit-Treffer.
- Windows ohne Developer Mode beziehungsweise passende Berechtigung kann die zwei Symlink-basierten Public-Release-Metadaten-Tests nicht ausführen. - Windows ohne Developer Mode beziehungsweise passende Berechtigung kann die zwei Symlink-basierten Public-Release-Metadaten-Tests nicht ausführen.
- Proxy-Bündelung garantiert keine lineare Addition der Einzelgeschwindigkeiten. Ergebnis und Stabilität hängen unter anderem von Proxy-Latenz, Proxy-Bandbreite, Provider-/CDN-IP-Bindung, Range-Unterstützung, Zielserver-Limits, Dateigröße und lokaler Schreibgeschwindigkeit ab. Nach einem späten Segmentfehler kann vor dem Direkt-Fallback zusätzlicher Providerverkehr angefallen sein. - Proxy-Bündelung garantiert keine lineare Addition der Einzelgeschwindigkeiten. Ergebnis und Stabilität hängen unter anderem von Proxy-Latenz, Proxy-Bandbreite, Provider-/CDN-IP-Bindung, Range-Unterstützung, Zielserver-Limits, Dateigröße und lokaler Schreibgeschwindigkeit ab. Nach einem späten Segmentfehler kann vor dem festen Einzel-Proxy-Fallback zusätzlicher Providerverkehr angefallen sein.
- Der feste API-Proxy wird absichtlich nicht automatisch rotiert, damit API- und Login-Sitzungen eine stabile Ausgangs-IP behalten. Bei Ausfall muss ein anderer gültiger Listeneintrag ausgewählt werden; bis dahin schlägt Proxy-only geschlossen fehl.
- Die bereitgestellte Proxy-Liste wurde mit dem ausdrücklich bereitgestellten Real-Debrid-Testlink erfolgreich real geprüft. Die Messung gilt nur für diesen Link, diesen Zeitpunkt und einen parallelen Download; daraus folgt noch keine Garantie für fünf gleichzeitige Downloads, andere CDN-Knoten oder dauerhafte Proxy-Leistung. - Die bereitgestellte Proxy-Liste wurde mit dem ausdrücklich bereitgestellten Real-Debrid-Testlink erfolgreich real geprüft. Die Messung gilt nur für diesen Link, diesen Zeitpunkt und einen parallelen Download; daraus folgt noch keine Garantie für fünf gleichzeitige Downloads, andere CDN-Knoten oder dauerhafte Proxy-Leistung.
- Der Arbeitsordner enthält `&`; ohne PowerShell-7-Skriptshell können npm-`cmd`-Shims fehlschlagen. - Der Arbeitsordner enthält `&`; ohne PowerShell-7-Skriptshell können npm-`cmd`-Shims fehlschlagen.
- Es gibt keine `.github`-Workflows und damit keine serverseitige CI-Absicherung im GitHub-Repository. - Es gibt keine `.github`-Workflows und damit keine serverseitige CI-Absicherung im GitHub-Repository.
+14 -4
View File
@@ -8,11 +8,12 @@
"name": "multi-debrid-downloader", "name": "multi-debrid-downloader",
"version": "2.0.74", "version": "2.0.74",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"adm-zip": "0.6.0", "adm-zip": "0.6.0",
"https-proxy-agent": "7.0.6", "https-proxy-agent": "7.0.6",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"undici": "6.28.0",
"uuid": "11.1.1" "uuid": "11.1.1"
}, },
"devDependencies": { "devDependencies": {
@@ -7727,6 +7728,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/undici": {
"version": "6.28.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz",
"integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==",
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.16.0", "version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+5 -4
View File
@@ -23,11 +23,12 @@
"release:win": "npm run build && electron-builder --publish never --win nsis portable", "release:win": "npm run build && electron-builder --publish never --win nsis portable",
"verify:release": "node scripts/verify_public_release.mjs" "verify:release": "node scripts/verify_public_release.mjs"
}, },
"dependencies": { "dependencies": {
"adm-zip": "0.6.0", "adm-zip": "0.6.0",
"https-proxy-agent": "7.0.6", "https-proxy-agent": "7.0.6",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"undici": "6.28.0",
"uuid": "11.1.1" "uuid": "11.1.1"
}, },
"devDependencies": { "devDependencies": {
+13 -9
View File
@@ -3,6 +3,7 @@ import { AllDebridHostInfo } from "../shared/types";
import { UnrestrictedLink } from "./realdebrid"; import { UnrestrictedLink } from "./realdebrid";
import { filenameFromUrl, sleep } from "./utils"; import { filenameFromUrl, sleep } from "./utils";
import { ALLDEBRID_LOGIN_HOSTS, applyRemoteLoginSecurity, createRemoteLoginWebPreferences } from "./browser-security"; import { ALLDEBRID_LOGIN_HOSTS, applyRemoteLoginSecurity, createRemoteLoginWebPreferences } from "./browser-security";
import { configureElectronProxySession } from "./network-proxy";
const ALLDEBRID_BASE_URL = "https://alldebrid.com"; const ALLDEBRID_BASE_URL = "https://alldebrid.com";
const ALLDEBRID_LOGIN_URL = `${ALLDEBRID_BASE_URL}/register/?from=de`; const ALLDEBRID_LOGIN_URL = `${ALLDEBRID_BASE_URL}/register/?from=de`;
@@ -211,9 +212,10 @@ export class AllDebridWebFallback {
window.focus(); window.focus();
} }
public async getHostInfo(host: string): Promise<AllDebridHostInfo> { public async getHostInfo(host: string): Promise<AllDebridHostInfo> {
const currentSession = session.fromPartition(this.getPartition()); const currentSession = session.fromPartition(this.getPartition());
const response = await currentSession.fetch(ALLDEBRID_STATUS_URL, { await configureElectronProxySession(currentSession);
const response = await currentSession.fetch(ALLDEBRID_STATUS_URL, {
headers: { headers: {
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
Referer: ALLDEBRID_SERVICE_REFERER, Referer: ALLDEBRID_SERVICE_REFERER,
@@ -285,9 +287,10 @@ export class AllDebridWebFallback {
return run; return run;
} }
private async ensureLoginWindow(): Promise<BrowserWindow> { private async ensureLoginWindow(): Promise<BrowserWindow> {
const partition = this.getPartition(); const partition = this.getPartition();
const existing = this.loginWindow; await configureElectronProxySession(session.fromPartition(partition));
const existing = this.loginWindow;
if (existing && !existing.isDestroyed() && this.loginWindowPartition === partition) { if (existing && !existing.isDestroyed() && this.loginWindowPartition === partition) {
return existing; return existing;
} }
@@ -327,9 +330,10 @@ export class AllDebridWebFallback {
body: URLSearchParams, body: URLSearchParams,
referer: string, referer: string,
signal?: AbortSignal signal?: AbortSignal
): Promise<{ response: Response; text: string }> { ): Promise<{ response: Response; text: string }> {
const currentSession = session.fromPartition(this.getPartition()); const currentSession = session.fromPartition(this.getPartition());
const response = await currentSession.fetch(url, { await configureElectronProxySession(currentSession);
const response = await currentSession.fetch(url, {
method: "POST", method: "POST",
headers: { headers: {
Accept: "application/json, text/javascript, */*; q=0.01", Accept: "application/json, text/javascript, */*; q=0.01",
+17
View File
@@ -88,6 +88,7 @@ import { NotificationOutbox } from "./notification-outbox";
import { sendNotification } from "./notify"; import { sendNotification } from "./notify";
import { DownloadHealthMonitor } from "./download-health-monitor"; import { DownloadHealthMonitor } from "./download-health-monitor";
import { shouldDeferAutoResumeToDailyStart } from "./daily-start-scheduler"; import { shouldDeferAutoResumeToDailyStart } from "./daily-start-scheduler";
import { configureNetworkProxy, shutdownNetworkProxy } from "./network-proxy";
function sanitizeSettingsPatch(partial: Partial<AppSettings>): Partial<AppSettings> { function sanitizeSettingsPatch(partial: Partial<AppSettings>): Partial<AppSettings> {
const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined); const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined);
@@ -168,6 +169,7 @@ export class AppController {
this.logDirectory this.logDirectory
); );
} }
this.applyNetworkProxyConfiguration();
this.initializeLogStorage(); this.initializeLogStorage();
this.runHistoryLifecycleCleanup("Start", () => resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode)); this.runHistoryLifecycleCleanup("Start", () => resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode));
const loadResult = loadSessionWithStatus(this.storagePaths); const loadResult = loadSessionWithStatus(this.storagePaths);
@@ -613,6 +615,7 @@ export class AppController {
this.overlayLiveUsageCounters(restoredSettings); this.overlayLiveUsageCounters(restoredSettings);
saveSettings(this.storagePaths, restoredSettings); saveSettings(this.storagePaths, restoredSettings);
this.settings = restoredSettings; this.settings = restoredSettings;
this.applyNetworkProxyConfiguration();
runtimeApplied = true; runtimeApplied = true;
this.manager.setSettings(this.settings, { settingsOnlyImport: true }); this.manager.setSettings(this.settings, { settingsOnlyImport: true });
this.manager.persistNowSync(); this.manager.persistNowSync();
@@ -622,6 +625,7 @@ export class AppController {
this.rollbackImportPersistence(rollback, "fehlgeschlagenem Settings-Import"); this.rollbackImportPersistence(rollback, "fehlgeschlagenem Settings-Import");
} }
this.settings = previousSettings; this.settings = previousSettings;
this.applyNetworkProxyConfiguration();
if (runtimeApplied) { if (runtimeApplied) {
try { try {
this.manager.setSettings(previousSettings, { settingsOnlyImport: true }); this.manager.setSettings(previousSettings, { settingsOnlyImport: true });
@@ -688,6 +692,7 @@ export class AppController {
throw error; throw error;
} }
this.settings = nextSettings; this.settings = nextSettings;
this.applyNetworkProxyConfiguration();
this.manager.setSettings(this.settings); this.manager.setSettings(this.settings);
this.audit("INFO", "Einstellungen aktualisiert", { this.audit("INFO", "Einstellungen aktualisiert", {
changedKeys: Object.keys(sanitizedPatch), changedKeys: Object.keys(sanitizedPatch),
@@ -1492,6 +1497,7 @@ export class AppController {
this.manager.skipShutdownPersist = true; this.manager.skipShutdownPersist = true;
this.manager.blockAllPersistence = true; this.manager.blockAllPersistence = true;
this.settings = restoredSettings; this.settings = restoredSettings;
this.applyNetworkProxyConfiguration();
runtimeApplied = true; runtimeApplied = true;
this.manager.setSettings(this.settings); this.manager.setSettings(this.settings);
this.manager.stop(); this.manager.stop();
@@ -1505,6 +1511,7 @@ export class AppController {
this.manager.skipShutdownPersist = previousSkipShutdownPersist; this.manager.skipShutdownPersist = previousSkipShutdownPersist;
this.manager.blockAllPersistence = previousBlockAllPersistence; this.manager.blockAllPersistence = previousBlockAllPersistence;
this.settings = previousSettings; this.settings = previousSettings;
this.applyNetworkProxyConfiguration();
if (runtimeApplied) { if (runtimeApplied) {
try { try {
this.manager.setSettings(previousSettings, { settingsOnlyImport: true }); this.manager.setSettings(previousSettings, { settingsOnlyImport: true });
@@ -1617,6 +1624,7 @@ export class AppController {
this.pendingRealDebridWebAccountIds.clear(); this.pendingRealDebridWebAccountIds.clear();
this.allDebridWebFallback.dispose(); this.allDebridWebFallback.dispose();
this.bestDebridWebFallback.dispose(); this.bestDebridWebFallback.dispose();
await shutdownNetworkProxy();
if (this.settings.historyRetentionMode === "session") { if (this.settings.historyRetentionMode === "session") {
this.runHistoryLifecycleCleanup("Beenden", () => clearHistory(this.storagePaths)); this.runHistoryLifecycleCleanup("Beenden", () => clearHistory(this.storagePaths));
} }
@@ -1629,6 +1637,15 @@ export class AppController {
logger.info("App beendet"); 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> { private async waitForShutdownTask(task: Promise<unknown>, deadlineAt: number): Promise<void> {
const remainingMs = Math.max(0, deadlineAt - Date.now()); const remainingMs = Math.max(0, deadlineAt - Date.now());
if (remainingMs <= 0) { if (remainingMs <= 0) {
+9 -6
View File
@@ -2,7 +2,8 @@ import fs from "node:fs";
import { session, type Session } from "electron"; import { session, type Session } from "electron";
import { UnrestrictedLink } from "./realdebrid"; import { UnrestrictedLink } from "./realdebrid";
import { filenameFromUrl, sleep } from "./utils"; 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_BASE_URL = "https://bestdebrid.com";
const BESTDEBRID_DOWNLOADER_URL = `${BESTDEBRID_BASE_URL}/en/downloader/`; 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."); throw new Error("BestDebrid: Cookie-Datei enthält keinen Login-Cookie. Bitte nach dem Login erneut exportieren.");
} }
const currentSession = session.fromPartition(this.getPartition()); const currentSession = session.fromPartition(this.getPartition());
await this.clearPartitionState(currentSession); await configureElectronProxySession(currentSession);
await this.clearPartitionState(currentSession);
for (const cookie of bestDebridCookies) { for (const cookie of bestDebridCookies) {
const url = `https://${cookie.domain.replace(/^\./, "")}${cookie.path}`; 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" }> { private async generate(link: string, signal?: AbortSignal): Promise<{ kind: "success"; value: UnrestrictedLink } | { kind: "login_required" }> {
throwIfAborted(signal); throwIfAborted(signal);
const currentSession = session.fromPartition(this.getPartition()); const currentSession = session.fromPartition(this.getPartition());
const response = await currentSession.fetch(BESTDEBRID_GENERATE_URL, { await configureElectronProxySession(currentSession);
const response = await currentSession.fetch(BESTDEBRID_GENERATE_URL, {
method: "POST", method: "POST",
headers: { headers: {
Accept: "application/json, text/javascript, */*; q=0.01", Accept: "application/json, text/javascript, */*; q=0.01",
+1
View File
@@ -108,6 +108,7 @@ export function defaultSettings(): AppSettings {
speedLimitMode: "global", speedLimitMode: "global",
proxyDownloadEnabled: false, proxyDownloadEnabled: false,
proxyListPath: "", proxyListPath: "",
proxyApiProxyIndex: 1,
proxyConnectionsPerDownload: 16, proxyConnectionsPerDownload: 16,
updateRepo: DEFAULT_UPDATE_REPO, updateRepo: DEFAULT_UPDATE_REPO,
autoUpdateCheck: true, autoUpdateCheck: true,
+3 -3
View File
@@ -11001,7 +11001,7 @@ export class DownloadManager extends EventEmitter {
if (!proxyAttempted && attempt === 1 && existingBytes === 0 && this.settings.proxyDownloadEnabled) { if (!proxyAttempted && attempt === 1 && existingBytes === 0 && this.settings.proxyDownloadEnabled) {
proxyAttempted = true; proxyAttempted = true;
if (this.settings.speedLimitEnabled) { 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 attempt
}); });
} else { } else {
@@ -11104,10 +11104,10 @@ export class DownloadManager extends EventEmitter {
item.downloadedBytes = 0; item.downloadedBytes = 0;
item.progressPercent = 0; item.progressPercent = 0;
item.speedBps = 0; item.speedBps = 0;
item.fullStatus = "Direktdownload wird gestartet"; item.fullStatus = "Einzel-Proxy-Download wird gestartet";
item.updatedAt = nowMs(); item.updatedAt = nowMs();
this.emitState(); this.emitState();
logAttemptEvent("WARN", "Proxy-Segmentdownload nicht verwendet, Direktdownload folgt", { logAttemptEvent("WARN", "Proxy-Segmentdownload nicht verwendet, fester Einzel-Proxy folgt", {
attempt, attempt,
reason: proxyReasonLabels[proxyResult.reason] reason: proxyReasonLabels[proxyResult.reason]
}); });
+9 -1
View File
@@ -32,6 +32,7 @@ import {
validateCollectorPersistenceState, validateCollectorPersistenceState,
validateCollectorTextPreparationRequest validateCollectorTextPreparationRequest
} from "../shared/collector"; } from "../shared/collector";
import { getProxyAuthentication } from "./network-proxy";
forceDarkNativeTheme(nativeTheme); forceDarkNativeTheme(nativeTheme);
@@ -1271,7 +1272,14 @@ function formatRendererErrorReport(rawReport: unknown): string {
return parts.join(" "); 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 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}` : ""}`; 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) { if (killed) {
+165
View File
@@ -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(() => {});
}
}
+58 -16
View File
@@ -15,10 +15,14 @@ const MAX_REDIRECTS = 5;
const MAX_PROXY_FILE_BYTES = 8 * 1024 * 1024; const MAX_PROXY_FILE_BYTES = 8 * 1024 * 1024;
const DISK_ERROR_CODES = new Set(["ENOSPC", "EDQUOT", "EACCES", "EPERM", "EROFS", "EIO", "ENODEV"]); const DISK_ERROR_CODES = new Set(["ENOSPC", "EDQUOT", "EACCES", "EPERM", "EROFS", "EIO", "ENODEV"]);
interface ProxyEndpoint { export interface ProxyEndpoint {
id: number; id: number;
url: string; url: string;
authorization: string; authorization: string;
username: string;
password: string;
hostname: string;
port: number;
} }
interface CachedProxyFile { interface CachedProxyFile {
@@ -110,9 +114,11 @@ function parseProxyLine(rawLine: string, id: number): ProxyEndpoint | null {
return null; return null;
} }
let authorization = ""; let authorization = "";
let username = "";
let password = "";
if (parsed.username || parsed.password) { if (parsed.username || parsed.password) {
const username = decodeURIComponent(parsed.username); username = decodeURIComponent(parsed.username);
const password = decodeURIComponent(parsed.password); password = decodeURIComponent(parsed.password);
authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`; authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
parsed.username = ""; parsed.username = "";
parsed.password = ""; parsed.password = "";
@@ -120,7 +126,15 @@ function parseProxyLine(rawLine: string, id: number): ProxyEndpoint | null {
parsed.pathname = ""; parsed.pathname = "";
parsed.search = ""; parsed.search = "";
parsed.hash = ""; parsed.hash = "";
return { id, url: parsed.toString(), authorization }; return {
id,
url: parsed.toString(),
authorization,
username,
password,
hostname: parsed.hostname,
port
};
} catch { } catch {
return null; return null;
} }
@@ -133,6 +147,45 @@ export function parseProxyList(content: string): number {
.reduce((count, line, index) => count + (parseProxyLine(line, index) ? 1 : 0), 0); .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< async function loadProxyFile(filePath: string): Promise<
{ status: "ok"; proxies: ProxyEndpoint[] } { status: "ok"; proxies: ProxyEndpoint[] }
| { status: "unavailable" } | { status: "unavailable" }
@@ -151,18 +204,7 @@ async function loadProxyFile(filePath: string): Promise<
: { status: "empty" }; : { status: "empty" };
} }
const content = await fs.promises.readFile(normalizedPath, "utf8"); const content = await fs.promises.readFile(normalizedPath, "utf8");
const seen = new Set<string>(); const proxies = parseUniqueProxyEndpoints(content);
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;
});
proxyFileCache.set(normalizedPath, { mtimeMs: stat.mtimeMs, size: stat.size, proxies }); proxyFileCache.set(normalizedPath, { mtimeMs: stat.mtimeMs, size: stat.size, proxies });
return proxies.length > 0 ? { status: "ok", proxies } : { status: "empty" }; return proxies.length > 0 ? { status: "ok", proxies } : { status: "empty" };
} catch { } catch {
+9 -5
View File
@@ -4,6 +4,7 @@ import { sleep } from "./utils";
import { API_BASE_URL, REQUEST_RETRIES } from "./constants"; import { API_BASE_URL, REQUEST_RETRIES } from "./constants";
import { applyRemoteLoginSecurity, createRemoteLoginWebPreferences, REALDEBRID_LOGIN_HOSTS } from "./browser-security"; import { applyRemoteLoginSecurity, createRemoteLoginWebPreferences, REALDEBRID_LOGIN_HOSTS } from "./browser-security";
import { buildRealDebridWebGenerationScript, normalizeRealDebridWebGenerationResult } from "./realdebrid-web-page"; import { buildRealDebridWebGenerationScript, normalizeRealDebridWebGenerationResult } from "./realdebrid-web-page";
import { configureElectronProxySession } from "./network-proxy";
const RD_BASE_URL = "https://real-debrid.com"; const RD_BASE_URL = "https://real-debrid.com";
const RD_LOGIN_URL = RD_BASE_URL; const RD_LOGIN_URL = RD_BASE_URL;
@@ -366,9 +367,10 @@ export class RealDebridWebFallback {
return raceWithAbort(run, signal); return raceWithAbort(run, signal);
} }
private async ensureLoginWindow(): Promise<BrowserWindow> { private async ensureLoginWindow(): Promise<BrowserWindow> {
const partition = this.getPartition(); const partition = this.getPartition();
const existing = this.loginWindow; await configureElectronProxySession(session.fromPartition(partition));
const existing = this.loginWindow;
if (existing && !existing.isDestroyed() && this.loginWindowPartition === partition) { if (existing && !existing.isDestroyed() && this.loginWindowPartition === partition) {
return existing; return existing;
} }
@@ -431,6 +433,7 @@ export class RealDebridWebFallback {
this.throwIfDisposed(); this.throwIfDisposed();
throwIfAborted(signal); throwIfAborted(signal);
const partition = this.getPartition(); const partition = this.getPartition();
await configureElectronProxySession(session.fromPartition(partition));
const existing = this.generatorWindow; const existing = this.generatorWindow;
if (existing && !existing.isDestroyed() && this.generatorWindowPartition === partition) { if (existing && !existing.isDestroyed() && this.generatorWindowPartition === partition) {
return existing; return existing;
@@ -603,8 +606,9 @@ export class RealDebridWebFallback {
} }
} }
const currentSession = session.fromPartition(this.getPartition()); const currentSession = session.fromPartition(this.getPartition());
const response = await currentSession.fetch(RD_APITOKEN_URL, { await configureElectronProxySession(currentSession);
const response = await currentSession.fetch(RD_APITOKEN_URL, {
headers: { headers: {
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
Referer: RD_BASE_URL + "/", Referer: RD_BASE_URL + "/",
+1
View File
@@ -206,6 +206,7 @@ export function createRendererSettings(settings: AppSettings): RendererSettings
speedLimitMode: settings.speedLimitMode, speedLimitMode: settings.speedLimitMode,
proxyDownloadEnabled: settings.proxyDownloadEnabled, proxyDownloadEnabled: settings.proxyDownloadEnabled,
proxyListPath: settings.proxyListPath, proxyListPath: settings.proxyListPath,
proxyApiProxyIndex: settings.proxyApiProxyIndex,
proxyConnectionsPerDownload: settings.proxyConnectionsPerDownload, proxyConnectionsPerDownload: settings.proxyConnectionsPerDownload,
updateRepo: settings.updateRepo, updateRepo: settings.updateRepo,
autoUpdateCheck: settings.autoUpdateCheck, autoUpdateCheck: settings.autoUpdateCheck,
+1
View File
@@ -614,6 +614,7 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
speedLimitMode: settings.speedLimitMode, speedLimitMode: settings.speedLimitMode,
proxyDownloadEnabled: Boolean(settings.proxyDownloadEnabled), proxyDownloadEnabled: Boolean(settings.proxyDownloadEnabled),
proxyListPath: asText(settings.proxyListPath).slice(0, 4096), proxyListPath: asText(settings.proxyListPath).slice(0, 4096),
proxyApiProxyIndex: clampNumber(settings.proxyApiProxyIndex, defaults.proxyApiProxyIndex, 1, 100000),
proxyConnectionsPerDownload: clampNumber(settings.proxyConnectionsPerDownload, defaults.proxyConnectionsPerDownload, 2, 32), proxyConnectionsPerDownload: clampNumber(settings.proxyConnectionsPerDownload, defaults.proxyConnectionsPerDownload, 2, 32),
autoUpdateCheck: Boolean(settings.autoUpdateCheck), autoUpdateCheck: Boolean(settings.autoUpdateCheck),
updateRepo: migrateUpdateRepo(asText(settings.updateRepo), defaults.updateRepo), updateRepo: migrateUpdateRepo(asText(settings.updateRepo), defaults.updateRepo),
+2 -1
View File
@@ -953,7 +953,7 @@ const emptySnapshot = (): UiSnapshot => ({
removeSamplesAfterExtract: false, enableIntegrityCheck: true, autoResumeOnStart: true, removeSamplesAfterExtract: false, enableIntegrityCheck: true, autoResumeOnStart: true,
autoReconnect: false, reconnectWaitSeconds: 45, completedCleanupPolicy: "never", autoReconnect: false, reconnectWaitSeconds: 45, completedCleanupPolicy: "never",
maxParallel: 4, maxParallelExtract: 2, extractCpuPriority: "high", retryLimit: 0, speedLimitEnabled: false, speedLimitKbps: 0, speedLimitMode: "global", 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, 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, 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, notifyMention: "", notifyOnPackageCompleted: false, notifyOnPackageFailed: false, notifyOnRunFinished: false,
@@ -6066,6 +6066,7 @@ export function App(): ReactElement {
historyMaxAgeDays: [0, 3650, 0], historyMaxAgeDays: [0, 3650, 0],
maxParallelExtract: [1, 8, 2], maxParallelExtract: [1, 8, 2],
reconnectWaitSeconds: [10, 600, 45], reconnectWaitSeconds: [10, 600, 45],
proxyApiProxyIndex: [1, 100000, 1],
proxyConnectionsPerDownload: [2, 32, 16] proxyConnectionsPerDownload: [2, 32, 16]
}; };
const bounds = numericLimits[fieldId as keyof RendererSettingsDraft]; const bounds = numericLimits[fieldId as keyof RendererSettingsDraft];
+15 -5
View File
@@ -424,15 +424,15 @@ export function buildSettingsFormViewModel({
}, },
{ {
id: "speed-proxy", id: "speed-proxy",
title: "Proxy-Download", title: "Proxy-only",
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.", 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: [ fields: [
{ {
id: "proxyDownloadEnabled", id: "proxyDownloadEnabled",
kind: "switch", kind: "switch",
label: "Proxy-Download aktivieren", label: "Proxy-only aktivieren",
value: settings.proxyDownloadEnabled, 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", id: "proxyListPath",
@@ -444,6 +444,16 @@ export function buildSettingsFormViewModel({
disabled: !settings.proxyDownloadEnabled, disabled: !settings.proxyDownloadEnabled,
help: "Unterstützt unter anderem Benutzer:Passwort@Host:Port. Zugangsdaten werden nicht protokolliert." 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", id: "proxyConnectionsPerDownload",
kind: "number", kind: "number",
@@ -452,7 +462,7 @@ export function buildSettingsFormViewModel({
min: 2, min: 2,
max: 32, max: 32,
disabled: !settings.proxyDownloadEnabled, 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.`
} }
] ]
}, },
+1
View File
@@ -142,6 +142,7 @@ export interface DailyStartSettings {
export interface ProxyDownloadSettings { export interface ProxyDownloadSettings {
proxyDownloadEnabled: boolean; proxyDownloadEnabled: boolean;
proxyListPath: string; proxyListPath: string;
proxyApiProxyIndex: number;
proxyConnectionsPerDownload: number; proxyConnectionsPerDownload: number;
} }
+156
View File
@@ -0,0 +1,156 @@
import fs from "node:fs";
import http from "node:http";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { defaultSettings } from "../src/main/constants";
import {
configureElectronProxySession,
configureNetworkProxy,
getProxyAuthentication,
shutdownNetworkProxy
} from "../src/main/network-proxy";
interface RunningServer {
port: number;
close: () => Promise<void>;
}
const cleanups: Array<() => Promise<void>> = [];
afterEach(async () => {
await shutdownNetworkProxy();
while (cleanups.length > 0) {
await cleanups.pop()?.();
}
});
async function listen(server: http.Server): Promise<RunningServer> {
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
if (!address || typeof address === "string") throw new Error("Testserver konnte nicht gestartet werden");
const sockets = new Set<net.Socket>();
server.on("connection", (socket) => {
sockets.add(socket);
socket.once("close", () => sockets.delete(socket));
});
const close = async (): Promise<void> => {
for (const socket of sockets) socket.destroy();
await new Promise<void>((resolve) => server.close(() => resolve()));
};
cleanups.push(close);
return { port: address.port, close };
}
async function createConnectProxy(username: string, password: string, acceptedConnections: { value: number }): Promise<RunningServer> {
const server = http.createServer();
server.on("connect", (request, clientSocket, head) => {
const expectedAuth = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
if (request.headers["proxy-authorization"] !== expectedAuth) {
clientSocket.end("HTTP/1.1 407 Proxy Authentication Required\r\n\r\n");
return;
}
const target = new URL(`http://${request.url}`);
const upstream = net.connect(Number(target.port), target.hostname, () => {
acceptedConnections.value += 1;
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
if (head.length > 0) upstream.write(head);
clientSocket.pipe(upstream);
upstream.pipe(clientSocket);
});
upstream.once("error", () => clientSocket.destroy());
clientSocket.once("error", () => upstream.destroy());
});
return listen(server);
}
async function createTempDirectory(): Promise<string> {
const directory = await fs.promises.mkdtemp(path.join(os.tmpdir(), "mdd-network-proxy-test-"));
cleanups.push(() => fs.promises.rm(directory, { recursive: true, force: true }));
return directory;
}
describe("proxy-only network routing", () => {
it("routes global API fetches only through the selected fixed proxy", async () => {
let targetRequests = 0;
const target = await listen(http.createServer((_request, response) => {
targetRequests += 1;
response.writeHead(200, { "Content-Type": "text/plain" });
response.end("proxied");
}));
const counters = [{ value: 0 }, { value: 0 }];
const proxies = await Promise.all([
createConnectProxy("first", "secret-one", counters[0]),
createConnectProxy("second", "secret-two", counters[1])
]);
const directory = await createTempDirectory();
const proxyFile = path.join(directory, "proxies.txt");
await fs.promises.writeFile(proxyFile, [
`first:secret-one@127.0.0.1:${proxies[0].port}`,
`second:secret-two@127.0.0.1:${proxies[1].port}`
].join("\n"));
expect(configureNetworkProxy({
...defaultSettings(),
proxyDownloadEnabled: true,
proxyListPath: proxyFile,
proxyApiProxyIndex: 2
})).toEqual({ status: "active", selectedIndex: 2, proxyCount: 2 });
const response = await fetch(`http://127.0.0.1:${target.port}/api`);
expect(await response.text()).toBe("proxied");
expect(targetRequests).toBe(1);
expect(counters[0].value).toBe(0);
expect(counters[1].value).toBe(1);
});
it("fails closed when the configured proxy list cannot be loaded", async () => {
let targetRequests = 0;
const target = await listen(http.createServer((_request, response) => {
targetRequests += 1;
response.end("direct-leak");
}));
expect(configureNetworkProxy({
...defaultSettings(),
proxyDownloadEnabled: true,
proxyListPath: path.join(os.tmpdir(), `missing-${Date.now()}.txt`),
proxyApiProxyIndex: 1
})).toEqual({ status: "blocked", reason: "proxy_file_unavailable" });
await expect(fetch(`http://127.0.0.1:${target.port}/must-not-connect`)).rejects.toThrow();
expect(targetRequests).toBe(0);
});
it("configures Electron sessions without embedding credentials and supplies matching proxy authentication", async () => {
const directory = await createTempDirectory();
const proxyFile = path.join(directory, "proxies.txt");
await fs.promises.writeFile(proxyFile, "api-user:api-password@proxy.example:3128");
configureNetworkProxy({
...defaultSettings(),
proxyDownloadEnabled: true,
proxyListPath: proxyFile,
proxyApiProxyIndex: 1
});
const calls: unknown[] = [];
let closedConnections = 0;
const fakeSession = {
setProxy: async (rules: unknown) => { calls.push(rules); },
closeAllConnections: async () => { closedConnections += 1; }
};
await configureElectronProxySession(fakeSession as never);
expect(calls).toEqual([{ mode: "fixed_servers", proxyRules: "http://proxy.example:3128" }]);
expect(JSON.stringify(calls)).not.toContain("api-password");
expect(closedConnections).toBe(1);
expect(getProxyAuthentication({ isProxy: true, host: "proxy.example", port: 3128 })).toEqual({
username: "api-user",
password: "api-password"
});
expect(getProxyAuthentication({ isProxy: false, host: "proxy.example", port: 3128 })).toBeNull();
expect(getProxyAuthentication({ isProxy: true, host: "other.example", port: 3128 })).toBeNull();
});
});
+22 -1
View File
@@ -4,7 +4,7 @@ import net from "node:net";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { afterEach, describe, expect, it } from "vitest"; import { afterEach, describe, expect, it } from "vitest";
import { downloadWithProxySegments, parseProxyList } from "../src/main/proxy-segmented-download"; import { downloadWithProxySegments, parseProxyList, selectFixedProxy } from "../src/main/proxy-segmented-download";
interface RunningServer { interface RunningServer {
port: number; port: number;
@@ -117,6 +117,27 @@ describe("proxy segmented download", () => {
expect(String(count)).not.toContain(secret); expect(String(count)).not.toContain(secret);
}); });
it("selects one fixed valid proxy by its 1-based list index", async () => {
const directory = await createTempDirectory();
const proxyFile = path.join(directory, "fixed-proxies.txt");
await fs.promises.writeFile(proxyFile, [
"invalid",
"first:secret-one@127.0.0.1:8080",
"second:secret-two@127.0.0.1:9090"
].join("\n"));
const selected = selectFixedProxy(proxyFile, 2);
expect(selected.status).toBe("ok");
if (selected.status !== "ok") throw new Error("Proxy wurde nicht ausgewählt");
expect(selected.selectedIndex).toBe(2);
expect(selected.proxyCount).toBe(2);
expect(selected.proxy.url).toBe("http://127.0.0.1:9090/");
expect(selected.proxy.username).toBe("second");
expect(selected.proxy.password).toBe("secret-two");
expect(selected.proxy.url).not.toContain("secret-two");
});
it("downloads exact byte segments through different authenticated proxies", async () => { it("downloads exact byte segments through different authenticated proxies", async () => {
const content = Buffer.allocUnsafe(256 * 1024); const content = Buffer.allocUnsafe(256 * 1024);
for (let index = 0; index < content.length; index += 1) content[index] = index % 251; for (let index = 0; index < content.length; index += 1) content[index] = index % 251;
+2
View File
@@ -480,6 +480,7 @@ describe("realdebrid-web", () => {
"persist:realdebrid-web-rdw_second" "persist:realdebrid-web-rdw_second"
]); ]);
mockFromPartition.mockClear();
await first.clearSessions(); await first.clearSessions();
expect(mockFromPartition).toHaveBeenCalledWith("persist:realdebrid-web-rdw_first"); expect(mockFromPartition).toHaveBeenCalledWith("persist:realdebrid-web-rdw_first");
@@ -691,6 +692,7 @@ describe("realdebrid-web", () => {
await controller.openRealDebridLoginWindow({ accountId: "rdw_close_auth", create: true, dailyLimitBytes: 123_456 }); await controller.openRealDebridLoginWindow({ accountId: "rdw_close_auth", create: true, dailyLimitBytes: 123_456 });
await vi.waitFor(() => expect(mockExecuteJavaScript).toHaveBeenCalledTimes(1)); await vi.waitFor(() => expect(mockExecuteJavaScript).toHaveBeenCalledTimes(1));
mockFromPartition.mockClear();
mockBrowserWindows[0]?.close(); mockBrowserWindows[0]?.close();
resolveClosingToken("close-time-token"); resolveClosingToken("close-time-token");
await vi.waitFor(() => expect(controller.settings.realDebridWebAccountIds).toEqual(["rdw_close_auth"])); await vi.waitFor(() => expect(controller.settings.realDebridWebAccountIds).toEqual(["rdw_close_auth"]));
+9 -1
View File
@@ -650,6 +650,7 @@ describe("settings views", () => {
maxParallel: 5, maxParallel: 5,
proxyDownloadEnabled: true, proxyDownloadEnabled: true,
proxyListPath: "C:\\proxy-list.txt", proxyListPath: "C:\\proxy-list.txt",
proxyApiProxyIndex: 7,
proxyConnectionsPerDownload: 16 proxyConnectionsPerDownload: 16
}), }),
archivePasswordList: "", archivePasswordList: "",
@@ -664,6 +665,7 @@ describe("settings views", () => {
expect(proxyGroup?.fields.map((field) => field.id)).toEqual([ expect(proxyGroup?.fields.map((field) => field.id)).toEqual([
"proxyDownloadEnabled", "proxyDownloadEnabled",
"proxyListPath", "proxyListPath",
"proxyApiProxyIndex",
"proxyConnectionsPerDownload" "proxyConnectionsPerDownload"
]); ]);
expect(proxyGroup?.fields.find((field) => field.id === "proxyListPath")).toEqual(expect.objectContaining({ expect(proxyGroup?.fields.find((field) => field.id === "proxyListPath")).toEqual(expect.objectContaining({
@@ -671,11 +673,17 @@ describe("settings views", () => {
actionLabel: "Datei wählen", actionLabel: "Datei wählen",
disabled: false disabled: false
})); }));
expect(proxyGroup?.fields.find((field) => field.id === "proxyApiProxyIndex")).toEqual(expect.objectContaining({
value: "7",
min: 1,
max: 100000,
disabled: false
}));
expect(proxyGroup?.fields.find((field) => field.id === "proxyConnectionsPerDownload")).toEqual(expect.objectContaining({ expect(proxyGroup?.fields.find((field) => field.id === "proxyConnectionsPerDownload")).toEqual(expect.objectContaining({
value: "16", value: "16",
min: 2, min: 2,
max: 32, max: 32,
help: expect.stringContaining("80 Proxy-Verbindungen") help: expect.stringContaining("80 parallele Segmentverbindungen")
})); }));
}); });
+4
View File
@@ -584,6 +584,7 @@ describe("settings storage", () => {
speedLimitKbps: -1, speedLimitKbps: -1,
proxyDownloadEnabled: true, proxyDownloadEnabled: true,
proxyListPath: " C:\\proxies.txt ", proxyListPath: " C:\\proxies.txt ",
proxyApiProxyIndex: 999999,
proxyConnectionsPerDownload: 999, proxyConnectionsPerDownload: 999,
outputDir: " ", outputDir: " ",
extractDir: " ", extractDir: " ",
@@ -604,6 +605,7 @@ describe("settings storage", () => {
expect(normalized.speedLimitKbps).toBe(0); expect(normalized.speedLimitKbps).toBe(0);
expect(normalized.proxyDownloadEnabled).toBe(true); expect(normalized.proxyDownloadEnabled).toBe(true);
expect(normalized.proxyListPath).toBe("C:\\proxies.txt"); expect(normalized.proxyListPath).toBe("C:\\proxies.txt");
expect(normalized.proxyApiProxyIndex).toBe(100000);
expect(normalized.proxyConnectionsPerDownload).toBe(32); expect(normalized.proxyConnectionsPerDownload).toBe(32);
expect(normalized.outputDir).toBe(defaultSettings().outputDir); expect(normalized.outputDir).toBe(defaultSettings().outputDir);
expect(normalized.extractDir).toBe(defaultSettings().extractDir); expect(normalized.extractDir).toBe(defaultSettings().extractDir);
@@ -639,6 +641,7 @@ describe("settings storage", () => {
speedLimitMode: "not-valid", speedLimitMode: "not-valid",
proxyDownloadEnabled: 1, proxyDownloadEnabled: 1,
proxyListPath: " C:\\proxy-list.txt ", proxyListPath: " C:\\proxy-list.txt ",
proxyApiProxyIndex: "0",
proxyConnectionsPerDownload: "1", proxyConnectionsPerDownload: "1",
updateRepo: "", updateRepo: "",
autoSortPackagesByProgress: false autoSortPackagesByProgress: false
@@ -655,6 +658,7 @@ describe("settings storage", () => {
expect(loaded.speedLimitMode).toBe("global"); expect(loaded.speedLimitMode).toBe("global");
expect(loaded.proxyDownloadEnabled).toBe(true); expect(loaded.proxyDownloadEnabled).toBe(true);
expect(loaded.proxyListPath).toBe("C:\\proxy-list.txt"); expect(loaded.proxyListPath).toBe("C:\\proxy-list.txt");
expect(loaded.proxyApiProxyIndex).toBe(1);
expect(loaded.proxyConnectionsPerDownload).toBe(2); expect(loaded.proxyConnectionsPerDownload).toBe(2);
expect(loaded.updateRepo).toBe(defaultSettings().updateRepo); expect(loaded.updateRepo).toBe(defaultSettings().updateRepo);
expect(loaded.autoSortPackagesByProgress).toBe(false); expect(loaded.autoSortPackagesByProgress).toBe(false);
+1
View File
@@ -119,6 +119,7 @@ function createSettings(): AppSettings {
speedLimitMode: "global", speedLimitMode: "global",
proxyDownloadEnabled: false, proxyDownloadEnabled: false,
proxyListPath: "", proxyListPath: "",
proxyApiProxyIndex: 1,
proxyConnectionsPerDownload: 16, proxyConnectionsPerDownload: 16,
updateRepo: "Sucukdeluxe/Multi-Debrid-Downloader", updateRepo: "Sucukdeluxe/Multi-Debrid-Downloader",
autoUpdateCheck: true, autoUpdateCheck: true,