Release v1.6.80
This commit is contained in:
parent
67854feeb4
commit
359fb93be3
4
package-lock.json
generated
4
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
143
tests/bestdebrid-web.test.ts
Normal file
143
tests/bestdebrid-web.test.ts
Normal 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/");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user