diff --git a/apps_script/Code.gs b/apps_script/Code.gs index 5e17515..f86fed3 100644 --- a/apps_script/Code.gs +++ b/apps_script/Code.gs @@ -5,27 +5,62 @@ // and never sees plaintext or holds the key. // // Wire: client POSTs base64(encrypted batch). We forward the bytes verbatim -// to RELAY_URL and return its response body verbatim. +// to one of RELAY_URLS and return its response body verbatim. // -// Replace RELAY_URL with your VPS address before deploying. +// Replace RELAY_URLS with your VPS address(es) before deploying. -const RELAY_URL = 'http://YOUR.VPS.IP:8443/tunnel'; +const RELAY_URLS = [ + // Replace YOUR_SERVER_PORT with server_config.json's server_port. + // The dist/server_config.json used for the current test listens on 5443. + 'http://YOUR.VPS.IP:YOUR_SERVER_PORT/tunnel', +]; const FORWARDER_VERSION = 1; const PROTOCOL_VERSION = 1; +const ENABLE_INVOCATION_COUNTING = false; +const GAS_RELAY_LOOP_RE = /^https?:\/\/script\.google\.com\/macros\//i; function doPost(e) { - bumpInvocationCount_(); + for (let i = 0; i < RELAY_URLS.length; i++) { + if (GAS_RELAY_LOOP_RE.test(RELAY_URLS[i])) { + return ContentService + .createTextOutput('relay_loop_detected: RELAY_URLS must point to your VPS /tunnel endpoint, not Apps Script') + .setMimeType(ContentService.MimeType.TEXT); + } + } + if (ENABLE_INVOCATION_COUNTING) { + bumpInvocationCount_(); + } const payload = (e && e.postData && e.postData.contents) || ''; - const resp = UrlFetchApp.fetch(RELAY_URL, { - method: 'post', - contentType: 'text/plain', - payload: payload, - muteHttpExceptions: true, - followRedirects: false, - deadline: 30, // seconds; long-poll window is kept at 8s for Apps Script stability - }); + let lastText = ''; + for (let i = 0; i < RELAY_URLS.length; i++) { + try { + const resp = UrlFetchApp.fetch(RELAY_URLS[i], { + method: 'post', + contentType: 'text/plain', + payload: payload, + muteHttpExceptions: true, + followRedirects: false, + deadline: 30, // seconds; long-poll window is kept below this for Apps Script stability + }); + const status = resp.getResponseCode(); + const text = resp.getContentText(); + lastText = text; + if (status === 200) { + return ContentService + .createTextOutput(text) + .setMimeType(ContentService.MimeType.TEXT); + } + lastText = JSON.stringify({ + e: 'upstream_status', + status: status, + body: text.slice(0, 1024), + }); + } catch (err) { + lastText = String(err); + } + } return ContentService - .createTextOutput(resp.getContentText()) + .createTextOutput(lastText) .setMimeType(ContentService.MimeType.TEXT); } diff --git a/client_config.example.json b/client_config.example.json index b4e9faf..49ff868 100644 --- a/client_config.example.json +++ b/client_config.example.json @@ -4,12 +4,11 @@ "socks_port": 1080, "google_host": "216.239.38.120", "sni": ["www.google.com", "mail.google.com", "accounts.google.com"], + "_comment_script_keys": "Add more entries here for extra deployments/accounts, e.g. {\"id\":\"AKfycb...\",\"account\":\"acct-b\"}. Deployments with the same account label share one Apps Script daily quota bucket.", "script_keys": [ - {"id": "REPLACE_WITH_DEPLOYMENT_ID", "account": "acct-a"}, - {"id": "OPTIONAL_SECOND_DEPLOYMENT_ID", "account": "acct-a"}, - {"id": "OPTIONAL_THIRD_DEPLOYMENT_ID", "account": "acct-b"} + {"id": "REPLACE_WITH_DEPLOYMENT_ID", "account": "acct-a"} ], - "tunnel_key": "REPLACE_WITH_OUTPUT_OF_scripts_gen-key.sh", + "tunnel_key": "REPLACE_WITH_64_HEX_CHARACTER_RANDOM_KEY", "_comment_socks_auth": "Optional: require SOCKS5 username/password (RFC 1929). Both fields must be set together or both omitted.", "socks_user": "", diff --git a/internal/config/client.go b/internal/config/client.go index d551f82..53660d4 100644 --- a/internal/config/client.go +++ b/internal/config/client.go @@ -323,15 +323,15 @@ func LoadClient(path string) (*Client, error) { relayURLs = dedupeStrings(relayURLs) key := strings.TrimSpace(f.TunnelKey) - if key == "" || key == "REPLACE_WITH_OUTPUT_OF_scripts_gen-key.sh" { - return nil, fmt.Errorf("tunnel_key is empty or still the placeholder text in %s.\n Fix: generate a key with 'bash scripts/gen-key.sh' and paste the 64-character output into the tunnel_key field. The same value must be in server_config.json", path) + if key == "" || key == "REPLACE_WITH_64_HEX_CHARACTER_RANDOM_KEY" { + return nil, fmt.Errorf("tunnel_key is empty or still the placeholder text in %s.\n Fix: generate a key with 'openssl rand -hex 32' and paste the 64-character output into the tunnel_key field. The same value must be in server_config.json", path) } if len(key) != 64 { - return nil, fmt.Errorf("tunnel_key must be exactly 64 hex characters (got %d) in %s.\n Fix: generate a fresh key with 'bash scripts/gen-key.sh' and paste the full output. Use the SAME value in client_config.json and server_config.json", len(key), path) + return nil, fmt.Errorf("tunnel_key must be exactly 64 hex characters (got %d) in %s.\n Fix: generate a fresh key with 'openssl rand -hex 32' and paste the full output. Use the SAME value in client_config.json and server_config.json", len(key), path) } raw, err := hex.DecodeString(key) if err != nil || len(raw) != 32 { - return nil, fmt.Errorf("tunnel_key in %s contains non-hex characters.\n Valid characters are 0-9 and a-f. Generate a fresh key with 'bash scripts/gen-key.sh' and copy it carefully — no spaces, quotes, or extra newlines", path) + return nil, fmt.Errorf("tunnel_key in %s contains non-hex characters.\n Valid characters are 0-9 and a-f. Generate a fresh key with 'openssl rand -hex 32' and copy it carefully — no spaces, quotes, or extra newlines", path) } useFronting := len(relayURLs) == 0 diff --git a/server_config.example.json b/server_config.example.json index aedca85..fbf7506 100644 --- a/server_config.example.json +++ b/server_config.example.json @@ -2,6 +2,7 @@ "server_host": "0.0.0.0", "server_port": 8443, "tunnel_key": "SAME_VALUE_AS_CLIENT_tunnel_key", - "upstream_proxy": "OPTIONAL_socks5://127.0.0.1:40000", + "_comment_upstream_proxy": "Optional: set to socks5://127.0.0.1:40000 only after installing and connecting Cloudflare WARP or another local SOCKS5 proxy on the VPS. Leave empty otherwise.", + "upstream_proxy": "", "debug_timing": false }