UNPKG

node-csfd-api

Version:

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

1 lines 13.4 kB
{"version":3,"file":"challenge.cjs","names":["solveProofOfWork","DEFAULT_TIME_BUDGET_MS"],"sources":["../../src/anubis/challenge.ts"],"sourcesContent":["import { DEFAULT_TIME_BUDGET_MS, solveProofOfWork } from './proof-of-work';\n\n// Anubis (BotStopper by Techaro) is a proof-of-work anti-bot interstitial:\n// instead of the page it serves an HTML challenge that a browser solves in\n// JavaScript. This module replicates the protocol so a plain `fetch` can earn\n// the auth cookie. See: https://github.com/TecharoHQ/anubis\n\nconst AUTH_COOKIE_NAME = 'techaro.lol-anubis-auth';\nconst VERIFY_COOKIE_NAME = 'techaro.lol-anubis-cookie-verification';\nconst PASS_CHALLENGE_PATH = '/.within.website/x/cmd/anubis/api/pass-challenge';\n\n// Anubis picks a challenge method per request. The SHA-256 ones make the client\n// burn CPU; `metarefresh` instead makes it sit out a declared delay. Anything\n// else (e.g. `preact`) needs a real JS runtime and must fail immediately rather\n// than burn the whole time budget computing a hash nobody asked for.\nconst PROOF_OF_WORK_ALGORITHMS = ['fast', 'slow'];\nconst METAREFRESH_ALGORITHM = 'metarefresh';\n\n// Anubis states the wait in a `Refresh` header or its `<meta>` twin. This is\n// only the fallback for a page that omits both, where the exchange URL has to\n// be rebuilt from the challenge anyway.\nconst DEFAULT_METAREFRESH_DELAY_MS = 2000;\n\n// Structural markers, deliberately not the localised body text: the\n// interstitial is translated, so matching prose would both miss locales and\n// risk false positives on user-generated content that quotes it.\nconst CHALLENGE_MARKERS = ['id=\"anubis_challenge\"', '/.within.website/x/cmd/anubis/'];\n\nexport type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;\n\ninterface ParsedChallenge {\n rules: { algorithm: string; difficulty: number };\n challenge: { id: string; randomData: string };\n}\n\nexport interface ChallengeResult {\n /** Cookie to replay, or `null` when the runtime's own cookie jar holds it. */\n cookie: string | null;\n /** True when Set-Cookie was hidden and the runtime now owns the cookie. */\n platformCookieJar: boolean;\n}\n\nexport interface PassChallengeParams {\n /** Body of the interstitial page that was served instead of the content. */\n html: string;\n /** Response headers that came with it, carrying the verification cookie. */\n headers: Headers;\n /** The URL that was blocked; used as the redirect target after passing. */\n url: string;\n /** Headers to reuse, so the exchange looks like the original request. */\n requestHeaders?: Headers;\n fetch: FetchLike;\n timeBudgetMs?: number;\n}\n\nexport const isAnubisChallenge = (html: string): boolean =>\n CHALLENGE_MARKERS.some((marker) => html.includes(marker));\n\nconst parseChallenge = (html: string): ParsedChallenge | null => {\n const match = html.match(\n /<script id=\"anubis_challenge\" type=\"application\\/json\">([\\s\\S]*?)<\\/script>/\n );\n if (!match) {\n return null;\n }\n try {\n return JSON.parse(match[1].trim()) as ParsedChallenge;\n } catch {\n return null;\n }\n};\n\nconst readCookie = (headers: Headers, name: string): string | null => {\n if (typeof headers.getSetCookie !== 'function') {\n return null;\n }\n const cookie = headers\n .getSetCookie()\n .map((entry) => entry.split(';', 1)[0])\n .find((pair) => pair.startsWith(`${name}=`) && pair.length > name.length + 1);\n return cookie ?? null;\n};\n\n// Set-Cookie is a forbidden response header outside Node, so seeing none on a\n// response that certainly carried them means the runtime (browser, React\n// Native) is hiding them and keeping the cookies in its own jar instead.\nconst hidesSetCookie = (headers: Headers): boolean =>\n typeof headers.getSetCookie !== 'function' || headers.getSetCookie().length === 0;\n\nconst REFRESH_HEADER_DIRECTIVE = /^\\s*(\\d+)\\s*;\\s*url=(.+)$/i;\nconst REFRESH_META_DIRECTIVE =\n /<meta[^>]+http-equiv=[\"']?refresh[\"']?[^>]*content=[\"'](\\d+)[^;]*;\\s*url=([^\"'>]+)/i;\n\ninterface RefreshDirective {\n delayMs: number;\n url: string;\n}\n\n/**\n * The `<delay>; url=<target>` directive Anubis serves with a metarefresh\n * challenge. It arrives as a `Refresh` header on some responses and as its\n * `<meta http-equiv>` equivalent on others, so both are read.\n */\nconst readRefreshDirective = (html: string, headers: Headers): RefreshDirective | null => {\n const directive =\n headers.get('refresh')?.match(REFRESH_HEADER_DIRECTIVE) ?? html.match(REFRESH_META_DIRECTIVE);\n if (!directive) {\n return null;\n }\n return {\n delayMs: Number(directive[1]) * 1000,\n // Inside a meta attribute the query separators arrive HTML-escaped.\n url: directive[2].trim().replace(/&amp;/g, '&')\n };\n};\n\nconst wait = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\nconst proofOfWorkPassUrl = async (\n url: string,\n { id, randomData }: ParsedChallenge['challenge'],\n difficulty: number,\n timeBudgetMs: number\n): Promise<string | null> => {\n const startedAt = Date.now();\n const solution = await solveProofOfWork(randomData, difficulty, timeBudgetMs);\n if (!solution) {\n return null;\n }\n\n const passUrl = new URL(PASS_CHALLENGE_PATH, url);\n passUrl.searchParams.set('id', id);\n passUrl.searchParams.set('response', solution.hash);\n passUrl.searchParams.set('nonce', String(solution.nonce));\n passUrl.searchParams.set('redir', url);\n passUrl.searchParams.set('elapsedTime', String(Date.now() - startedAt));\n return passUrl.toString();\n};\n\n/**\n * Metarefresh asks for patience rather than hashes: Anubis hands over the\n * exchange URL up front but answers it with 403 until the delay it declared has\n * actually elapsed, so the wait is the whole proof.\n */\nconst metarefreshPassUrl = async (\n url: string,\n { id, randomData }: ParsedChallenge['challenge'],\n directive: RefreshDirective | null,\n timeBudgetMs: number\n): Promise<string | null> => {\n const delayMs = directive?.delayMs ?? DEFAULT_METAREFRESH_DELAY_MS;\n // Waiting longer than the caller allowed is worse than not passing at all.\n if (delayMs > timeBudgetMs) {\n return null;\n }\n\n // Anubis' own URL is authoritative, so it is preferred over rebuilding one.\n let passUrl: URL;\n if (directive) {\n passUrl = new URL(directive.url, url);\n } else {\n passUrl = new URL(PASS_CHALLENGE_PATH, url);\n passUrl.searchParams.set('challenge', randomData);\n passUrl.searchParams.set('id', id);\n passUrl.searchParams.set('redir', url);\n }\n\n await wait(delayMs);\n return passUrl.toString();\n};\n\n/**\n * Solve the challenge on an interstitial page and exchange it for an Anubis\n * auth cookie. Returns `null` if the challenge could not be passed.\n */\nexport const passChallenge = async ({\n html: challengeHtml,\n headers: challengeHeaders,\n url,\n requestHeaders,\n fetch,\n timeBudgetMs = DEFAULT_TIME_BUDGET_MS\n}: PassChallengeParams): Promise<ChallengeResult | null> => {\n let html = challengeHtml;\n let headers = challengeHeaders;\n let platformCookieJar = false;\n\n // On a cookie-jar runtime the first request was made without credentials, so\n // the jar never stored Anubis' verification cookie. Ask for a fresh challenge\n // with credentials enabled and let the jar keep it this time.\n if (hidesSetCookie(headers)) {\n platformCookieJar = true;\n const reissued = await fetch(url, {\n credentials: 'include',\n redirect: 'manual',\n headers: requestHeaders\n });\n const reissuedHtml = await reissued.text();\n\n // Credentials change the answer: the jar may already hold a valid auth\n // cookie, in which case this sails straight past Anubis. There is then no\n // challenge left to solve — only a request worth retrying with the jar.\n if (!isAnubisChallenge(reissuedHtml)) {\n return { cookie: null, platformCookieJar };\n }\n\n html = reissuedHtml;\n headers = reissued.headers;\n }\n\n const parsed = parseChallenge(html);\n if (!parsed) {\n return null;\n }\n\n const { challenge, rules } = parsed;\n let passUrl: string | null = null;\n if (PROOF_OF_WORK_ALGORITHMS.includes(rules.algorithm)) {\n passUrl = await proofOfWorkPassUrl(url, challenge, rules.difficulty, timeBudgetMs);\n } else if (rules.algorithm === METAREFRESH_ALGORITHM) {\n passUrl = await metarefreshPassUrl(\n url,\n challenge,\n readRefreshDirective(html, headers),\n timeBudgetMs\n );\n }\n if (!passUrl) {\n return null;\n }\n\n // Anubis requires the verification cookie it set on the interstitial as proof\n // that cookies work; on a jar runtime the runtime itself attaches it.\n const passHeaders = new Headers(requestHeaders);\n const verifyCookie = readCookie(headers, VERIFY_COOKIE_NAME);\n if (verifyCookie) {\n passHeaders.set('Cookie', verifyCookie);\n }\n\n // `redirect: 'manual'` stops fetch from following the 302 to `redir`, which\n // would discard the Set-Cookie we need to read off this very response.\n const response = await fetch(passUrl, {\n method: 'GET',\n credentials: platformCookieJar ? 'include' : 'omit',\n redirect: 'manual',\n headers: passHeaders\n });\n\n const authCookie = readCookie(response.headers, AUTH_COOKIE_NAME);\n if (authCookie) {\n return { cookie: authCookie, platformCookieJar };\n }\n\n // No cookie in hand. A runtime that lets us read Set-Cookie would have shown\n // it, so this is a failed exchange; only a jar runtime can have passed while\n // keeping the cookie to itself.\n if (!platformCookieJar) {\n return null;\n }\n\n // A 302 to `redir` is Anubis' success signal, and the jar has just stored the\n // cookie off it. Browsers report the unfollowed redirect as `opaqueredirect`.\n if (response.status === 302 || response.type === 'opaqueredirect') {\n return { cookie: null, platformCookieJar };\n }\n\n // React Native ignores `redirect: 'manual'` and follows the 302 itself, so\n // what we hold is the page we were after — proof enough, unless Anubis is\n // still challenging us.\n if (response.ok) {\n const body = await response.text();\n return body && !isAnubisChallenge(body) ? { cookie: null, platformCookieJar } : null;\n }\n\n return null;\n};\n"],"mappings":"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"}