mirror of
https://github.com/denuitt1/mhr-cfw.git
synced 2026-05-17 21:24:36 +03:00
Add deploy/upstream-forwarder
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
DOMAIN=example.com
|
||||
LETSENCRYPT_EMAIL=user1234@example.com
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY upstream_forwarder.js .
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
ENV PORT=8787
|
||||
ENV HOST=0.0.0.0
|
||||
|
||||
EXPOSE 8787
|
||||
|
||||
CMD ["node", "upstream_forwarder.js"]
|
||||
@@ -0,0 +1,113 @@
|
||||
# docker-compose.yml
|
||||
|
||||
name: "mhr-cfw-upstream-forwarder-cluster"
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:v3.6
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
- "./traefik.yml:/traefik.yml:ro"
|
||||
- "./data/letsencrypt/acme.json:/letsencrypt/acme.json"
|
||||
networks:
|
||||
- traefik-network
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
# - 8080:8080
|
||||
command:
|
||||
- "--configFile=/traefik.yml"
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.dashboard.rule=Host(`traefik.${DOMAIN}`)"
|
||||
- "traefik.http.routers.dashboard.entrypoints=web,websecure"
|
||||
- "traefik.http.routers.dashboard.service=api@internal"
|
||||
- "traefik.http.routers.dashboard.tls=true"
|
||||
- "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
|
||||
- "traefik.docker.network=traefik-network"
|
||||
portainer:
|
||||
image: portainer/portainer-ce:lts
|
||||
container_name: portainer
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||
- "portainer-data:/data"
|
||||
networks:
|
||||
- traefik-network
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.portainer.rule=Host(`portainer.${DOMAIN}`)"
|
||||
- "traefik.http.routers.portainer.entrypoints=web,websecure"
|
||||
- "traefik.http.routers.portainer.tls=true"
|
||||
- "traefik.http.routers.portainer.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.portainer.loadbalancer.server.port=9000"
|
||||
- "traefik.docker.network=traefik-network"
|
||||
mhr-cfw-upstream-forwarder-node1:
|
||||
image: mhr-cfw-upstream-forwarder-node1
|
||||
build: ./services/mhr-cfw-upstream-forwarder/.
|
||||
container_name: mhr-cfw-upstream-forwarder-node1
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- traefik-network
|
||||
environment:
|
||||
AUTH_KEY: "YOUR_SECRET_KEY" # replace with your own secret key
|
||||
PORT: 8787
|
||||
HOST: 0.0.0.0
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.mhr-cfw-upstream-forwarder-node1.rule=Host(`node1.${DOMAIN}`)"
|
||||
- "traefik.http.routers.mhr-cfw-upstream-forwarder-node1.entrypoints=web,websecure"
|
||||
- "traefik.http.routers.mhr-cfw-upstream-forwarder-node1.tls=true"
|
||||
- "traefik.http.routers.mhr-cfw-upstream-forwarder-node1.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.mhr-cfw-upstream-forwarder-node1.loadbalancer.server.port=8787"
|
||||
- "traefik.docker.network=traefik-network"
|
||||
# Optional: basic healthcheck
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8787/"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
mhr-cfw-upstream-forwarder-node2:
|
||||
image: mhr-cfw-upstream-forwarder-node2
|
||||
build: ./services/mhr-cfw-upstream-forwarder/.
|
||||
container_name: mhr-cfw-upstream-forwarder-node2
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- traefik-network
|
||||
environment:
|
||||
AUTH_KEY: "YOUR_SECRET_KEY" # replace with your own secret key
|
||||
PORT: 8787
|
||||
HOST: 0.0.0.0
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.mhr-cfw-upstream-forwarder-node2.rule=Host(`node2.${DOMAIN}`)"
|
||||
- "traefik.http.routers.mhr-cfw-upstream-forwarder-node2.entrypoints=web,websecure"
|
||||
- "traefik.http.routers.mhr-cfw-upstream-forwarder-node2.tls=true"
|
||||
- "traefik.http.routers.mhr-cfw-upstream-forwarder-node2.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.mhr-cfw-upstream-forwarder-node2.loadbalancer.server.port=8787"
|
||||
- "traefik.docker.network=traefik-network"
|
||||
# Optional: basic healthcheck
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8787/"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
volumes:
|
||||
portainer-data:
|
||||
name: portainer-data
|
||||
external: false
|
||||
|
||||
networks:
|
||||
traefik-network:
|
||||
name: traefik-network
|
||||
driver: bridge
|
||||
external: true
|
||||
@@ -0,0 +1,38 @@
|
||||
# traefik.yml
|
||||
|
||||
global:
|
||||
checkNewVersion: true
|
||||
sendAnonymousUsage: true
|
||||
|
||||
log:
|
||||
level: DEBUG
|
||||
|
||||
api:
|
||||
insecure: false
|
||||
dashboard: true
|
||||
|
||||
providers:
|
||||
docker:
|
||||
#watch: true
|
||||
endpoint: "unix:///var/run/docker.sock"
|
||||
exposedByDefault: false
|
||||
network: traefik-network
|
||||
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
# http:
|
||||
# redirections:
|
||||
# entryPoint:
|
||||
# to: websecure
|
||||
# scheme: https
|
||||
websecure:
|
||||
address: ":443"
|
||||
|
||||
certificatesResolvers:
|
||||
letsencrypt:
|
||||
acme:
|
||||
email: ${LETSENCRYPT_EMAIL}
|
||||
storage: /letsencrypt/acme.json
|
||||
httpChallenge:
|
||||
entryPoint: web
|
||||
@@ -0,0 +1,143 @@
|
||||
// Upstream Forwarder — single-file Node 18+ HTTP server.
|
||||
//
|
||||
// Purpose: Provide a stable exit IP for the Cloudflare Worker relay so
|
||||
// CAPTCHA tokens (Turnstile, reCAPTCHA, hCaptcha) bound to the solving
|
||||
// IP survive verification on the target site.
|
||||
//
|
||||
// Run on a VPS with a stable public IP. Expose behind Caddy/nginx with
|
||||
// TLS — the Worker rejects non-HTTPS forwarder URLs.
|
||||
//
|
||||
// Required env:
|
||||
// AUTH_KEY — must match the Worker's UPSTREAM_AUTH_KEY (>= 32 chars)
|
||||
//
|
||||
// Optional env:
|
||||
// PORT — listen port (default 8787)
|
||||
// HOST — listen host (default 127.0.0.1, so Caddy/nginx fronts it)
|
||||
//
|
||||
// Wire protocol matches main/services/cloudflare-worker/worker.js:
|
||||
// POST /fwd body: { u, m, h, b, ct, r } → { s, h, b } or { e }
|
||||
|
||||
"use strict";
|
||||
|
||||
const http = require("http");
|
||||
|
||||
const AUTH_KEY = process.env.AUTH_KEY || "";
|
||||
const PORT = parseInt(process.env.PORT, 10) || 8787;
|
||||
const HOST = process.env.HOST || "127.0.0.1";
|
||||
|
||||
if (!AUTH_KEY || AUTH_KEY.length < 32) {
|
||||
console.error("FATAL: AUTH_KEY env var missing or shorter than 32 chars.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Mirrors SKIP_HEADERS in main/script/Code.gs:6-9.
|
||||
const SKIP_HEADERS = new Set([
|
||||
"host",
|
||||
"connection",
|
||||
"content-length",
|
||||
"transfer-encoding",
|
||||
"proxy-connection",
|
||||
"proxy-authorization"
|
||||
]);
|
||||
|
||||
const STATUS_PAGE =
|
||||
"<!DOCTYPE html><html><head><title>Forwarder Active</title></head>" +
|
||||
'<body style="font-family:sans-serif;max-width:600px;margin:40px auto">' +
|
||||
'<h1>Forwarder <span style="color:#16a34a;font-weight:700">Active</span></h1>' +
|
||||
"<p>Upstream forwarder for the relay Worker.</p>" +
|
||||
"</body></html>";
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
if (req.method === "GET" && (req.url === "/" || req.url === "")) {
|
||||
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
||||
res.end(STATUS_PAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method !== "POST" || req.url !== "/fwd") {
|
||||
sendJson(res, 404, { e: "not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.headers["x-upstream-auth"] !== AUTH_KEY) {
|
||||
sendJson(res, 401, { e: "unauthorized" });
|
||||
return;
|
||||
}
|
||||
|
||||
const raw = await readBody(req);
|
||||
let body;
|
||||
try {
|
||||
body = JSON.parse(raw);
|
||||
} catch (_) {
|
||||
sendJson(res, 400, { e: "invalid json" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!body.u || typeof body.u !== "string" || !/^https?:\/\//i.test(body.u)) {
|
||||
sendJson(res, 400, { e: "bad url" });
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = {};
|
||||
if (body.h && typeof body.h === "object") {
|
||||
for (const [k, v] of Object.entries(body.h)) {
|
||||
if (typeof v !== "string") continue;
|
||||
if (SKIP_HEADERS.has(k.toLowerCase())) continue;
|
||||
headers[k] = v;
|
||||
}
|
||||
}
|
||||
headers["x-fwd-hop"] = "1";
|
||||
|
||||
const fetchOptions = {
|
||||
method: (body.m || "GET").toUpperCase(),
|
||||
headers,
|
||||
redirect: body.r === false ? "manual" : "follow"
|
||||
};
|
||||
|
||||
if (body.b) {
|
||||
fetchOptions.body = Buffer.from(body.b, "base64");
|
||||
}
|
||||
|
||||
let resp;
|
||||
try {
|
||||
resp = await fetch(body.u, fetchOptions);
|
||||
} catch (err) {
|
||||
sendJson(res, 502, { e: "fetch failed: " + String(err && err.message || err) });
|
||||
return;
|
||||
}
|
||||
|
||||
const buf = Buffer.from(await resp.arrayBuffer());
|
||||
const responseHeaders = {};
|
||||
resp.headers.forEach((v, k) => {
|
||||
responseHeaders[k] = v;
|
||||
});
|
||||
|
||||
sendJson(res, 200, {
|
||||
s: resp.status,
|
||||
h: responseHeaders,
|
||||
b: buf.toString("base64")
|
||||
});
|
||||
} catch (err) {
|
||||
sendJson(res, 500, { e: String(err && err.message || err) });
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(PORT, HOST, () => {
|
||||
console.log("upstream_forwarder listening on " + HOST + ":" + PORT);
|
||||
});
|
||||
|
||||
function readBody(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
req.on("data", c => chunks.push(c));
|
||||
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
||||
req.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function sendJson(res, status, obj) {
|
||||
const body = JSON.stringify(obj);
|
||||
res.writeHead(status, { "content-type": "application/json" });
|
||||
res.end(body);
|
||||
}
|
||||
Reference in New Issue
Block a user