Files
stack/infra/cloudflared/worker/worker.js
kert 88901d0d17
Some checks failed
CI / skinny-install (aco) (push) Successful in 2m19s
CI / lint-test (push) Failing after 3m13s
CI / skinny-install (api) (push) Successful in 1m45s
CI / skinny-install (bcda) (push) Successful in 1m41s
CI / skinny-install (bib) (push) Successful in 2m2s
CI / skinny-install (bls) (push) Successful in 2m1s
CI / skinny-install (ccw) (push) Successful in 1m37s
CI / skinny-install (cli) (push) Successful in 2m7s
CI / skinny-install (cms) (push) Successful in 1m40s
CI / skinny-install (conf) (push) Successful in 1m57s
CI / skinny-install (perf) (push) Successful in 1m41s
CI / skinny-install (rex) (push) Successful in 1m46s
CI / skinny-install (pfs) (push) Successful in 2m37s
Deploy / build-scan-report (push) Failing after 2m34s
feat: Cloudflare Worker PDF proxy + headless batch retrieval (fixes #273)
Cloudflare Worker (pdf-proxy) deployed to bypass ISP-level SciHub blocking:
- Worker fetches PDFs from SciHub mirrors on Cloudflare's edge network
- Authenticated via X-Proxy-Key header
- Routes: /pdf?doi=, /fetch?url=, /health
- Deployed at pdf-proxy.lite7889.workers.dev

fetch_pdfs.py updated with 5-phase waterfall:
1. Unpaywall (legal OA, ~4% for this corpus)
2. PMC (free, PMCID-based)
3. Semantic Scholar (batch 500)
4. CF Worker → SciHub (72% hit rate on first 50, running full batch)
5. SciHub direct (fallback with proxy)

Also: cloudflared tunnel (stack-proxy) with WARP routing in compose.yml,
CoreDNS sci-hub domain routing via unfiltered DNS.

Test: 36/50 PDFs (72%) retrieved in first batch via Worker.
2026-03-25 21:54:44 -04:00

140 lines
3.9 KiB
JavaScript

/**
* PDF Fetch Proxy — Cloudflare Worker
*
* Relays HTTP requests through Cloudflare's edge network,
* bypassing ISP-level DNS/IP blocking of academic sources.
*
* Usage:
* GET https://<worker>/fetch?url=https://sci-hub.st/10.1234/example
* GET https://<worker>/pdf?doi=10.1234/example
*
* Security: requires X-Proxy-Key header matching the SECRET binding.
*/
const SCIHUB_MIRRORS = [
"https://sci-hub.st",
"https://sci-hub.ru",
"https://sci-hub.se",
"https://sci-hub.ren",
"https://sci-hub.ee",
];
export default {
async fetch(request, env) {
// Auth check
const key = request.headers.get("X-Proxy-Key");
if (!key || key !== env.SECRET) {
return new Response("Unauthorized", { status: 401 });
}
const url = new URL(request.url);
// Route: /fetch?url=<encoded_url> — generic proxy
if (url.pathname === "/fetch") {
const targetUrl = url.searchParams.get("url");
if (!targetUrl) {
return new Response("Missing url parameter", { status: 400 });
}
return proxyFetch(targetUrl);
}
// Route: /pdf?doi=<doi> — SciHub PDF resolver
if (url.pathname === "/pdf") {
const doi = url.searchParams.get("doi");
if (!doi) {
return new Response("Missing doi parameter", { status: 400 });
}
return fetchPdfFromScihub(doi);
}
// Route: /health
if (url.pathname === "/health") {
return new Response(JSON.stringify({ status: "ok", ts: Date.now() }), {
headers: { "Content-Type": "application/json" },
});
}
return new Response("Not found. Use /fetch?url=, /pdf?doi=, or /health", {
status: 404,
});
},
};
async function proxyFetch(targetUrl) {
try {
const resp = await fetch(targetUrl, {
headers: { "User-Agent": "Mozilla/5.0 (compatible; stack-proxy/1.0)" },
redirect: "follow",
});
// Stream the response back with original headers
const headers = new Headers(resp.headers);
headers.set("X-Proxy-Source", "cloudflare-worker");
return new Response(resp.body, {
status: resp.status,
headers,
});
} catch (e) {
return new Response(JSON.stringify({ error: e.message }), {
status: 502,
headers: { "Content-Type": "application/json" },
});
}
}
async function fetchPdfFromScihub(doi) {
for (const mirror of SCIHUB_MIRRORS) {
try {
const pageUrl = `${mirror}/${doi}`;
const resp = await fetch(pageUrl, {
headers: { "User-Agent": "Mozilla/5.0 (compatible; stack-proxy/1.0)" },
redirect: "follow",
});
if (resp.status !== 200) continue;
const html = await resp.text();
// Extract PDF URL from SciHub page
let pdfUrl = null;
const patterns = [
/id="pdf"[^>]*src="([^"]+)"/,
/<iframe[^>]*src="([^"]*\.pdf[^"]*)"/,
/<embed[^>]*src="([^"]*\.pdf[^"]*)"/,
];
for (const pat of patterns) {
const m = html.match(pat);
if (m) {
pdfUrl = m[1];
break;
}
}
if (!pdfUrl) continue;
// Normalize URL
if (pdfUrl.startsWith("//")) pdfUrl = "https:" + pdfUrl;
else if (pdfUrl.startsWith("/")) pdfUrl = mirror + pdfUrl;
// Fetch the actual PDF
const pdfResp = await fetch(pdfUrl, {
headers: { "User-Agent": "Mozilla/5.0" },
redirect: "follow",
});
if (pdfResp.status === 200) {
const headers = new Headers(pdfResp.headers);
headers.set("Content-Type", "application/pdf");
headers.set("X-Proxy-Source", "cloudflare-worker");
headers.set("X-Proxy-Mirror", mirror);
return new Response(pdfResp.body, { status: 200, headers });
}
} catch (e) {
continue;
}
}
return new Response(
JSON.stringify({ error: "PDF not found on any mirror", doi }),
{ status: 404, headers: { "Content-Type": "application/json" } }
);
}