From e5f7b35580e6be426efbd1a34004a4d70dbdb5e2 Mon Sep 17 00:00:00 2001 From: lapp Date: Wed, 6 May 2026 15:34:57 -0700 Subject: [PATCH] Add deploy/cloudflare-worker/worker.js --- deploy/cloudflare-worker/worker.js | 178 +++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 deploy/cloudflare-worker/worker.js diff --git a/deploy/cloudflare-worker/worker.js b/deploy/cloudflare-worker/worker.js new file mode 100644 index 0000000..85449c8 --- /dev/null +++ b/deploy/cloudflare-worker/worker.js @@ -0,0 +1,178 @@ +// Cloudflare Worker + +const WORKER_URL = "myworker.workers.dev"; + +const DEFAULT_UPSTREAM_TIMEOUT_MS = 25000; + +export default { + async fetch(request, env) { + try { + const hop = request.headers.get("x-relay-hop"); + const fwdHop = request.headers.get("x-fwd-hop"); + if (hop === "1" || fwdHop === "1") { + return json({ e: "loop detected" }, 508); + } + + if (request.method == "GET") { + return json({ e: "Relay is Active." }, 200); + } + + if (request.method !== "POST") { + return json({ e: "Method not allowed." }, 405); + } + + const req = await request.json(); + + if (!req.u) { + return json({ e: "missing url" }, 400); + } + + const targetUrl = new URL(req.u); + + const BLOCKED_HOSTS = [ + WORKER_URL, + ]; + + if (BLOCKED_HOSTS.some(h => targetUrl.hostname.endsWith(h))) { + return json({ e: "self-fetch blocked" }, 400); + } + + const upstreamUrl = (env && env.UPSTREAM_FORWARDER_URL) || ""; + if (upstreamUrl) { + const upstreamResp = await forwardViaUpstream(req, env, upstreamUrl); + if (upstreamResp) return upstreamResp; + // fall through to direct fetch only when fail-mode is open + } + + const headers = new Headers(); + if (req.h && typeof req.h === "object") { + for (const [k, v] of Object.entries(req.h)) { + headers.set(k, v); + } + } + + headers.set("x-relay-hop", "1"); + + const fetchOptions = { + method: (req.m || "GET").toUpperCase(), + headers, + redirect: req.r === false ? "manual" : "follow" + }; + + if (req.b) { + const binary = Uint8Array.from(atob(req.b), c => c.charCodeAt(0)); + fetchOptions.body = binary; + } + + const resp = await fetch(targetUrl.toString(), fetchOptions); + + // Read response safely (no stack overflow) + const buffer = await resp.arrayBuffer(); + const uint8 = new Uint8Array(buffer); + + let binary = ""; + const chunkSize = 0x8000; // prevent call stack overflow + + for (let i = 0; i < uint8.length; i += chunkSize) { + binary += String.fromCharCode.apply( + null, + uint8.subarray(i, i + chunkSize) + ); + } + + const base64 = btoa(binary); + + const responseHeaders = {}; + resp.headers.forEach((v, k) => { + responseHeaders[k] = v; + }); + + return json({ + s: resp.status, + h: responseHeaders, + b: base64 + }); + + } catch (err) { + return json({ e: String(err) }, 500); + } + } +}; + +async function forwardViaUpstream(req, env, upstreamUrl) { + const failMode = (env.UPSTREAM_FAIL_MODE || "closed").toLowerCase(); + const timeoutMs = parseInt(env.UPSTREAM_TIMEOUT_MS, 10) || DEFAULT_UPSTREAM_TIMEOUT_MS; + const authKey = env.UPSTREAM_AUTH_KEY || ""; + + let parsed; + try { + parsed = new URL(upstreamUrl); + } catch (_) { + return upstreamFailure("invalid UPSTREAM_FORWARDER_URL", failMode); + } + if (parsed.protocol !== "https:") { + return upstreamFailure("UPSTREAM_FORWARDER_URL must be https://", failMode); + } + if (parsed.hostname.endsWith(WORKER_URL)) { + return upstreamFailure("self-forward blocked", failMode); + } + if (!authKey) { + return upstreamFailure("UPSTREAM_AUTH_KEY missing", failMode); + } + + const payload = { + u: req.u, + m: req.m, + h: req.h, + b: req.b, + ct: req.ct, + r: req.r + }; + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + + try { + const resp = await fetch(upstreamUrl, { + method: "POST", + headers: { + "content-type": "application/json", + "x-upstream-auth": authKey + }, + body: JSON.stringify(payload), + signal: controller.signal + }); + + if (!resp.ok) { + return upstreamFailure("forwarder status " + resp.status, failMode); + } + + // Pass body straight through without parsing — saves CPU and memory. + const body = await resp.text(); + return new Response(body, { + status: 200, + headers: { "content-type": "application/json" } + }); + } catch (err) { + return upstreamFailure(String(err && err.message || err), failMode); + } finally { + clearTimeout(timer); + } +} + +function upstreamFailure(reason, failMode) { + if (failMode === "open") { + console.warn("upstream forwarder failed (falling back to direct):", reason); + return null; // signals caller to fall through to direct fetch + } + return json({ e: "upstream forwarder failed: " + reason }, 502); +} + +function json(obj, status = 200) { + return new Response(JSON.stringify(obj), { + status, + headers: { + "content-type": "application/json" + } + }); +}