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 { 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 ` MangaDex Auth Proxy

MangaDex Auth Proxy

Proxy MangaDex OAuth/API requests from a browser and inject CORS response headers.

This worker only allows auth.mangadex.org and api.mangadex.org.

Usage

${origin}/https://auth.mangadex.org/realms/mangadex/protocol/openid-connect/token

Important

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.

`; }