image / auth /worker.ts
radithgo's picture
Initial MangaDex auth and image proxies
3be8994
Raw
History Blame Contribute Delete
5.78 kB
const DEFAULT_ALLOW_HEADERS =
"Accept, Authorization, Cache-Control, Content-Type, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Requested-With";
const DROP_HEADERS = new Set([
"accept-encoding",
"cf-connecting-ip",
"cf-ipcountry",
"cf-ray",
"cf-visitor",
"content-length",
"cookie",
"host",
"origin",
"referer",
"sec-ch-ua",
"sec-ch-ua-mobile",
"sec-ch-ua-platform",
"sec-fetch-dest",
"sec-fetch-mode",
"sec-fetch-site",
"sec-fetch-storage-access",
"sec-fetch-user",
"true-client-ip",
"x-forwarded-for",
"x-forwarded-proto",
"x-real-ip",
]);
export default {
async fetch(request: Request): Promise<Response> {
const reqHeaders = new Headers(request.headers);
const responseHeaders = corsHeaders(reqHeaders);
try {
const target = parseTargetUrl(request.url);
if (request.method === "OPTIONS" || !target) {
return new Response(await getHelp(new URL(request.url)), {
status: request.method === "OPTIONS" ? 200 : 400,
headers: withContentType(responseHeaders, "text/html; charset=utf-8"),
});
}
const upstreamRequest: RequestInit = {
method: request.method,
headers: upstreamHeaders(reqHeaders),
};
if (["POST", "PUT", "PATCH", "DELETE"].includes(request.method)) {
upstreamRequest.body = await requestBody(request, reqHeaders);
}
const upstreamResponse = await fetch(target, upstreamRequest);
const contentType = upstreamResponse.headers.get("content-type");
if (contentType) {
responseHeaders.set("content-type", contentType);
}
responseHeaders.set("cache-control", "no-store");
return new Response(upstreamResponse.body, {
status: upstreamResponse.status,
statusText: upstreamResponse.statusText,
headers: responseHeaders,
});
} catch (err) {
return new Response(
JSON.stringify({
code: -1,
msg: err instanceof Error ? err.message : String(err),
}),
{
status: 500,
headers: withContentType(responseHeaders, "application/json; charset=utf-8"),
}
);
}
},
};
function corsHeaders(requestHeaders: Headers) {
return new Headers({
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
"access-control-allow-headers":
requestHeaders.get("access-control-request-headers") || DEFAULT_ALLOW_HEADERS,
});
}
function parseTargetUrl(requestUrl: string) {
const current = new URL(requestUrl);
let rawTarget = decodeURIComponent(current.pathname.slice(1));
if (
rawTarget.length < 3 ||
rawTarget.indexOf(".") === -1 ||
rawTarget === "favicon.ico" ||
rawTarget === "robots.txt"
) {
return null;
}
rawTarget = fixUrl(rawTarget);
if (!isAllowedTarget(rawTarget)) {
throw new Error("Only MangaDex auth/API URLs are allowed by this proxy.");
}
if (current.search) {
rawTarget += current.search;
}
return rawTarget;
}
function isAllowedTarget(url: string) {
const parsed = new URL(url);
return (
parsed.protocol === "https:" &&
(parsed.hostname === "auth.mangadex.org" || parsed.hostname === "api.mangadex.org")
);
}
function fixUrl(url: string) {
if (url.includes("://")) {
return url;
}
if (url.includes(":/")) {
return url.replace(":/", "://");
}
return `https://${url}`;
}
function upstreamHeaders(requestHeaders: Headers) {
const headers = new Headers();
requestHeaders.forEach((value, key) => {
if (!DROP_HEADERS.has(key.toLowerCase())) {
headers.set(key, value);
}
});
if (!headers.has("user-agent")) {
headers.set("user-agent", "MangaDex-Auth-Proxy/1.0");
}
return headers;
}
async function requestBody(request: Request, requestHeaders: Headers) {
const contentType = (requestHeaders.get("content-type") || "").toLowerCase();
if (
contentType.includes("application/x-www-form-urlencoded") ||
contentType.includes("application/json") ||
contentType.includes("application/text") ||
contentType.includes("text/")
) {
return request.text();
}
if (contentType.includes("multipart/form-data")) {
return request.formData();
}
return request.arrayBuffer();
}
function withContentType(headers: Headers, contentType: string) {
const nextHeaders = new Headers(headers);
nextHeaders.set("content-type", contentType);
return nextHeaders;
}
async function getHelp(url: URL) {
const origin = `${url.protocol}//${url.hostname}`;
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MangaDex Auth Proxy</title>
<style>
body {
max-width: 780px;
margin: 40px auto;
padding: 0 18px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
line-height: 1.55;
}
code {
background: #f1f5f9;
padding: 2px 5px;
}
pre {
background: #0f172a;
color: #e2e8f0;
overflow: auto;
padding: 16px;
}
</style>
</head>
<body>
<h1>MangaDex Auth Proxy</h1>
<p>Proxy MangaDex OAuth/API requests from a browser and inject CORS response headers.</p>
<p>This worker only allows <code>auth.mangadex.org</code> and <code>api.mangadex.org</code>.</p>
<h2>Usage</h2>
<pre>${origin}/https://auth.mangadex.org/realms/mangadex/protocol/openid-connect/token</pre>
<h2>Important</h2>
<p>This proxy can see credentials and refresh tokens sent through it. Deploy your own worker and do not use a random public proxy for auth.</p>
</body>
</html>`;
}