node-csfd-api
Version:
ČSFD API in JavaScript. Amazing NPM library for scrapping csfd.cz :)
133 lines (132 loc) • 5.46 kB
JavaScript
import { DEFAULT_TIME_BUDGET_MS, solveProofOfWork } from "./proof-of-work.js";
//#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(/&/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 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 = 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
export { isAnubisChallenge, passChallenge };
//# sourceMappingURL=challenge.js.map