Release v1.6.80

This commit is contained in:
Sucukdeluxe 2026-03-06 12:04:56 +01:00
parent 67854feeb4
commit 359fb93be3
4 changed files with 215 additions and 11 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.6.69", "version": "1.6.80",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.6.69", "version": "1.6.80",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.6.79", "version": "1.6.80",
"description": "Desktop downloader", "description": "Desktop downloader",
"main": "build/main/main/main.js", "main": "build/main/main/main.js",
"author": "Sucukdeluxe", "author": "Sucukdeluxe",

View File

@ -1,5 +1,5 @@
import fs from "node:fs"; import fs from "node:fs";
import { 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";
@ -43,6 +43,7 @@ function parseJson(text: string): Record<string, unknown> | null {
interface NetscapeCookie { interface NetscapeCookie {
domain: string; domain: string;
includeSubdomains: boolean;
httpOnly: boolean; httpOnly: boolean;
path: string; path: string;
secure: boolean; secure: boolean;
@ -55,16 +56,26 @@ function parseNetscapeCookieFile(text: string): NetscapeCookie[] {
const cookies: NetscapeCookie[] = []; const cookies: NetscapeCookie[] = [];
for (const line of text.split(/\r?\n/)) { for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim(); const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) { if (!trimmed) {
continue; continue;
} }
const parts = trimmed.split("\t");
let normalizedLine = trimmed;
let httpOnly = false;
if (normalizedLine.startsWith("#HttpOnly_")) {
httpOnly = true;
normalizedLine = normalizedLine.slice("#HttpOnly_".length);
} else if (normalizedLine.startsWith("#")) {
continue;
}
const parts = normalizedLine.split("\t");
if (parts.length < 7) { if (parts.length < 7) {
continue; continue;
} }
cookies.push({ cookies.push({
domain: parts[0], domain: parts[0],
httpOnly: parts[1].toUpperCase() === "TRUE", includeSubdomains: parts[1].toUpperCase() === "TRUE",
httpOnly,
path: parts[2], path: parts[2],
secure: parts[3].toUpperCase() === "TRUE", secure: parts[3].toUpperCase() === "TRUE",
expirationDate: Number(parts[4]) || 0, expirationDate: Number(parts[4]) || 0,
@ -75,6 +86,25 @@ function parseNetscapeCookieFile(text: string): NetscapeCookie[] {
return cookies; return cookies;
} }
function isLikelyBestDebridAuthCookie(name: string): boolean {
const normalized = String(name || "").trim();
return /phpsessid|sess(?:ion)?|auth|login/i.test(normalized);
}
function isAuthenticatedBestDebridHtml(html: string): boolean {
const normalized = String(html || "");
if (!normalized) {
return false;
}
return /href\s*=\s*["']logout["']/i.test(normalized)
|| /title\s*=\s*["'][^"']*premium until/i.test(normalized)
|| (/user-profile-image/i.test(normalized) && !/>\s*guest\s*</i.test(normalized));
}
function looksLikeGuestAccessMessage(message: string): boolean {
return /free users are not allowed|purchase a premium plan|premium required/i.test(String(message || ""));
}
export class BestDebridWebFallback { export class BestDebridWebFallback {
private queue: Promise<unknown> = Promise.resolve(); private queue: Promise<unknown> = Promise.resolve();
@ -102,6 +132,7 @@ export class BestDebridWebFallback {
if (result.kind === "success") { if (result.kind === "success") {
return result.value; return result.value;
} }
this.cookiesImported = false;
throw new Error("BestDebrid: Nicht eingeloggt. Bitte neue Cookie-Datei importieren."); throw new Error("BestDebrid: Nicht eingeloggt. Bitte neue Cookie-Datei importieren.");
}, overallSignal); }, overallSignal);
} }
@ -117,21 +148,27 @@ export class BestDebridWebFallback {
throw new Error("Keine BestDebrid-Cookies in der Datei gefunden"); throw new Error("Keine BestDebrid-Cookies in der Datei gefunden");
} }
if (!bestDebridCookies.some((cookie) => isLikelyBestDebridAuthCookie(cookie.name))) {
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());
currentSession.setUserAgent(BESTDEBRID_USER_AGENT);
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}`;
await currentSession.cookies.set({ const details: Parameters<typeof currentSession.cookies.set>[0] = {
url, url,
name: cookie.name, name: cookie.name,
value: cookie.value, value: cookie.value,
domain: cookie.domain,
path: cookie.path, path: cookie.path,
secure: cookie.secure, secure: cookie.secure,
httpOnly: cookie.httpOnly, httpOnly: cookie.httpOnly,
expirationDate: cookie.expirationDate > 0 ? cookie.expirationDate : undefined expirationDate: cookie.expirationDate > 0 ? cookie.expirationDate : undefined
}); };
if (cookie.includeSubdomains || cookie.domain.startsWith(".")) {
details.domain = cookie.domain;
}
await currentSession.cookies.set(details);
} }
this.cookiesImported = true; this.cookiesImported = true;
@ -217,6 +254,12 @@ export class BestDebridWebFallback {
if (/login|log in|sign in|not logged|session|auth/i.test(message)) { if (/login|log in|sign in|not logged|session|auth/i.test(message)) {
return { kind: "login_required" }; return { kind: "login_required" };
} }
if (looksLikeGuestAccessMessage(message)) {
const authenticated = await this.isAuthenticated(currentSession, signal).catch(() => null);
if (authenticated === false) {
return { kind: "login_required" };
}
}
throw new Error(`BestDebrid Web: ${message || "Unbekannter Fehler"}`); throw new Error(`BestDebrid Web: ${message || "Unbekannter Fehler"}`);
} }
@ -248,4 +291,22 @@ export class BestDebridWebFallback {
} }
}; };
} }
private async isAuthenticated(currentSession: Session, signal?: AbortSignal): Promise<boolean> {
throwIfAborted(signal);
const response = await currentSession.fetch(BESTDEBRID_DOWNLOADER_URL, {
method: "GET",
headers: {
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
Referer: BESTDEBRID_BASE_URL,
"User-Agent": BESTDEBRID_USER_AGENT
},
signal: withTimeoutSignal(signal, 20_000)
});
if (!response.ok) {
return false;
}
const text = await response.text();
return isAuthenticatedBestDebridHtml(text);
}
} }

View File

@ -0,0 +1,143 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const {
mockCookiesSet,
mockFetch,
mockClearStorageData,
mockClearCache,
mockFromPartition,
mockSession
} = vi.hoisted(() => {
const cookiesSet = vi.fn();
const fetch = vi.fn();
const clearStorageData = vi.fn();
const clearCache = vi.fn();
const fromPartition = vi.fn();
return {
mockCookiesSet: cookiesSet,
mockFetch: fetch,
mockClearStorageData: clearStorageData,
mockClearCache: clearCache,
mockFromPartition: fromPartition,
mockSession: {
cookies: {
set: cookiesSet
},
fetch,
clearStorageData,
clearCache
}
};
});
vi.mock("electron", () => ({
session: {
fromPartition: mockFromPartition
}
}));
vi.mock("../src/main/logger", () => ({
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn()
}
}));
import { BestDebridWebFallback } from "../src/main/bestdebrid-web";
function createCookieFile(contents: string): string {
const filePath = path.join(os.tmpdir(), `bestdebrid-cookies-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`);
fs.writeFileSync(filePath, contents, "utf8");
return filePath;
}
describe("bestdebrid-web", () => {
const tempFiles: string[] = [];
beforeEach(() => {
mockFromPartition.mockReturnValue(mockSession);
});
afterEach(() => {
vi.clearAllMocks();
mockFromPartition.mockReturnValue(mockSession);
while (tempFiles.length > 0) {
const filePath = tempFiles.pop();
if (!filePath) {
continue;
}
try {
fs.rmSync(filePath, { force: true });
} catch {
// ignore temp cleanup failures
}
}
});
it("imports HttpOnly Netscape cookies instead of skipping them as comments", async () => {
const filePath = createCookieFile([
"# Netscape HTTP Cookie File",
"#HttpOnly_.bestdebrid.com\tTRUE\t/\tTRUE\t1803585385\tPHPSESSID\tsecret-session",
".bestdebrid.com\tTRUE\t/\tFALSE\t1806720721\t_ga\ttracking"
].join("\n"));
tempFiles.push(filePath);
const fallback = new BestDebridWebFallback(() => true);
const count = await fallback.importCookiesFromFile(filePath);
expect(count).toBe(2);
expect(mockCookiesSet).toHaveBeenCalledTimes(2);
expect(mockCookiesSet).toHaveBeenCalledWith(expect.objectContaining({
name: "PHPSESSID",
domain: ".bestdebrid.com",
httpOnly: true,
secure: true
}));
});
it("rejects cookie files that only contain tracking cookies", async () => {
const filePath = createCookieFile([
"# Netscape HTTP Cookie File",
".bestdebrid.com\tTRUE\t/\tTRUE\t1803585385\t__stripe_mid\tstripe",
".bestdebrid.com\tTRUE\t/\tFALSE\t1806720721\t_ga\ttracking"
].join("\n"));
tempFiles.push(filePath);
const fallback = new BestDebridWebFallback(() => true);
await expect(fallback.importCookiesFromFile(filePath))
.rejects.toThrow("Login-Cookie");
expect(mockCookiesSet).not.toHaveBeenCalled();
});
it("treats BestDebrid free-user errors as logged-out sessions when the account page is guest-only", async () => {
const filePath = createCookieFile([
"# Netscape HTTP Cookie File",
"bestdebrid.com\tFALSE\t/\tTRUE\t1803585385\tPHPSESSID\tsecret-session"
].join("\n"));
tempFiles.push(filePath);
mockFetch
.mockResolvedValueOnce(new Response(JSON.stringify({
error: 1,
message: "Free users are not allowed to download using a VPN or proxy. Please purchase a premium plan."
}), { status: 200 }))
.mockResolvedValueOnce(new Response("<div class=\"font-medium\">Guest</div>", { status: 200 }));
const fallback = new BestDebridWebFallback(() => true);
await fallback.importCookiesFromFile(filePath);
await expect(fallback.unrestrict("https://1fichier.com/?abc"))
.rejects.toThrow("Nicht eingeloggt");
await expect(fallback.unrestrict("https://1fichier.com/?abc"))
.rejects.toThrow("Keine Cookies importiert");
expect(mockFetch).toHaveBeenCalledTimes(2);
expect(mockFetch.mock.calls[0]?.[0]).toBe("https://bestdebrid.com/api/v1/generateLink");
expect(mockFetch.mock.calls[1]?.[0]).toBe("https://bestdebrid.com/en/downloader/");
});
});