mirror of
https://github.com/masterking32/MasterHttpRelayVPN.git
synced 2026-05-17 21:24:37 +03:00
feat: add VPS exit node installer and server scripts
This commit is contained in:
@@ -0,0 +1,243 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# MasterHttpRelayVPN — VPS Exit Node Installer (Linux only)
|
||||||
|
# =============================================================================
|
||||||
|
# Installs and configures the vps_exit_node.py relay server as a systemd
|
||||||
|
# service on a fresh Linux VPS.
|
||||||
|
#
|
||||||
|
# What this script does:
|
||||||
|
# 1. Verifies the OS is Linux (aborts otherwise)
|
||||||
|
# 2. Installs Python 3 if not present
|
||||||
|
# 3. Creates /opt/exit-node/ and copies vps_exit_node.py there
|
||||||
|
# 4. Prompts for a PSK (or generates one automatically)
|
||||||
|
# 5. Prompts for the listen port (default: 8181)
|
||||||
|
# 6. Writes /etc/exit-node.env (holds EXIT_NODE_PSK — readable by root only)
|
||||||
|
# 7. Installs a systemd service that auto-starts on boot
|
||||||
|
# 8. Opens the chosen port in ufw / firewalld if either tool is present
|
||||||
|
# 9. Prints the final config snippet to paste into config.json
|
||||||
|
#
|
||||||
|
# Usage (run as root or with sudo):
|
||||||
|
# bash setup_vps_exit_node.sh
|
||||||
|
#
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── Colours ──────────────────────────────────────────────────────────────────
|
||||||
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
|
||||||
|
|
||||||
|
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||||
|
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||||
|
header() { echo -e "\n${BOLD}${CYAN}$*${NC}"; }
|
||||||
|
|
||||||
|
# ── Platform check ────────────────────────────────────────────────────────────
|
||||||
|
if [[ "$(uname -s)" != "Linux" ]]; then
|
||||||
|
error "This installer only supports Linux. Detected: $(uname -s)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Root / sudo check ─────────────────────────────────────────────────────────
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
error "Please run as root or with sudo:"
|
||||||
|
echo " sudo bash $0"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Detect script directory so we can find vps_exit_node.py ──────────────────
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PYTHON_SCRIPT="$SCRIPT_DIR/vps_exit_node.py"
|
||||||
|
|
||||||
|
if [[ ! -f "$PYTHON_SCRIPT" ]]; then
|
||||||
|
error "vps_exit_node.py not found at: $PYTHON_SCRIPT"
|
||||||
|
error "Run this script from the apps_script/ directory of MasterHttpRelayVPN."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
header "Step 1/7 — Checking Python 3"
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
PYTHON_BIN=""
|
||||||
|
for candidate in python3 python3.12 python3.11 python3.10; do
|
||||||
|
if command -v "$candidate" &>/dev/null; then
|
||||||
|
VER=$("$candidate" -c 'import sys; print("%d%d" % sys.version_info[:2])')
|
||||||
|
if [[ "$VER" -ge 310 ]]; then
|
||||||
|
PYTHON_BIN=$(command -v "$candidate")
|
||||||
|
info "Found $PYTHON_BIN ($(\"$PYTHON_BIN\" --version 2>&1))"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$PYTHON_BIN" ]]; then
|
||||||
|
info "Python 3.10+ not found — attempting install..."
|
||||||
|
if command -v apt-get &>/dev/null; then
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y python3 python3-minimal
|
||||||
|
elif command -v dnf &>/dev/null; then
|
||||||
|
dnf install -y python3
|
||||||
|
elif command -v yum &>/dev/null; then
|
||||||
|
yum install -y python3
|
||||||
|
elif command -v pacman &>/dev/null; then
|
||||||
|
pacman -Sy --noconfirm python
|
||||||
|
else
|
||||||
|
error "Cannot determine package manager. Please install Python 3.10+ manually."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
PYTHON_BIN=$(command -v python3)
|
||||||
|
info "Installed: $PYTHON_BIN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
header "Step 2/7 — Collecting configuration"
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Port
|
||||||
|
read -rp $'\n'"Listen port [default: 8181]: " INPUT_PORT
|
||||||
|
PORT="${INPUT_PORT:-8181}"
|
||||||
|
if ! [[ "$PORT" =~ ^[0-9]+$ ]] || [[ "$PORT" -lt 1 ]] || [[ "$PORT" -gt 65535 ]]; then
|
||||||
|
error "Invalid port: $PORT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
info "Port: $PORT"
|
||||||
|
|
||||||
|
# PSK
|
||||||
|
echo ""
|
||||||
|
read -rp "Pre-shared key (leave empty to auto-generate): " INPUT_PSK
|
||||||
|
if [[ -z "$INPUT_PSK" ]]; then
|
||||||
|
PSK=$("$PYTHON_BIN" -c "import secrets; print(secrets.token_hex(32))")
|
||||||
|
info "Auto-generated PSK: ${BOLD}${PSK}${NC}"
|
||||||
|
warn "Save this PSK — you will need it in config.json on your client."
|
||||||
|
else
|
||||||
|
PSK="$INPUT_PSK"
|
||||||
|
info "Using provided PSK."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
header "Step 3/7 — Installing server files"
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
INSTALL_DIR="/opt/exit-node"
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
cp "$PYTHON_SCRIPT" "$INSTALL_DIR/vps_exit_node.py"
|
||||||
|
chmod 700 "$INSTALL_DIR/vps_exit_node.py"
|
||||||
|
info "Installed to $INSTALL_DIR/vps_exit_node.py"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
header "Step 4/7 — Writing environment file"
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
ENV_FILE="/etc/exit-node.env"
|
||||||
|
cat > "$ENV_FILE" <<EOF
|
||||||
|
# MasterHttpRelayVPN VPS exit node — environment configuration
|
||||||
|
# Generated by setup_vps_exit_node.sh
|
||||||
|
EXIT_NODE_PSK=${PSK}
|
||||||
|
EOF
|
||||||
|
chmod 600 "$ENV_FILE"
|
||||||
|
info "Environment file written to $ENV_FILE (mode 600)"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
header "Step 5/7 — Installing systemd service"
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
SERVICE_FILE="/etc/systemd/system/exit-node.service"
|
||||||
|
cat > "$SERVICE_FILE" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=MasterHttpRelayVPN Exit Node
|
||||||
|
Documentation=https://github.com/masterking32/MasterHttpRelayVPN
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
EnvironmentFile=/etc/exit-node.env
|
||||||
|
ExecStart=${PYTHON_BIN} ${INSTALL_DIR}/vps_exit_node.py --port ${PORT}
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
# Security hardening
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
PrivateTmp=yes
|
||||||
|
ProtectSystem=strict
|
||||||
|
ReadWritePaths=/var/log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable exit-node
|
||||||
|
systemctl restart exit-node
|
||||||
|
info "Service installed and started."
|
||||||
|
|
||||||
|
# Give it a moment to confirm it really started.
|
||||||
|
sleep 2
|
||||||
|
if systemctl is-active --quiet exit-node; then
|
||||||
|
info "Service is ${GREEN}running${NC}."
|
||||||
|
else
|
||||||
|
warn "Service may not have started. Check: journalctl -u exit-node -n 30"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
header "Step 6/7 — Firewall"
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if command -v ufw &>/dev/null; then
|
||||||
|
ufw allow "$PORT"/tcp comment "exit-node" || true
|
||||||
|
info "ufw rule added for port $PORT/tcp"
|
||||||
|
elif command -v firewall-cmd &>/dev/null; then
|
||||||
|
firewall-cmd --permanent --add-port="$PORT"/tcp || true
|
||||||
|
firewall-cmd --reload || true
|
||||||
|
info "firewalld rule added for port $PORT/tcp"
|
||||||
|
else
|
||||||
|
warn "No ufw or firewalld found. Make sure port $PORT/tcp is open in your VPS firewall panel."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
header "Step 7/7 — Health check"
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
HEALTH_URL="http://127.0.0.1:${PORT}/"
|
||||||
|
if command -v curl &>/dev/null; then
|
||||||
|
HEALTH=$(curl -sf --max-time 5 "$HEALTH_URL" 2>/dev/null || echo "")
|
||||||
|
if echo "$HEALTH" | grep -q '"ok"'; then
|
||||||
|
info "Health check OK: $HEALTH"
|
||||||
|
else
|
||||||
|
warn "Health check returned unexpected response. Check: journalctl -u exit-node -n 30"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "curl not found — skipping health check. You can test manually:"
|
||||||
|
echo " curl http://127.0.0.1:${PORT}/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
PUBLIC_IP=$(curl -sf --max-time 5 https://ifconfig.me 2>/dev/null || echo "YOUR-VPS-IP")
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}${GREEN}============================================================${NC}"
|
||||||
|
echo -e "${BOLD} Installation complete!${NC}"
|
||||||
|
echo -e "${BOLD}${GREEN}============================================================${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " Service status : ${CYAN}systemctl status exit-node${NC}"
|
||||||
|
echo -e " Live logs : ${CYAN}journalctl -fu exit-node${NC}"
|
||||||
|
echo -e " Uninstall : ${CYAN}systemctl disable --now exit-node && rm $SERVICE_FILE $ENV_FILE${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}Add this to config.json on your client machine:${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}"
|
||||||
|
cat <<JSON
|
||||||
|
"exit_node": {
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "vps",
|
||||||
|
"url": "http://${PUBLIC_IP}:${PORT}",
|
||||||
|
"psk": "${PSK}",
|
||||||
|
"mode": "full",
|
||||||
|
"hosts": []
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
echo -e "${NC}"
|
||||||
|
warn "For production: put nginx/Caddy in front and use HTTPS."
|
||||||
|
warn "Replace the IP with your domain name if you have one."
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
MasterHttpRelayVPN — VPS Exit Node Server (Linux only)
|
||||||
|
|
||||||
|
A lightweight HTTP relay server you can run on your own Linux VPS.
|
||||||
|
It receives relay requests forwarded by Apps Script (on behalf of
|
||||||
|
MasterHttpRelayVPN) and makes the actual outbound HTTP/HTTPS connections
|
||||||
|
using your VPS's IP address.
|
||||||
|
|
||||||
|
Traffic path with this server:
|
||||||
|
Browser → Local Proxy → Apps Script (Google) → THIS SERVER → Target website
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 vps_exit_node.py --psk YOUR_STRONG_SECRET [--host 0.0.0.0] [--port 8181]
|
||||||
|
|
||||||
|
Or use the environment variable instead of --psk:
|
||||||
|
export EXIT_NODE_PSK=YOUR_STRONG_SECRET
|
||||||
|
python3 vps_exit_node.py
|
||||||
|
|
||||||
|
For easy installation on a fresh Linux VPS, use the provided installer:
|
||||||
|
bash setup_vps_exit_node.sh
|
||||||
|
|
||||||
|
For production use, run behind a reverse proxy (nginx / Caddy) that
|
||||||
|
handles TLS so the endpoint is reachable over HTTPS.
|
||||||
|
|
||||||
|
NOTE: This script is designed for Linux only. It will refuse to start
|
||||||
|
on Windows or macOS.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import http.server
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import socketserver
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Logging
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||||
|
stream=sys.stdout,
|
||||||
|
)
|
||||||
|
log = logging.getLogger("exit-node")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Constants
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Headers that must never be forwarded to the upstream target because they
|
||||||
|
# are connection-local, injected by the relay chain, or could leak caller
|
||||||
|
# information.
|
||||||
|
_STRIP_HEADERS = frozenset(
|
||||||
|
[
|
||||||
|
"host",
|
||||||
|
"connection",
|
||||||
|
"content-length",
|
||||||
|
"transfer-encoding",
|
||||||
|
"keep-alive",
|
||||||
|
"te",
|
||||||
|
"trailer",
|
||||||
|
"upgrade",
|
||||||
|
"proxy-connection",
|
||||||
|
"proxy-authorization",
|
||||||
|
"proxy-authenticate",
|
||||||
|
"x-forwarded-for",
|
||||||
|
"x-forwarded-host",
|
||||||
|
"x-forwarded-proto",
|
||||||
|
"x-forwarded-port",
|
||||||
|
"x-real-ip",
|
||||||
|
"forwarded",
|
||||||
|
"via",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Maximum request body accepted from the relay chain (32 MiB).
|
||||||
|
_MAX_REQUEST_BODY = 32 * 1024 * 1024
|
||||||
|
|
||||||
|
# Maximum response body forwarded back (64 MiB).
|
||||||
|
_MAX_RESPONSE_BODY = 64 * 1024 * 1024
|
||||||
|
|
||||||
|
# Outbound request timeout in seconds.
|
||||||
|
_OUTBOUND_TIMEOUT = 30
|
||||||
|
|
||||||
|
# Pre-shared key loaded at startup.
|
||||||
|
_PSK: str = ""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _sanitize_headers(raw: object) -> dict[str, str]:
|
||||||
|
"""Return a clean header dict, dropping hop-by-hop and proxy headers."""
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
return {}
|
||||||
|
out: dict[str, str] = {}
|
||||||
|
for k, v in raw.items():
|
||||||
|
if not k or not isinstance(k, str):
|
||||||
|
continue
|
||||||
|
if k.lower() in _STRIP_HEADERS:
|
||||||
|
continue
|
||||||
|
out[k] = str(v) if v is not None else ""
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_url(url: str) -> bool:
|
||||||
|
"""Return True only for plain http:// or https:// URLs (no localhost / LAN)."""
|
||||||
|
if not re.match(r"^https?://", url, re.IGNORECASE):
|
||||||
|
return False
|
||||||
|
# Block requests to loopback / private addresses to prevent SSRF.
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
host = urlparse(url).hostname or ""
|
||||||
|
host = host.lower().rstrip(".")
|
||||||
|
# Reject empty, numeric localhost, and obviously private hostnames.
|
||||||
|
_PRIVATE = re.compile(
|
||||||
|
r"^("
|
||||||
|
r"localhost"
|
||||||
|
r"|127\.\d+\.\d+\.\d+"
|
||||||
|
r"|::1"
|
||||||
|
r"|0\.0\.0\.0"
|
||||||
|
r"|10\.\d+\.\d+\.\d+"
|
||||||
|
r"|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+"
|
||||||
|
r"|192\.168\.\d+\.\d+"
|
||||||
|
r"|169\.254\.\d+\.\d+"
|
||||||
|
r"|fc[0-9a-f]{2}:.*"
|
||||||
|
r"|fd[0-9a-f]{2}:.*"
|
||||||
|
r")$"
|
||||||
|
)
|
||||||
|
if _PRIVATE.match(host):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _relay_request(
|
||||||
|
url: str, method: str, headers: dict[str, str], body: bytes
|
||||||
|
) -> dict:
|
||||||
|
"""Perform the outbound HTTP/HTTPS request and return a relay-JSON dict."""
|
||||||
|
request = urllib.request.Request(url, method=method, headers=headers)
|
||||||
|
if body:
|
||||||
|
request.data = body
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=_OUTBOUND_TIMEOUT) as resp:
|
||||||
|
data = resp.read(_MAX_RESPONSE_BODY)
|
||||||
|
resp_headers: dict[str, str] = {}
|
||||||
|
for k, v in resp.headers.items():
|
||||||
|
resp_headers[k] = v
|
||||||
|
return {
|
||||||
|
"s": resp.status,
|
||||||
|
"h": resp_headers,
|
||||||
|
"b": base64.b64encode(data).decode(),
|
||||||
|
}
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
data = exc.read(_MAX_RESPONSE_BODY) if exc.fp else b""
|
||||||
|
resp_headers = {}
|
||||||
|
if exc.headers:
|
||||||
|
for k, v in exc.headers.items():
|
||||||
|
resp_headers[k] = v
|
||||||
|
return {
|
||||||
|
"s": exc.code,
|
||||||
|
"h": resp_headers,
|
||||||
|
"b": base64.b64encode(data).decode(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# HTTP request handler
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class _ExitNodeHandler(http.server.BaseHTTPRequestHandler):
|
||||||
|
# Suppress the default per-request access log lines; we emit our own.
|
||||||
|
def log_message(self, fmt, *args): # noqa: D102
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _send_json(self, status: int, obj: dict) -> None:
|
||||||
|
body = json.dumps(obj).encode()
|
||||||
|
self.send_response(status)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def do_GET(self): # noqa: N802
|
||||||
|
"""Health-check endpoint — returns a friendly JSON status."""
|
||||||
|
self._send_json(
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"ok": True,
|
||||||
|
"status": "healthy",
|
||||||
|
"message": "VPS exit node is running.",
|
||||||
|
"usage": "Send POST with relay payload for actual proxy requests.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def do_POST(self): # noqa: N802
|
||||||
|
"""Relay endpoint — receives a JSON relay payload, fetches the URL."""
|
||||||
|
content_length = int(self.headers.get("Content-Length") or 0)
|
||||||
|
if content_length <= 0:
|
||||||
|
self._send_json(400, {"e": "empty_body"})
|
||||||
|
return
|
||||||
|
if content_length > _MAX_REQUEST_BODY:
|
||||||
|
self._send_json(413, {"e": "request_too_large"})
|
||||||
|
return
|
||||||
|
|
||||||
|
raw = self.rfile.read(content_length)
|
||||||
|
try:
|
||||||
|
body = json.loads(raw)
|
||||||
|
except Exception:
|
||||||
|
self._send_json(400, {"e": "bad_json"})
|
||||||
|
return
|
||||||
|
|
||||||
|
if not isinstance(body, dict):
|
||||||
|
self._send_json(400, {"e": "bad_json"})
|
||||||
|
return
|
||||||
|
|
||||||
|
k = str(body.get("k") or "")
|
||||||
|
u = str(body.get("u") or "")
|
||||||
|
m = str(body.get("m") or "GET").upper()
|
||||||
|
h = _sanitize_headers(body.get("h"))
|
||||||
|
b64 = body.get("b")
|
||||||
|
|
||||||
|
if not _PSK:
|
||||||
|
self._send_json(500, {"e": "server_psk_missing"})
|
||||||
|
return
|
||||||
|
|
||||||
|
if k != _PSK:
|
||||||
|
log.warning("Rejected unauthorized request from %s", self.client_address[0])
|
||||||
|
self._send_json(401, {"e": "unauthorized"})
|
||||||
|
return
|
||||||
|
|
||||||
|
if not _safe_url(u):
|
||||||
|
self._send_json(400, {"e": "bad_url"})
|
||||||
|
return
|
||||||
|
|
||||||
|
payload_bytes = b""
|
||||||
|
if isinstance(b64, str) and b64:
|
||||||
|
try:
|
||||||
|
payload_bytes = base64.b64decode(b64)
|
||||||
|
except Exception:
|
||||||
|
self._send_json(400, {"e": "bad_base64"})
|
||||||
|
return
|
||||||
|
|
||||||
|
log.info("Relaying %s %s", m, u[:100])
|
||||||
|
try:
|
||||||
|
result = _relay_request(u, m, h, payload_bytes)
|
||||||
|
except Exception as exc:
|
||||||
|
log.warning("Relay error for %s: %s", u[:80], exc)
|
||||||
|
self._send_json(500, {"e": str(exc) or type(exc).__name__})
|
||||||
|
return
|
||||||
|
|
||||||
|
log.info("Relay OK %s → HTTP %d (%d B)", u[:80], result["s"], len(result.get("b", "")))
|
||||||
|
self._send_json(200, result)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Server entry-point
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class _ThreadedHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
|
||||||
|
"""HTTP server that handles each request in a separate thread."""
|
||||||
|
|
||||||
|
allow_reuse_address = True
|
||||||
|
daemon_threads = True
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="MasterHttpRelayVPN — VPS Exit Node Server",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog=__doc__,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--psk",
|
||||||
|
default="",
|
||||||
|
metavar="SECRET",
|
||||||
|
help="Pre-shared key for authentication (or set EXIT_NODE_PSK env var).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--host",
|
||||||
|
default="0.0.0.0",
|
||||||
|
help="Host/IP to listen on (default: 0.0.0.0).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--port",
|
||||||
|
type=int,
|
||||||
|
default=8181,
|
||||||
|
help="TCP port to listen on (default: 8181).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--log-level",
|
||||||
|
default="INFO",
|
||||||
|
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
|
||||||
|
help="Logging verbosity (default: INFO).",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
logging.getLogger().setLevel(args.log_level)
|
||||||
|
|
||||||
|
if sys.platform != "linux":
|
||||||
|
log.error(
|
||||||
|
"This VPS exit node is designed for Linux only. "
|
||||||
|
"Current platform: %s", sys.platform
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
global _PSK
|
||||||
|
_PSK = (args.psk or os.environ.get("EXIT_NODE_PSK", "")).strip()
|
||||||
|
if not _PSK:
|
||||||
|
log.error(
|
||||||
|
"No PSK configured. Pass --psk YOUR_SECRET or set the "
|
||||||
|
"EXIT_NODE_PSK environment variable."
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
server = _ThreadedHTTPServer((args.host, args.port), _ExitNodeHandler)
|
||||||
|
log.info(
|
||||||
|
"VPS exit node listening on %s:%d (press Ctrl+C to stop)",
|
||||||
|
args.host,
|
||||||
|
args.port,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
server.serve_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
log.info("Shutting down.")
|
||||||
|
server.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1122,6 +1122,10 @@ class DomainFronter:
|
|||||||
"worker": "cloudflare",
|
"worker": "cloudflare",
|
||||||
"cf": "cloudflare",
|
"cf": "cloudflare",
|
||||||
"deno_deploy": "deno",
|
"deno_deploy": "deno",
|
||||||
|
"self_hosted": "vps",
|
||||||
|
"self-hosted": "vps",
|
||||||
|
"selfhosted": "vps",
|
||||||
|
"server": "vps",
|
||||||
}
|
}
|
||||||
return aliases.get(provider, provider or "custom")
|
return aliases.get(provider, provider or "custom")
|
||||||
|
|
||||||
@@ -1160,6 +1164,12 @@ class DomainFronter:
|
|||||||
selected = _pick_from(en_cfg, "deno_url") or _pick_from(
|
selected = _pick_from(en_cfg, "deno_url") or _pick_from(
|
||||||
providers, "deno", "deno_deploy",
|
providers, "deno", "deno_deploy",
|
||||||
)
|
)
|
||||||
|
elif provider == "vps":
|
||||||
|
selected = _pick_from(
|
||||||
|
en_cfg, "vps_url", "server_url", "self_hosted_url",
|
||||||
|
) or _pick_from(
|
||||||
|
providers, "vps", "self_hosted", "server",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
selected = ""
|
selected = ""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user