-
Notifications
You must be signed in to change notification settings - Fork 247
Expand file tree
/
Copy pathcloudflare_worker.js
More file actions
151 lines (135 loc) · 4.83 KB
/
cloudflare_worker.js
File metadata and controls
151 lines (135 loc) · 4.83 KB
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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// MasterHttpRelay exit node for Cloudflare Workers.
// Deploy as HTTP endpoint and set PSK to a strong secret.
const PSK = "CHANGE_ME_TO_A_STRONG_SECRET";
const STRIP_HEADERS = new Set([
"host",
"connection",
"content-length",
"transfer-encoding",
"proxy-connection",
"proxy-authorization",
"x-forwarded-for",
"x-forwarded-host",
"x-forwarded-proto",
"x-forwarded-port",
"x-real-ip",
"forwarded",
"via",
// Internal relay hop header — must not propagate to the final target.
"x-mhr-hop",
// Workers cannot decompress gzip/br/deflate — stripping accept-encoding
// forces targets to reply with plain bodies the Worker can forward as-is.
"accept-encoding",
]);
function decodeBase64ToBytes(input) {
const bin = atob(input);
const out = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
return out;
}
function encodeBytesToBase64(bytes) {
let bin = "";
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
return btoa(bin);
}
function sanitizeHeaders(h) {
const out = {};
if (!h || typeof h !== "object") return out;
for (const [k, v] of Object.entries(h)) {
if (!k) continue;
if (STRIP_HEADERS.has(k.toLowerCase())) continue;
out[k] = String(v ?? "");
}
return out;
}
export default {
async fetch(req) {
try {
// Cloudflare dashboard and browsers commonly test a Worker with GET.
// Return a friendly health response so users don't misread it as failure.
if (req.method === "GET") {
return Response.json(
{
ok: true,
status: "healthy",
message: "Everything is OK. Worker is deployed and reachable.",
usage: "Send POST with relay payload for actual proxy requests.",
},
{ status: 200 }
);
}
if (req.method !== "POST") {
return Response.json(
{
e: "method_not_allowed",
message: "Use POST for relay requests. GET is only a health check.",
},
{ status: 405 }
);
}
const body = await req.json();
if (!body || typeof body !== "object") {
return Response.json({ e: "bad_json" }, { status: 400 });
}
if (!PSK) {
return Response.json({ e: "server_psk_missing" }, { status: 500 });
}
const k = String(body.k ?? "");
const u = String(body.u ?? "");
const m = String(body.m ?? "GET").toUpperCase();
const h = sanitizeHeaders(body.h);
const b64 = body.b;
if (k !== PSK) return Response.json({ e: "unauthorized" }, { status: 401 });
if (!/^https?:\/\//i.test(u)) return Response.json({ e: "bad_url" }, { status: 400 });
// ── Loop detection ────────────────────────────────────────────────────
// Case 1 — self-loop: target URL resolves back to this Worker.
// Happens when a user sets exit_node_url to the Worker URL itself.
try {
const targetHost = new URL(u).hostname.toLowerCase();
const workerHost = new URL(req.url).hostname.toLowerCase();
if (targetHost === workerHost) {
return Response.json(
{ e: "loop_detected", detail: "target URL resolves to this Worker" },
{ status: 508 }
);
}
} catch (_) {
// Malformed URL already caught by the regex above; ignore parse errors.
}
// Case 2 — GAS→Worker→GAS loop: the incoming request was relayed from
// a Google Apps Script instance (x-mhr-hop header set), and the
// target is another Apps Script script URL.
// Without this guard, a misconfigured chain would bounce between
// Apps Script and Cloudflare until quota is exhausted.
const hopHeader = req.headers.get("x-mhr-hop");
if (hopHeader && /\/macros\/s\//i.test(u)) {
return Response.json(
{ e: "loop_detected", detail: "GAS→Worker→GAS relay loop" },
{ status: 508 }
);
}
let payload;
if (typeof b64 === "string" && b64.length > 0) payload = decodeBase64ToBytes(b64);
const requestBody = payload ? Uint8Array.from(payload) : undefined;
const resp = await fetch(u, {
method: m,
headers: h,
body: requestBody,
redirect: "manual",
});
const data = new Uint8Array(await resp.arrayBuffer());
const respHeaders = {};
resp.headers.forEach((value, key) => {
respHeaders[key] = value;
});
return Response.json({
s: resp.status,
h: respHeaders,
b: encodeBytesToBase64(data),
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return Response.json({ e: message }, { status: 500 });
}
},
};