Spaces:
Runtime error
Runtime error
| /** | |
| * Cloudflare Worker source emitter for the OmniRoute proxy relay. | |
| * | |
| * Port of upstream decolua/9router PR #1360. The Worker plays the same role | |
| * the Vercel-relay edge function does (`src/app/api/settings/proxy/vercel-deploy/route.ts`): | |
| * - Accepts inbound requests carrying x-relay-target / x-relay-path / | |
| * x-relay-auth headers. | |
| * - Authorises with the embedded relayAuth secret (so a leaked workers.dev URL | |
| * is not an open SSRF proxy). | |
| * - Inlines an SSRF guard rejecting RFC1918 / loopback / link-local / IPv6 ULA | |
| * targets β the Edge runtime cannot import Node helpers, the guard lives | |
| * here as a string. | |
| * - Strips Host + relay control headers before forwarding upstream. | |
| * | |
| * The string template is fed to Cloudflare's PUT /accounts/{id}/workers/scripts/{name} | |
| * API with main_module=index.js (ESM Workers Modules format). | |
| * | |
| * The OmniRoute variant intentionally diverges from the upstream PR: | |
| * - The upstream worker had NO auth check, leaving the deployed workers.dev URL | |
| * as an open SSRF proxy. We mirror Vercel's x-relay-auth scheme instead so the | |
| * same buildVercelRelayHeaders helper (open-sse/utils/proxyDispatcher.ts) and | |
| * the same proxyFetch relay short-circuit work unchanged. | |
| * - SSRF guard is inlined so a leaked relay URL cannot scan internal IPs. | |
| */ | |
| export function buildCloudflareWorkerScript(relayAuth: string): string { | |
| // relayAuth is generated server-side via randomBytes(24).toString("hex") β no | |
| // user-controlled input ever reaches this template, so direct interpolation | |
| // into the worker source string is safe. | |
| return `// OmniRoute Cloudflare Worker proxy relay β generated at deploy time. | |
| function isPrivateHostname(h) { | |
| if (!h) return true; | |
| const host = h.trim().toLowerCase().replace(/^\\[|\\]$/g, ""); | |
| if ( | |
| host === "localhost" || | |
| host === "0.0.0.0" || | |
| host === "127.0.0.1" || | |
| host === "::1" || | |
| host.endsWith(".localhost") || | |
| host.endsWith(".local") || | |
| host.endsWith(".internal") || | |
| host.startsWith("::ffff:") | |
| ) return true; | |
| const v4 = host.match(/^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/); | |
| if (v4) { | |
| const a = +v4[1], b = +v4[2]; | |
| if (a === 0 || a === 10 || a === 127) return true; | |
| if (a === 169 && b === 254) return true; // link-local IPv4 | |
| if (a === 192 && b === 168) return true; | |
| if (a === 172 && b >= 16 && b <= 31) return true; | |
| if (a === 100 && b >= 64 && b <= 127) return true; | |
| return false; | |
| } | |
| if (host.includes(":")) { | |
| // IPv6 loopback/ULA/link-local (fe80::/10) | |
| return host === "::1" || host.startsWith("fc") || host.startsWith("fd") || host.startsWith("fe80:"); | |
| } | |
| return false; | |
| } | |
| export default { | |
| async fetch(request, env, ctx) { | |
| const auth = request.headers.get("x-relay-auth"); | |
| if (auth !== "${relayAuth}") { | |
| return new Response("Unauthorized", { status: 401 }); | |
| } | |
| const target = request.headers.get("x-relay-target"); | |
| if (!target) { | |
| return new Response("missing x-relay-target", { status: 400 }); | |
| } | |
| let targetUrl; | |
| try { targetUrl = new URL(target); } catch { return new Response("invalid x-relay-target", { status: 400 }); } | |
| if (targetUrl.protocol !== "http:" && targetUrl.protocol !== "https:") { | |
| return new Response("forbidden x-relay-target protocol", { status: 403 }); | |
| } | |
| if (targetUrl.username || targetUrl.password) { | |
| return new Response("forbidden x-relay-target (embedded credentials)", { status: 403 }); | |
| } | |
| if (isPrivateHostname(targetUrl.hostname)) { | |
| return new Response("forbidden x-relay-target (private/loopback host)", { status: 403 }); | |
| } | |
| const relayPath = request.headers.get("x-relay-path") || "/"; | |
| const headers = new Headers(request.headers); | |
| ["x-relay-target", "x-relay-path", "x-relay-auth", "host"].forEach((h) => headers.delete(h)); | |
| const init = { | |
| method: request.method, | |
| headers, | |
| }; | |
| if (request.method !== "GET" && request.method !== "HEAD") { | |
| init.body = request.body; | |
| init.duplex = "half"; | |
| } | |
| try { | |
| const upstream = await fetch(target.replace(/\\/$/, "") + relayPath, init); | |
| return new Response(upstream.body, { | |
| status: upstream.status, | |
| headers: upstream.headers, | |
| }); | |
| } catch (error) { | |
| return new Response(JSON.stringify({ error: error && error.message ? error.message : "relay error" }), { | |
| status: 502, | |
| headers: { "content-type": "application/json" }, | |
| }); | |
| } | |
| }, | |
| }; | |
| `; | |
| } | |