UNPKG

node-csfd-api

Version:

ČSFD API in JavaScript. Amazing NPM library for scrapping csfd.cz :)

134 lines (133 loc) 5.53 kB
const require_proof_of_work = require("./proof-of-work.cjs"); //#region src/anubis/challenge.ts const AUTH_COOKIE_NAME = "techaro.lol-anubis-auth"; const VERIFY_COOKIE_NAME = "techaro.lol-anubis-cookie-verification"; const PASS_CHALLENGE_PATH = "/.within.website/x/cmd/anubis/api/pass-challenge"; const PROOF_OF_WORK_ALGORITHMS = ["fast", "slow"]; const METAREFRESH_ALGORITHM = "metarefresh"; const DEFAULT_METAREFRESH_DELAY_MS = 2e3; const CHALLENGE_MARKERS = ["id=\"anubis_challenge\"", "/.within.website/x/cmd/anubis/"]; const isAnubisChallenge = (html) => CHALLENGE_MARKERS.some((marker) => html.includes(marker)); const parseChallenge = (html) => { const match = html.match(/<script id="anubis_challenge" type="application\/json">([\s\S]*?)<\/script>/); if (!match) return null; try { return JSON.parse(match[1].trim()); } catch { return null; } }; const readCookie = (headers, name) => { if (typeof headers.getSetCookie !== "function") return null; return headers.getSetCookie().map((entry) => entry.split(";", 1)[0]).find((pair) => pair.startsWith(`${name}=`) && pair.length > name.length + 1) ?? null; }; const hidesSetCookie = (headers) => typeof headers.getSetCookie !== "function" || headers.getSetCookie().length === 0; const REFRESH_HEADER_DIRECTIVE = /^\s*(\d+)\s*;\s*url=(.+)$/i; const REFRESH_META_DIRECTIVE = /<meta[^>]+http-equiv=["']?refresh["']?[^>]*content=["'](\d+)[^;]*;\s*url=([^"'>]+)/i; /** * The `<delay>; url=<target>` directive Anubis serves with a metarefresh * challenge. It arrives as a `Refresh` header on some responses and as its * `<meta http-equiv>` equivalent on others, so both are read. */ const readRefreshDirective = (html, headers) => { const directive = headers.get("refresh")?.match(REFRESH_HEADER_DIRECTIVE) ?? html.match(REFRESH_META_DIRECTIVE); if (!directive) return null; return { delayMs: Number(directive[1]) * 1e3, url: directive[2].trim().replace(/&amp;/g, "&") }; }; const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const proofOfWorkPassUrl = async (url, { id, randomData }, difficulty, timeBudgetMs) => { const startedAt = Date.now(); const solution = await require_proof_of_work.solveProofOfWork(randomData, difficulty, timeBudgetMs); if (!solution) return null; const passUrl = new URL(PASS_CHALLENGE_PATH, url); passUrl.searchParams.set("id", id); passUrl.searchParams.set("response", solution.hash); passUrl.searchParams.set("nonce", String(solution.nonce)); passUrl.searchParams.set("redir", url); passUrl.searchParams.set("elapsedTime", String(Date.now() - startedAt)); return passUrl.toString(); }; /** * Metarefresh asks for patience rather than hashes: Anubis hands over the * exchange URL up front but answers it with 403 until the delay it declared has * actually elapsed, so the wait is the whole proof. */ const metarefreshPassUrl = async (url, { id, randomData }, directive, timeBudgetMs) => { const delayMs = directive?.delayMs ?? DEFAULT_METAREFRESH_DELAY_MS; if (delayMs > timeBudgetMs) return null; let passUrl; if (directive) passUrl = new URL(directive.url, url); else { passUrl = new URL(PASS_CHALLENGE_PATH, url); passUrl.searchParams.set("challenge", randomData); passUrl.searchParams.set("id", id); passUrl.searchParams.set("redir", url); } await wait(delayMs); return passUrl.toString(); }; /** * Solve the challenge on an interstitial page and exchange it for an Anubis * auth cookie. Returns `null` if the challenge could not be passed. */ const passChallenge = async ({ html: challengeHtml, headers: challengeHeaders, url, requestHeaders, fetch, timeBudgetMs = require_proof_of_work.DEFAULT_TIME_BUDGET_MS }) => { let html = challengeHtml; let headers = challengeHeaders; let platformCookieJar = false; if (hidesSetCookie(headers)) { platformCookieJar = true; const reissued = await fetch(url, { credentials: "include", redirect: "manual", headers: requestHeaders }); const reissuedHtml = await reissued.text(); if (!isAnubisChallenge(reissuedHtml)) return { cookie: null, platformCookieJar }; html = reissuedHtml; headers = reissued.headers; } const parsed = parseChallenge(html); if (!parsed) return null; const { challenge, rules } = parsed; let passUrl = null; if (PROOF_OF_WORK_ALGORITHMS.includes(rules.algorithm)) passUrl = await proofOfWorkPassUrl(url, challenge, rules.difficulty, timeBudgetMs); else if (rules.algorithm === METAREFRESH_ALGORITHM) passUrl = await metarefreshPassUrl(url, challenge, readRefreshDirective(html, headers), timeBudgetMs); if (!passUrl) return null; const passHeaders = new Headers(requestHeaders); const verifyCookie = readCookie(headers, VERIFY_COOKIE_NAME); if (verifyCookie) passHeaders.set("Cookie", verifyCookie); const response = await fetch(passUrl, { method: "GET", credentials: platformCookieJar ? "include" : "omit", redirect: "manual", headers: passHeaders }); const authCookie = readCookie(response.headers, AUTH_COOKIE_NAME); if (authCookie) return { cookie: authCookie, platformCookieJar }; if (!platformCookieJar) return null; if (response.status === 302 || response.type === "opaqueredirect") return { cookie: null, platformCookieJar }; if (response.ok) { const body = await response.text(); return body && !isAnubisChallenge(body) ? { cookie: null, platformCookieJar } : null; } return null; }; //#endregion exports.isAnubisChallenge = isAnubisChallenge; exports.passChallenge = passChallenge; //# sourceMappingURL=challenge.cjs.map