diff --git a/package-lock.json b/package-lock.json index 1bbce4d..7017f39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "real-debrid-downloader", - "version": "1.6.69", + "version": "1.6.80", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "real-debrid-downloader", - "version": "1.6.69", + "version": "1.6.80", "license": "MIT", "dependencies": { "adm-zip": "^0.5.16", diff --git a/package.json b/package.json index c2da959..9398894 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.6.79", + "version": "1.6.80", "description": "Desktop downloader", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/bestdebrid-web.ts b/src/main/bestdebrid-web.ts index 82adcaa..2f6dcc2 100644 --- a/src/main/bestdebrid-web.ts +++ b/src/main/bestdebrid-web.ts @@ -1,5 +1,5 @@ import fs from "node:fs"; -import { session } from "electron"; +import { session, type Session } from "electron"; import { UnrestrictedLink } from "./realdebrid"; import { filenameFromUrl, sleep } from "./utils"; import { logger } from "./logger"; @@ -43,6 +43,7 @@ function parseJson(text: string): Record | null { interface NetscapeCookie { domain: string; + includeSubdomains: boolean; httpOnly: boolean; path: string; secure: boolean; @@ -55,16 +56,26 @@ function parseNetscapeCookieFile(text: string): NetscapeCookie[] { const cookies: NetscapeCookie[] = []; for (const line of text.split(/\r?\n/)) { const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) { + if (!trimmed) { 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) { continue; } cookies.push({ domain: parts[0], - httpOnly: parts[1].toUpperCase() === "TRUE", + includeSubdomains: parts[1].toUpperCase() === "TRUE", + httpOnly, path: parts[2], secure: parts[3].toUpperCase() === "TRUE", expirationDate: Number(parts[4]) || 0, @@ -75,6 +86,25 @@ function parseNetscapeCookieFile(text: string): NetscapeCookie[] { 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* = Promise.resolve(); @@ -102,6 +132,7 @@ export class BestDebridWebFallback { if (result.kind === "success") { return result.value; } + this.cookiesImported = false; throw new Error("BestDebrid: Nicht eingeloggt. Bitte neue Cookie-Datei importieren."); }, overallSignal); } @@ -117,21 +148,27 @@ export class BestDebridWebFallback { 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()); - currentSession.setUserAgent(BESTDEBRID_USER_AGENT); for (const cookie of bestDebridCookies) { const url = `https://${cookie.domain.replace(/^\./, "")}${cookie.path}`; - await currentSession.cookies.set({ + const details: Parameters[0] = { url, name: cookie.name, value: cookie.value, - domain: cookie.domain, path: cookie.path, secure: cookie.secure, httpOnly: cookie.httpOnly, 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; @@ -217,6 +254,12 @@ export class BestDebridWebFallback { if (/login|log in|sign in|not logged|session|auth/i.test(message)) { 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"}`); } @@ -248,4 +291,22 @@ export class BestDebridWebFallback { } }; } + + private async isAuthenticated(currentSession: Session, signal?: AbortSignal): Promise { + 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); + } } diff --git a/tests/bestdebrid-web.test.ts b/tests/bestdebrid-web.test.ts new file mode 100644 index 0000000..727b3e9 --- /dev/null +++ b/tests/bestdebrid-web.test.ts @@ -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("
Guest
", { 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/"); + }); +});