mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-18 06:44:35 +03:00
c12ffd4dd4
The val.town founder asked us not to promote using their service. This commit removes every val.town reference from the codebase and rewrites the exit-node guides to be platform-agnostic. Changes: - Renamed assets/exit_node/valtown.ts → assets/exit_node/exit_node.ts. TypeScript itself is unchanged — same web-standard Request/Response/ fetch API that runs on any serverless runtime. - Rewrote assets/exit_node/README.md and README.fa.md to recommend Deno Deploy as the primary host for users who want a free serverless TS endpoint, with fly.io and your-own-VPS as alternatives. CF Workers is explicitly called out as not-helpful (CF outbound is still on CF's flagged IP space). - Updated all val.town mentions in source comments (src/config.rs, src/domain_fronter.rs, src/bin/ui.rs) to neutral wording. - Updated config.exit-node.example.json `_comment` strings and the example URL. - Updated main README.md FAQ entries (Persian + English) and docs/guide.md / docs/guide.fa.md. - Old changelog files (v1.9.4 / v1.9.5 / v1.9.9) had val.town mentions retroactively replaced too — same redaction principle. - Bumped to v1.9.10 with a changelog noting the rename + Telegram channel brief format from earlier today. Users who already have an exit node deployed (on whichever host they picked) don't need to change anything — the wire protocol is identical and the renamed script is byte-identical to the old one. Tests: 179 lib + 35 tunnel-node green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
165 lines
5.6 KiB
TypeScript
165 lines
5.6 KiB
TypeScript
// mhrv-rs exit node — deploy as an HTTP endpoint on any serverless
|
|
// TypeScript host with a public IP that isn't a Google datacenter
|
|
// (Deno Deploy, fly.io, your own VPS, etc.). Uses only web-standard
|
|
// `Request` / `Response` / `fetch` so it's portable across runtimes.
|
|
//
|
|
// Purpose: chain client → Apps Script → this exit node → destination.
|
|
// Apps Script's UrlFetchApp can't reach Cloudflare-protected sites that
|
|
// flag Google datacenter IPs as bots (chatgpt.com, claude.ai, grok.com,
|
|
// many other CF-fronted SaaS). This exit node sits between Apps Script
|
|
// and the destination; the destination sees the exit node's outbound IP
|
|
// (generally not flagged as Google datacenter) and accepts the request.
|
|
//
|
|
// Setup:
|
|
// 1. Pick a host that runs web-standard fetch handlers (e.g. Deno
|
|
// Deploy, fly.io with a thin server wrapper, or any cheap VPS
|
|
// running Deno / Node + this script as a handler).
|
|
// 2. Paste the contents of this file as the request handler.
|
|
// 3. Set PSK below to a strong secret (`openssl rand -hex 32` from
|
|
// a terminal — DO NOT leave the placeholder in production).
|
|
// 4. Deploy and copy the public URL of the deployed handler.
|
|
// 5. In mhrv-rs config.json, add:
|
|
// "exit_node": {
|
|
// "enabled": true,
|
|
// "relay_url": "https://your-deployed-exit-node.example.com",
|
|
// "psk": "<the same PSK you set above>",
|
|
// "mode": "selective",
|
|
// "hosts": ["chatgpt.com", "claude.ai", "x.com", "grok.com"]
|
|
// }
|
|
//
|
|
// Threat model: PSK is the only thing keeping this from being an open
|
|
// proxy on the public internet. Treat it like a password: do not commit
|
|
// to source control, do not share publicly, rotate if leaked. The exit
|
|
// node refuses all requests that don't carry the matching PSK.
|
|
//
|
|
// Failure mode: if the exit node is unreachable, mhrv-rs falls back to
|
|
// the regular Apps Script relay automatically — the only consequence
|
|
// of an offline exit node is that ChatGPT/Claude/Grok stop working;
|
|
// other sites are unaffected.
|
|
|
|
const PSK = "CHANGE_ME_TO_A_STRONG_SECRET";
|
|
|
|
// Headers the client may send that must NOT be forwarded to the
|
|
// destination — they're hop-by-hop or would break re-encoding.
|
|
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",
|
|
]);
|
|
|
|
function decodeBase64ToBytes(input: string): Uint8Array {
|
|
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: Uint8Array): string {
|
|
let bin = "";
|
|
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
return btoa(bin);
|
|
}
|
|
|
|
function sanitizeHeaders(h: unknown): Record<string, string> {
|
|
const out: Record<string, string> = {};
|
|
if (!h || typeof h !== "object") return out;
|
|
for (const [k, v] of Object.entries(h as Record<string, unknown>)) {
|
|
if (!k) continue;
|
|
if (STRIP_HEADERS.has(k.toLowerCase())) continue;
|
|
out[k] = String(v ?? "");
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export default async function (req: Request): Promise<Response> {
|
|
// Fail closed on the placeholder PSK so a fresh deploy without setup
|
|
// can't accidentally serve as an open relay.
|
|
if (PSK === "CHANGE_ME_TO_A_STRONG_SECRET") {
|
|
return Response.json(
|
|
{
|
|
e:
|
|
"exit_node misconfigured: PSK is still the placeholder. Set " +
|
|
"a strong secret in the source before deploying.",
|
|
},
|
|
{ status: 503 },
|
|
);
|
|
}
|
|
|
|
try {
|
|
if (req.method !== "POST") {
|
|
return Response.json({ e: "method_not_allowed" }, { status: 405 });
|
|
}
|
|
|
|
const body = await req.json();
|
|
if (!body || typeof body !== "object") {
|
|
return Response.json({ e: "bad_json" }, { status: 400 });
|
|
}
|
|
|
|
const k = String((body as any).k ?? "");
|
|
const u = String((body as any).u ?? "");
|
|
const m = String((body as any).m ?? "GET").toUpperCase();
|
|
const h = sanitizeHeaders((body as any).h);
|
|
const b64 = (body as any).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 guard: if u points at this exit node's own host, refuse.
|
|
// Without this, a misconfigured client could chain exit-node →
|
|
// exit-node → exit-node → ... and burn the host's runtime budget.
|
|
try {
|
|
const reqUrl = new URL(req.url);
|
|
const dstUrl = new URL(u);
|
|
if (
|
|
reqUrl.host === dstUrl.host &&
|
|
reqUrl.protocol === dstUrl.protocol
|
|
) {
|
|
return Response.json({ e: "exit-node loop refused" }, { status: 400 });
|
|
}
|
|
} catch {
|
|
// Malformed URL — let the fetch below 400.
|
|
}
|
|
|
|
let payload: Uint8Array | undefined;
|
|
if (typeof b64 === "string" && b64.length > 0) {
|
|
payload = decodeBase64ToBytes(b64);
|
|
}
|
|
|
|
const resp = await fetch(u, {
|
|
method: m,
|
|
headers: h,
|
|
body: payload,
|
|
redirect: "manual",
|
|
});
|
|
|
|
const data = new Uint8Array(await resp.arrayBuffer());
|
|
const respHeaders: Record<string, string> = {};
|
|
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 });
|
|
}
|
|
}
|