Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 21 additions & 19 deletions apps_script/Code.gs
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ const SAFE_REPLAY_METHODS = { GET: 1, HEAD: 1, OPTIONS: 1 };
function doPost(e) {
try {
var req = JSON.parse(e.postData.contents);
if (req.k !== AUTH_KEY) return _json({ e: "unauthorized" });
if (req.k !== AUTH_KEY) return _api(false, "unauthorized", "auth key mismatch", null);

// Batch mode: { k, q: [...] }
if (Array.isArray(req.q)) return _doBatch(req.q);

// Single mode
return _doSingle(req);
} catch (err) {
return _json({ e: String(err) });
return _api(false, "bad_request", String(err), null);
}
}

Expand All @@ -72,14 +72,14 @@ function _maybeGzip(bytes) {

function _doSingle(req) {
if (!req.u || typeof req.u !== "string" || !req.u.match(/^https?:\/\//i)) {
return _json({ e: "bad url" });
return _api(false, "bad_url", "url must be http(s)", null);
}
// Loop guard: refuse to relay back to any Apps Script deployment.
// This fires when an exit node URL is misconfigured to point at a GAS
// script — without this check the script would call itself indefinitely
// and burn through the daily UrlFetch quota in seconds.
if (_GAS_URL_RE.test(req.u)) {
return _json({ e: "loop detected: relay target cannot be a Google Apps Script URL" });
return _api(false, "loop_detected", "relay target cannot be a Google Apps Script URL", null);
}
var opts = _buildOpts(req);
var resp = UrlFetchApp.fetch(req.u, opts);
Expand All @@ -90,7 +90,7 @@ function _doSingle(req) {
b: Utilities.base64Encode(gz.b),
};
if (gz.gz) result.gz = 1;
return _json(result);
return _api(true, "ok", "relay_success", result);
}

function _doBatch(items) {
Expand Down Expand Up @@ -178,7 +178,7 @@ function _doBatch(items) {
}
}
}
return _json({ q: results });
return _api(true, "ok", "batch_relay_success", { q: results });
}

function _buildOpts(req) {
Expand Down Expand Up @@ -221,19 +221,21 @@ function _respHeaders(resp) {
}

function doGet(e) {
return HtmlService.createHtmlOutput(
"<!DOCTYPE html><html><head><title>My App</title></head>" +
'<body style="font-family:sans-serif;max-width:600px;margin:40px auto">' +
"<h1>Welcome</h1><p>This application is running normally.</p>" +
"</body></html>"
);
return _api(true, "healthy", "deployment is reachable", {
now_ms: Date.now(),
quota_note: "apps script quotas are managed by Google account limits"
});
}

function _json(obj) {
// HtmlService responses can stay on script.google.com for /dev, while
// ContentService commonly bounces through script.googleusercontent.com.
// The Python client extracts the JSON payload from the body either way.
return HtmlService.createHtmlOutput(JSON.stringify(obj)).setXFrameOptionsMode(
HtmlService.XFrameOptionsMode.ALLOWALL
);
function _api(ok, code, message, data) {
return ContentService
.createTextOutput(
JSON.stringify({
ok: !!ok,
code: String(code || (ok ? "ok" : "error")),
message: String(message || ""),
data: data === undefined ? null : data
})
)
.setMimeType(ContentService.MimeType.JSON);
}
38 changes: 38 additions & 0 deletions apps_script/vps_exit_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import logging
import os
import re
import socket
import socketserver
import sys
import urllib.error
Expand Down Expand Up @@ -209,6 +210,23 @@ def _relay_request(
}




def _relay_udp_packet(host: str, port: int, payload: bytes) -> dict:
"""Send one UDP packet and return one response packet (best effort)."""
if not host or port <= 0 or port > 65535:
return {"e": "bad_udp_target"}
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
sock.settimeout(2.0)
sock.sendto(payload, (host, port))
data, _ = sock.recvfrom(65535)
return {"ok": True, "payload": base64.b64encode(data).decode()}
except Exception as exc:
return {"e": str(exc) or type(exc).__name__}
finally:
sock.close()

# ---------------------------------------------------------------------------
# HTTP request handler
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -265,6 +283,7 @@ def do_POST(self): # noqa: N802
m = str(body.get("m") or "GET").upper()
h = _sanitize_headers(body.get("h"))
b64 = body.get("b")
udp_mode = bool(body.get("udp"))

if not _PSK:
self._send_json(500, {"e": "server_psk_missing"})
Expand All @@ -275,6 +294,25 @@ def do_POST(self): # noqa: N802
self._send_json(401, {"e": "unauthorized"})
return


if udp_mode:
host = str(body.get("host") or "")
try:
port = int(body.get("port") or 0)
except Exception:
port = 0
payload = b""
pb64 = body.get("payload")
if isinstance(pb64, str) and pb64:
try:
payload = base64.b64decode(pb64)
except Exception:
self._send_json(400, {"e": "bad_udp_payload"})
return
result = _relay_udp_packet(host, port, payload)
self._send_json(200, result)
return

if not _safe_url(u):
self._send_json(400, {"e": "bad_url"})
return
Expand Down
Loading