File size: 4,527 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
/**
 * 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" },
      });
    }
  },
};
`;
}