somratpro commited on
Commit
b47e0f4
·
1 Parent(s): 7f99b73

feat: extend cloudflare-proxy to support fetch interception and simplify configuration, while removing redundant dns-fix utility.

Browse files
Files changed (8) hide show
  1. CHANGELOG.md +3 -3
  2. Dockerfile +4 -3
  3. README.md +45 -9
  4. cloudflare-proxy-setup.py +223 -0
  5. cloudflare-proxy.js +109 -55
  6. cloudflare-worker.js +4 -3
  7. dns-fix.js +0 -124
  8. start.sh +11 -0
CHANGELOG.md CHANGED
@@ -10,8 +10,9 @@ All notable changes to this project will be documented in this file.
10
 
11
  - **Self-hosted n8n** — Runs the latest n8n on HuggingFace Spaces Docker using SQLite (no external DB required).
12
  - **Persistent Backup** — Automatically syncs the entire n8n workspace (workflows, credentials, database) to a private HF Dataset.
13
- - **Cloudflare Transparent Proxy** — Built-in fix to bypass platform network blocks for services like Telegram and Discord.
14
- - **DNS-over-HTTPS (DoH)** — Automatic fallback resolution for domains blocked at the DNS level (e.g., WhatsApp, Telegram).
 
15
  - **Premium Dashboard** — Beautiful web interface at `/` for real-time uptime monitoring and sync health tracking.
16
  - **Built-in Keep-Alive** — Integrated UptimeRobot setup tool directly from the dashboard to prevent free HF Spaces from sleeping.
17
  - **Native Security** — Optimized for n8n v2 native user management with hardened file permissions (`umask 0077`).
@@ -25,7 +26,6 @@ All notable changes to this project will be documented in this file.
25
  - `start.sh` — Orchestrates startup, validates environment, and manages service lifecycle.
26
  - `health-server.js` — High-performance namespace proxy and dashboard server.
27
  - `cloudflare-proxy.js` — Transparently intercepts and routes blocked traffic via Cloudflare Workers.
28
- - `dns-fix.js` — Monkey-patches Node.js DNS for reliable DoH fallback.
29
  - `n8n-sync.py` — Robust background sync engine using the `huggingface_hub` API.
30
  - `start.sh` — Configures environment, restores backup, and launches background sync loop.
31
 
 
10
 
11
  - **Self-hosted n8n** — Runs the latest n8n on HuggingFace Spaces Docker using SQLite (no external DB required).
12
  - **Persistent Backup** — Automatically syncs the entire n8n workspace (workflows, credentials, database) to a private HF Dataset.
13
+ - **Cloudflare Transparent Proxy** — Built-in fix to bypass platform network blocks for Telegram, WhatsApp-related APIs, Google integrations, Discord, and other outbound services.
14
+ - **Automatic Cloudflare Worker provisioning** — Hugging8n can now create or update its outbound proxy automatically from `CLOUDFLARE_WORKERS_TOKEN`, matching HuggingClaw's auto-setup flow.
15
+ - **Google node coverage widened** — Worker defaults now cover Google API families more broadly so Sheets, Drive, Gmail, OAuth, and related nodes work without manual domain tuning.
16
  - **Premium Dashboard** — Beautiful web interface at `/` for real-time uptime monitoring and sync health tracking.
17
  - **Built-in Keep-Alive** — Integrated UptimeRobot setup tool directly from the dashboard to prevent free HF Spaces from sleeping.
18
  - **Native Security** — Optimized for n8n v2 native user management with hardened file permissions (`umask 0077`).
 
26
  - `start.sh` — Orchestrates startup, validates environment, and manages service lifecycle.
27
  - `health-server.js` — High-performance namespace proxy and dashboard server.
28
  - `cloudflare-proxy.js` — Transparently intercepts and routes blocked traffic via Cloudflare Workers.
 
29
  - `n8n-sync.py` — Robust background sync engine using the `huggingface_hub` API.
30
  - `start.sh` — Configures environment, restores backup, and launches background sync loop.
31
 
Dockerfile CHANGED
@@ -29,16 +29,17 @@ RUN mkdir -p /home/node/app /home/node/.n8n && \
29
  WORKDIR /home/node/app
30
 
31
  COPY --chown=node:node health-server.js /home/node/app/health-server.js
32
- COPY --chown=node:node dns-fix.js /opt/dns-fix.js
33
  COPY --chown=node:node cloudflare-proxy.js /opt/cloudflare-proxy.js
 
 
34
 
35
  # Set NODE_OPTIONS after preload scripts are copied
36
- ENV NODE_OPTIONS="--require /opt/dns-fix.js --require /opt/cloudflare-proxy.js"
37
  COPY --chown=node:node n8n-sync.py /home/node/app/n8n-sync.py
38
  COPY --chown=node:node setup-uptimerobot.sh /home/node/app/setup-uptimerobot.sh
39
  COPY --chown=node:node start.sh /home/node/app/start.sh
40
 
41
- RUN chmod +x /home/node/app/start.sh /home/node/app/setup-uptimerobot.sh
42
 
43
  USER node
44
 
 
29
  WORKDIR /home/node/app
30
 
31
  COPY --chown=node:node health-server.js /home/node/app/health-server.js
 
32
  COPY --chown=node:node cloudflare-proxy.js /opt/cloudflare-proxy.js
33
+ COPY --chown=node:node cloudflare-worker.js /home/node/app/cloudflare-worker.js
34
+ COPY --chown=node:node cloudflare-proxy-setup.py /home/node/app/cloudflare-proxy-setup.py
35
 
36
  # Set NODE_OPTIONS after preload scripts are copied
37
+ ENV NODE_OPTIONS="--require /opt/cloudflare-proxy.js"
38
  COPY --chown=node:node n8n-sync.py /home/node/app/n8n-sync.py
39
  COPY --chown=node:node setup-uptimerobot.sh /home/node/app/setup-uptimerobot.sh
40
  COPY --chown=node:node start.sh /home/node/app/start.sh
41
 
42
+ RUN chmod +x /home/node/app/start.sh /home/node/app/setup-uptimerobot.sh /home/node/app/cloudflare-proxy-setup.py
43
 
44
  USER node
45
 
README.md CHANGED
@@ -10,8 +10,9 @@ license: mit
10
  secrets:
11
  - name: HF_TOKEN
12
  description: HuggingFace token with write access. Used for automatic workspace backup.
13
- - name: CLOUDFLARE_PROXY_URL
14
- description: Your Cloudflare Worker URL to bypass platform blocks (Telegram/Discord).
 
15
  ---
16
 
17
  <!-- Badges -->
@@ -42,7 +43,7 @@ secrets:
42
  - ⚡ **Zero Config:** Duplicate this Space, set `HF_TOKEN`, and start automating – no other setup needed.
43
  - 💾 **Persistent Backup:** Workflows, credentials, and settings automatically sync to a private HF Dataset, preserving your data across restarts.
44
  - 🔐 **Secure by Default:** Uses n8n's native user management and restricted file permissions (`umask 0077`).
45
- - 🌐 **Built-in Connectivity:** Includes Transparent Outbound Proxying and DNS-over-HTTPS (DoH) to bypass Hugging Face networking blocks for Telegram, Discord, and others.
46
  - 📊 **Premium Dashboard:** Beautiful Web UI at `/` for real-time monitoring of uptime, sync health, and n8n status.
47
  - ⏰ **Easy Keep-Alive:** Set up a one-time UptimeRobot monitor directly from the dashboard to keep your free Space awake.
48
  - 🐳 **Optimized Infrastructure:** Minimal resource usage with clean startup logs and production-ready proxying.
@@ -58,7 +59,8 @@ secrets:
58
  Navigate to your new Space's **Settings**, scroll down to **Variables and secrets**, and add:
59
 
60
  - `HF_TOKEN` – Your HuggingFace token with **Write** access (to enable automatic backup).
61
- - `CLOUDFLARE_PROXY_URL` – *(Optional but Recommended)* Your Cloudflare Worker URL to bypass platform blocks. check [Setup Guide](#-cloudflare-proxy-setup).
 
62
  - `CLOUDFLARE_PROXY_SECRET` – *(Optional, Security Recommended)* Shared secret used between Space and Worker to prevent proxy abuse.
63
 
64
  ### Step 3: Deploy & Initialize
@@ -79,7 +81,31 @@ Use the built-in dashboard at the root URL (`/`) to track:
79
 
80
  ## 🌐 Cloudflare Proxy Setup
81
 
82
- Hugging Face Free Tier blocks outgoing connections to some services (Telegram, Discord, etc.). Hugging8n includes a transparent proxy system to bypass this.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
  1. Go to [Cloudflare Workers](https://dash.cloudflare.com/?to=/:account/workers-and-pages).
85
  2. Create a new Worker using "Start with Hello World!" template
@@ -95,8 +121,16 @@ If you skip steps 8-9, proxying still works. The secret simply adds request auth
95
 
96
  Optional Worker vars for tighter control:
97
 
98
- - `ALLOWED_TARGETS` (comma-separated, defaults to Telegram/Discord hosts)
99
- - `ALLOW_PROXY_ALL` (`false` by default; set `true` only if you fully trust your setup)
 
 
 
 
 
 
 
 
100
 
101
  ## 💾 Persistent Backup
102
 
@@ -129,8 +163,10 @@ Customize your instance with these environment variables:
129
  | :--- | :--- | :--- |
130
  | `GENERIC_TIMEZONE` | `UTC` | Timezone for your n8n instance |
131
  | `N8N_LOG_LEVEL` | `error` | Set to `info` or `debug` for more details |
132
- | `CLOUDFLARE_PROXY_DOMAINS` | (default list) | Comma-separated domains to proxy (or `*` for all) |
 
133
  | `CLOUDFLARE_PROXY_SECRET` | — | Optional shared secret for app-to-worker proxy authentication |
 
134
  | `SPACE_HOST_OVERRIDE` | — | Override detected host for custom domains |
135
  | `N8N_STARTUP_TIMEOUT` | `180` | Max seconds to wait for n8n readiness before fail-fast |
136
  | `UPTIMEROBOT_SETUP_ENABLED` | `true` | Enable/disable dashboard helper endpoint |
@@ -162,7 +198,7 @@ docker run -p 7861:7861 --env-file .env hugging8n
162
 
163
  ## 🐛 Troubleshooting
164
 
165
- - **Telegram/Discord not connecting:** Ensure `CLOUDFLARE_PROXY_URL` is set correctly.
166
  - **Workflows not saving:** Check if `HF_TOKEN` has **Write** access to your account.
167
  - **Space keeps sleeping:** Use the dashboard to set up an UptimeRobot monitor.
168
  - **Authentication errors:** n8n v2 uses its own internal users; ensure you created the owner account on first run.
 
10
  secrets:
11
  - name: HF_TOKEN
12
  description: HuggingFace token with write access. Used for automatic workspace backup.
13
+ - name: CLOUDFLARE_WORKERS_TOKEN
14
+ description: Optional Cloudflare API token for automatic Worker proxy setup.
15
+
16
  ---
17
 
18
  <!-- Badges -->
 
43
  - ⚡ **Zero Config:** Duplicate this Space, set `HF_TOKEN`, and start automating – no other setup needed.
44
  - 💾 **Persistent Backup:** Workflows, credentials, and settings automatically sync to a private HF Dataset, preserving your data across restarts.
45
  - 🔐 **Secure by Default:** Uses n8n's native user management and restricted file permissions (`umask 0077`).
46
+ - 🌐 **Built-in Connectivity:** Includes transparent outbound proxying via Cloudflare Workers for Telegram, WhatsApp-related APIs, Google APIs, Discord, and other external services.
47
  - 📊 **Premium Dashboard:** Beautiful Web UI at `/` for real-time monitoring of uptime, sync health, and n8n status.
48
  - ⏰ **Easy Keep-Alive:** Set up a one-time UptimeRobot monitor directly from the dashboard to keep your free Space awake.
49
  - 🐳 **Optimized Infrastructure:** Minimal resource usage with clean startup logs and production-ready proxying.
 
59
  Navigate to your new Space's **Settings**, scroll down to **Variables and secrets**, and add:
60
 
61
  - `HF_TOKEN` – Your HuggingFace token with **Write** access (to enable automatic backup).
62
+ - `CLOUDFLARE_WORKERS_TOKEN` – *(Optional, Recommended)* Cloudflare API token for automatic Worker proxy provisioning.
63
+ - `CLOUDFLARE_PROXY_URL` – *(Optional)* Your Cloudflare Worker URL for outbound proxying if you already have a Worker. Check [Setup Guide](#-cloudflare-proxy-setup).
64
  - `CLOUDFLARE_PROXY_SECRET` – *(Optional, Security Recommended)* Shared secret used between Space and Worker to prevent proxy abuse.
65
 
66
  ### Step 3: Deploy & Initialize
 
81
 
82
  ## 🌐 Cloudflare Proxy Setup
83
 
84
+ Hugging Face Free Tier can be unreliable for outbound connections to some services. Hugging8n includes a transparent proxy system to route external API traffic through Cloudflare Workers.
85
+
86
+ Automatic setup:
87
+
88
+ 1. Create a Cloudflare API Token for your account's Workers.
89
+ 2. Add `CLOUDFLARE_WORKERS_TOKEN` as a Space secret.
90
+ 3. Restart the Space.
91
+
92
+ Hugging8n will:
93
+
94
+ - create or update a Worker named from your Space host
95
+ - generate a private shared secret automatically
96
+ - export `CLOUDFLARE_PROXY_URL` and `CLOUDFLARE_PROXY_SECRET` before n8n starts
97
+ - transparently proxy outbound external requests through Cloudflare by default
98
+
99
+ Recommended token setup:
100
+
101
+ - Secret name: `CLOUDFLARE_WORKERS_TOKEN`
102
+ - Token type: `API Token`
103
+ - Account permission: `Workers Scripts: Edit`
104
+ - Account auto-discovery is built in; `CLOUDFLARE_ACCOUNT_ID` is not required
105
+
106
+ Do not use a Global API key, tunnel token, worker secret, or another Cloudflare credential here.
107
+
108
+ Manual setup is also available if you prefer to deploy the Worker yourself:
109
 
110
  1. Go to [Cloudflare Workers](https://dash.cloudflare.com/?to=/:account/workers-and-pages).
111
  2. Create a new Worker using "Start with Hello World!" template
 
121
 
122
  Optional Worker vars for tighter control:
123
 
124
+ - `ALLOWED_TARGETS` (comma-separated; only used when `ALLOW_PROXY_ALL=false`)
125
+ - `ALLOW_PROXY_ALL` (`true` by default; proxies all external traffic except HF-internal hosts)
126
+
127
+ Default behavior:
128
+
129
+ - `CLOUDFLARE_PROXY_DOMAINS=*`
130
+ - all external traffic is proxied
131
+ - Hugging Face internal hosts stay direct automatically
132
+
133
+ That wider default is intentional so Google nodes, Telegram, WhatsApp-related APIs, Discord, and other external integrations work without extra domain tuning.
134
 
135
  ## 💾 Persistent Backup
136
 
 
163
  | :--- | :--- | :--- |
164
  | `GENERIC_TIMEZONE` | `UTC` | Timezone for your n8n instance |
165
  | `N8N_LOG_LEVEL` | `error` | Set to `info` or `debug` for more details |
166
+ | `CLOUDFLARE_WORKERS_TOKEN` | | Cloudflare API token for automatic Worker setup |
167
+ | `CLOUDFLARE_PROXY_DOMAINS` | `*` | Comma-separated domains to proxy (or `*` for all external traffic) |
168
  | `CLOUDFLARE_PROXY_SECRET` | — | Optional shared secret for app-to-worker proxy authentication |
169
+ | `CLOUDFLARE_ACCOUNT_ID` | auto | Optional Cloudflare account ID override if you want to pin a specific account |
170
  | `SPACE_HOST_OVERRIDE` | — | Override detected host for custom domains |
171
  | `N8N_STARTUP_TIMEOUT` | `180` | Max seconds to wait for n8n readiness before fail-fast |
172
  | `UPTIMEROBOT_SETUP_ENABLED` | `true` | Enable/disable dashboard helper endpoint |
 
198
 
199
  ## 🐛 Troubleshooting
200
 
201
+ - **Telegram/Google/WhatsApp not connecting:** Ensure `CLOUDFLARE_WORKERS_TOKEN` or `CLOUDFLARE_PROXY_URL` is set correctly, or keep `CLOUDFLARE_PROXY_DOMAINS=*`.
202
  - **Workflows not saving:** Check if `HF_TOKEN` has **Write** access to your account.
203
  - **Space keeps sleeping:** Use the dashboard to set up an UptimeRobot monitor.
204
  - **Authentication errors:** n8n v2 uses its own internal users; ensure you created the owner account on first run.
cloudflare-proxy-setup.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import secrets
7
+ import sys
8
+ import urllib.error
9
+ import urllib.request
10
+ from pathlib import Path
11
+
12
+ API_BASE = "https://api.cloudflare.com/client/v4"
13
+ ENV_FILE = Path("/tmp/hugging8n-cloudflare-proxy.env")
14
+ DEFAULT_ALLOWED = [
15
+ "api.telegram.org",
16
+ "discord.com",
17
+ "discordapp.com",
18
+ "gateway.discord.gg",
19
+ "status.discord.com",
20
+ "web.whatsapp.com",
21
+ "graph.facebook.com",
22
+ "googleapis.com",
23
+ "google.com",
24
+ "googleusercontent.com",
25
+ "gstatic.com",
26
+ ]
27
+
28
+
29
+ def cf_request(method: str, path: str, token: str, body: bytes | None = None, content_type: str = "application/json"):
30
+ req = urllib.request.Request(
31
+ f"{API_BASE}{path}",
32
+ data=body,
33
+ method=method,
34
+ headers={
35
+ "Authorization": f"Bearer {token}",
36
+ "Content-Type": content_type,
37
+ },
38
+ )
39
+ with urllib.request.urlopen(req, timeout=30) as response:
40
+ payload = json.loads(response.read().decode("utf-8"))
41
+ if not payload.get("success"):
42
+ errors = payload.get("errors") or [{"message": "Unknown Cloudflare API error"}]
43
+ raise RuntimeError(errors[0].get("message", "Unknown Cloudflare API error"))
44
+ return payload["result"]
45
+
46
+
47
+ def slugify(value: str) -> str:
48
+ cleaned = re.sub(r"[^a-z0-9-]+", "-", value.lower()).strip("-")
49
+ cleaned = re.sub(r"-{2,}", "-", cleaned)
50
+ if not cleaned:
51
+ cleaned = "hugging8n-proxy"
52
+ return cleaned[:63].rstrip("-")
53
+
54
+
55
+ def derive_worker_name() -> str:
56
+ explicit = os.environ.get("CLOUDFLARE_WORKER_NAME", "").strip()
57
+ if explicit:
58
+ return slugify(explicit)
59
+ space_host = os.environ.get("SPACE_HOST_OVERRIDE", "").strip() or os.environ.get("SPACE_HOST", "").strip()
60
+ if space_host:
61
+ base = space_host.replace(".hf.space", "")
62
+ return slugify(f"{base}-proxy")
63
+ return "hugging8n-proxy"
64
+
65
+
66
+ def render_worker(secret_value: str, allowed_targets: list[str], allow_proxy_all: bool) -> str:
67
+ allowed_json = json.dumps(allowed_targets)
68
+ allow_all_js = "true" if allow_proxy_all else "false"
69
+ secret_json = json.dumps(secret_value)
70
+ return f"""addEventListener("fetch", (event) => {{
71
+ event.respondWith(handleRequest(event.request));
72
+ }});
73
+
74
+ const PROXY_SHARED_SECRET = {secret_json};
75
+ const ALLOW_PROXY_ALL = {allow_all_js};
76
+ const ALLOWED_TARGETS = {allowed_json};
77
+
78
+ function isAllowedHost(hostname) {{
79
+ const normalized = String(hostname || "").trim().toLowerCase();
80
+ if (!normalized) return false;
81
+ if (ALLOW_PROXY_ALL) return true;
82
+ return ALLOWED_TARGETS.some(
83
+ (domain) => normalized === domain || normalized.endsWith(`.${{domain}}`),
84
+ );
85
+ }}
86
+
87
+ async function handleRequest(request) {{
88
+ const url = new URL(request.url);
89
+ const targetHost = request.headers.get("x-target-host");
90
+
91
+ if (PROXY_SHARED_SECRET) {{
92
+ const providedSecret = request.headers.get("x-proxy-key") || "";
93
+ if (providedSecret !== PROXY_SHARED_SECRET) {{
94
+ return new Response("Unauthorized", {{ status: 401 }});
95
+ }}
96
+ }}
97
+
98
+ let targetBase = "";
99
+ if (targetHost) {{
100
+ if (!isAllowedHost(targetHost)) {{
101
+ return new Response("Target host is not allowed.", {{ status: 403 }});
102
+ }}
103
+ targetBase = `https://${{targetHost}}`;
104
+ }} else if (url.pathname.startsWith("/bot")) {{
105
+ targetBase = "https://api.telegram.org";
106
+ }} else {{
107
+ return new Response("Invalid request.", {{ status: 400 }});
108
+ }}
109
+
110
+ const targetUrl = targetBase + url.pathname + url.search;
111
+ const headers = new Headers(request.headers);
112
+ headers.delete("host");
113
+ headers.delete("cf-connecting-ip");
114
+ headers.delete("cf-ray");
115
+ headers.delete("cf-visitor");
116
+ headers.delete("x-real-ip");
117
+ headers.delete("x-target-host");
118
+
119
+ const proxiedRequest = new Request(targetUrl, {{
120
+ method: request.method,
121
+ headers,
122
+ body: request.body,
123
+ redirect: "follow",
124
+ }});
125
+
126
+ try {{
127
+ return await fetch(proxiedRequest);
128
+ }} catch (error) {{
129
+ return new Response(`Proxy Error: ${{error.message}}`, {{ status: 502 }});
130
+ }}
131
+ }}
132
+ """
133
+
134
+
135
+ def write_env(proxy_url: str, proxy_secret: str) -> None:
136
+ ENV_FILE.write_text(
137
+ "\n".join(
138
+ [
139
+ f'export CLOUDFLARE_PROXY_URL="{proxy_url}"',
140
+ f'export CLOUDFLARE_PROXY_SECRET="{proxy_secret}"',
141
+ ]
142
+ )
143
+ + "\n",
144
+ encoding="utf-8",
145
+ )
146
+
147
+
148
+ def main() -> int:
149
+ existing_url = os.environ.get("CLOUDFLARE_PROXY_URL", "").strip()
150
+ existing_secret = os.environ.get("CLOUDFLARE_PROXY_SECRET", "").strip()
151
+ workers_token = (
152
+ os.environ.get("CLOUDFLARE_WORKERS_TOKEN", "").strip()
153
+ or os.environ.get("CLOUDFLARE_API_TOKEN", "").strip()
154
+ )
155
+
156
+ if existing_url:
157
+ if existing_secret:
158
+ write_env(existing_url, existing_secret)
159
+ print(f"☁️ Using configured Cloudflare proxy: {existing_url}")
160
+ return 0
161
+
162
+ if not workers_token:
163
+ return 0
164
+
165
+ account_id = os.environ.get("CLOUDFLARE_ACCOUNT_ID", "").strip()
166
+ try:
167
+ if not account_id:
168
+ accounts = cf_request("GET", "/accounts", workers_token)
169
+ if not accounts:
170
+ raise RuntimeError("No Cloudflare account available for this token.")
171
+ account_id = accounts[0]["id"]
172
+
173
+ subdomain_info = cf_request(
174
+ "GET",
175
+ f"/accounts/{account_id}/workers/subdomain",
176
+ workers_token,
177
+ )
178
+ subdomain = (subdomain_info or {}).get("subdomain", "").strip()
179
+ if not subdomain:
180
+ raise RuntimeError(
181
+ "Cloudflare Workers subdomain is not configured. Enable workers.dev in your Cloudflare account first."
182
+ )
183
+
184
+ worker_name = derive_worker_name()
185
+ allowed_raw = os.environ.get("CLOUDFLARE_PROXY_DOMAINS", "").strip()
186
+ allow_proxy_all = not allowed_raw or allowed_raw == "*"
187
+ allowed_targets = DEFAULT_ALLOWED if allow_proxy_all else [
188
+ value.strip() for value in allowed_raw.split(",") if value.strip()
189
+ ]
190
+ proxy_secret = existing_secret or secrets.token_urlsafe(24)
191
+ worker_source = render_worker(proxy_secret, allowed_targets, allow_proxy_all)
192
+
193
+ cf_request(
194
+ "PUT",
195
+ f"/accounts/{account_id}/workers/scripts/{worker_name}",
196
+ workers_token,
197
+ body=worker_source.encode("utf-8"),
198
+ content_type="application/javascript",
199
+ )
200
+
201
+ proxy_url = f"https://{worker_name}.{subdomain}.workers.dev"
202
+ write_env(proxy_url, proxy_secret)
203
+ print(f"☁️ Cloudflare proxy ready: {proxy_url}")
204
+ return 0
205
+ except urllib.error.HTTPError as error:
206
+ detail = error.read().decode("utf-8", errors="replace")
207
+ if error.code == 403 and '"code":9109' in detail:
208
+ print(
209
+ "☁️ Cloudflare proxy setup failed: invalid Workers token. "
210
+ "Use a Cloudflare API Token in CLOUDFLARE_WORKERS_TOKEN "
211
+ "(not a Global API Key, tunnel token, or worker secret). "
212
+ "For auto-setup, it should have account-level 'Workers Scripts: Edit'. "
213
+ "The setup can auto-discover your account; CLOUDFLARE_ACCOUNT_ID is not required."
214
+ )
215
+ print(f"☁️ Cloudflare proxy setup failed: HTTP {error.code} {detail}")
216
+ return 1
217
+ except Exception as error:
218
+ print(f"☁️ Cloudflare proxy setup failed: {error}")
219
+ return 1
220
+
221
+
222
+ if __name__ == "__main__":
223
+ raise SystemExit(main())
cloudflare-proxy.js CHANGED
@@ -1,8 +1,8 @@
1
  /**
2
  * Cloudflare Proxy: Transparent Fix for Blocked Domains
3
  *
4
- * Patches https.request to redirect traffic for Telegram/Discord
5
- * through a Cloudflare Worker proxy.
6
  */
7
  "use strict";
8
 
@@ -20,12 +20,10 @@ if (
20
 
21
  const DEBUG = process.env.CLOUDFLARE_PROXY_DEBUG === "true";
22
  const PROXY_SHARED_SECRET = (process.env.CLOUDFLARE_PROXY_SECRET || "").trim();
23
-
24
- // Allow user to define what to proxy. Use "*" to proxy everything except internal HF traffic.
25
- const PROXY_DOMAINS =
26
- process.env.CLOUDFLARE_PROXY_DOMAINS ||
27
- "api.telegram.org,discord.com,discordapp.com,gateway.discord.gg,status.discord.com";
28
- const BLOCKED_DOMAINS = PROXY_DOMAINS.split(",").map((d) => d.trim());
29
  const PROXY_ALL = PROXY_DOMAINS === "*";
30
 
31
  if (PROXY_URL) {
@@ -33,18 +31,40 @@ if (PROXY_URL) {
33
  const proxy = new URL(PROXY_URL);
34
  const originalHttpsRequest = https.request;
35
  const originalHttpRequest = http.request;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
- const patch = (original, isHttps) => {
38
- return function (options, callback) {
39
  let hostname = "";
40
  let path = "";
41
  let headers = {};
42
 
43
- // 1. Extract hostname and path from various possible input types
44
  if (typeof options === "string") {
45
- const u = new URL(options);
46
- hostname = u.hostname;
47
- path = u.pathname + u.search;
48
  } else if (options instanceof URL) {
49
  hostname = options.hostname;
50
  path = options.pathname + options.search;
@@ -52,58 +72,38 @@ if (PROXY_URL) {
52
  } else {
53
  hostname =
54
  options.hostname ||
55
- (options.host ? options.host.split(":")[0] : "");
56
  path = options.path || "/";
57
  headers = options.headers || {};
58
  }
59
 
60
- // 2. Check if we should intercept (and prevent recursion)
61
- const isInternal =
62
- hostname === "localhost" ||
63
- hostname === "127.0.0.1" ||
64
- hostname.endsWith(".hf.space") ||
65
- hostname.endsWith(".huggingface.co") ||
66
- hostname === "huggingface.co";
67
-
68
- let shouldProxy = false;
69
- if (PROXY_ALL) {
70
- shouldProxy = !isInternal;
71
- } else {
72
- shouldProxy = BLOCKED_DOMAINS.some(
73
- (domain) => hostname === domain || hostname.endsWith("." + domain),
74
- );
75
- }
76
-
77
  const alreadyProxied =
78
- options._proxied || (headers && headers["x-target-host"]);
 
 
 
79
 
80
- if (shouldProxy && !alreadyProxied) {
81
- if (DEBUG)
82
  console.log(
83
- `[cloudflare-proxy] Redirecting ${hostname}${path} -> ${proxy.hostname}`,
84
  );
 
85
 
86
- // 3. Create fresh options for the proxied request
87
  const newOptions =
88
  typeof options === "string" || options instanceof URL
89
- ? { protocol: "https:", path: path }
90
  : { ...options };
91
 
92
- // Ensure it's an object we can modify
93
- if (typeof newOptions !== "object")
94
- return original.apply(this, arguments);
95
-
96
  newOptions._proxied = true;
97
  newOptions.protocol = "https:";
98
  newOptions.hostname = proxy.hostname;
99
  newOptions.port = proxy.port || 443;
100
-
101
- // CRITICAL: Force fresh TLS handshake for the new domain
102
  newOptions.servername = proxy.hostname;
103
- delete newOptions.host; // Prefer hostname
104
- delete newOptions.agent; // Force a new agent to prevent connection reuse issues
105
 
106
- // Merge and update headers
107
  newOptions.headers = {
108
  ...(newOptions.headers || {}),
109
  host: proxy.host,
@@ -114,21 +114,72 @@ if (PROXY_URL) {
114
  newOptions.headers["x-proxy-key"] = PROXY_SHARED_SECRET;
115
  }
116
 
117
- // Always use HTTPS for the proxy connection
118
  return originalHttpsRequest.call(https, newOptions, callback);
119
  }
120
 
121
- return original.apply(this, arguments);
122
  };
123
  };
124
 
125
- https.request = patch(originalHttpsRequest, true);
126
- http.request = patch(originalHttpRequest, false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
  if (DEBUG) {
129
  if (PROXY_ALL) {
130
  console.log(
131
- `[cloudflare-proxy] Transparent proxy active in WILDCARD mode (Proxying ALL except HF internal)`,
132
  );
133
  } else {
134
  console.log(
@@ -137,8 +188,11 @@ if (PROXY_URL) {
137
  }
138
  console.log(`[cloudflare-proxy] Target proxy: ${proxy.hostname}`);
139
  }
140
- } catch (e) {
141
- if (DEBUG)
142
- console.error(`[cloudflare-proxy] Failed to initialize: ${e.message}`);
 
 
 
143
  }
144
  }
 
1
  /**
2
  * Cloudflare Proxy: Transparent Fix for Blocked Domains
3
  *
4
+ * Patches https.request/http.request/fetch to redirect traffic for blocked
5
+ * hosts through a Cloudflare Worker proxy.
6
  */
7
  "use strict";
8
 
 
20
 
21
  const DEBUG = process.env.CLOUDFLARE_PROXY_DEBUG === "true";
22
  const PROXY_SHARED_SECRET = (process.env.CLOUDFLARE_PROXY_SECRET || "").trim();
23
+ const PROXY_DOMAINS = process.env.CLOUDFLARE_PROXY_DOMAINS || "*";
24
+ const BLOCKED_DOMAINS = PROXY_DOMAINS.split(",")
25
+ .map((domain) => domain.trim())
26
+ .filter(Boolean);
 
 
27
  const PROXY_ALL = PROXY_DOMAINS === "*";
28
 
29
  if (PROXY_URL) {
 
31
  const proxy = new URL(PROXY_URL);
32
  const originalHttpsRequest = https.request;
33
  const originalHttpRequest = http.request;
34
+ const originalFetch =
35
+ typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : null;
36
+
37
+ const shouldProxyHost = (hostname) => {
38
+ const normalized = String(hostname || "").trim().toLowerCase();
39
+ if (!normalized) return false;
40
+
41
+ const isInternal =
42
+ normalized === "localhost" ||
43
+ normalized === "127.0.0.1" ||
44
+ normalized.endsWith(".hf.space") ||
45
+ normalized.endsWith(".huggingface.co") ||
46
+ normalized === "huggingface.co";
47
+
48
+ if (PROXY_ALL) {
49
+ return !isInternal;
50
+ }
51
+
52
+ return BLOCKED_DOMAINS.some(
53
+ (domain) =>
54
+ normalized === domain || normalized.endsWith(`.${domain}`),
55
+ );
56
+ };
57
 
58
+ const patch = (original, originalModuleName) => {
59
+ return function patchedRequest(options, callback) {
60
  let hostname = "";
61
  let path = "";
62
  let headers = {};
63
 
 
64
  if (typeof options === "string") {
65
+ const parsed = new URL(options);
66
+ hostname = parsed.hostname;
67
+ path = parsed.pathname + parsed.search;
68
  } else if (options instanceof URL) {
69
  hostname = options.hostname;
70
  path = options.pathname + options.search;
 
72
  } else {
73
  hostname =
74
  options.hostname ||
75
+ (options.host ? String(options.host).split(":")[0] : "");
76
  path = options.path || "/";
77
  headers = options.headers || {};
78
  }
79
 
80
+ const shouldProxy = shouldProxyHost(hostname);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  const alreadyProxied =
82
+ options && typeof options === "object" && options._proxied;
83
+ const hasTargetHeader =
84
+ headers &&
85
+ (headers["x-target-host"] || headers["X-Target-Host"]);
86
 
87
+ if (shouldProxy && !alreadyProxied && !hasTargetHeader) {
88
+ if (DEBUG) {
89
  console.log(
90
+ `[cloudflare-proxy] Redirecting ${originalModuleName}://${hostname}${path} -> ${proxy.hostname}`,
91
  );
92
+ }
93
 
 
94
  const newOptions =
95
  typeof options === "string" || options instanceof URL
96
+ ? { protocol: "https:", path }
97
  : { ...options };
98
 
 
 
 
 
99
  newOptions._proxied = true;
100
  newOptions.protocol = "https:";
101
  newOptions.hostname = proxy.hostname;
102
  newOptions.port = proxy.port || 443;
 
 
103
  newOptions.servername = proxy.hostname;
104
+ delete newOptions.host;
105
+ delete newOptions.agent;
106
 
 
107
  newOptions.headers = {
108
  ...(newOptions.headers || {}),
109
  host: proxy.host,
 
114
  newOptions.headers["x-proxy-key"] = PROXY_SHARED_SECRET;
115
  }
116
 
 
117
  return originalHttpsRequest.call(https, newOptions, callback);
118
  }
119
 
120
+ return original.call(this, options, callback);
121
  };
122
  };
123
 
124
+ https.request = patch(originalHttpsRequest, "https");
125
+ http.request = patch(originalHttpRequest, "http");
126
+
127
+ if (originalFetch) {
128
+ globalThis.fetch = async function patchedFetch(input, init) {
129
+ const request = input instanceof Request ? input : null;
130
+ const url =
131
+ request
132
+ ? new URL(request.url)
133
+ : input instanceof URL
134
+ ? input
135
+ : new URL(String(input));
136
+
137
+ const hostname = url.hostname;
138
+ const shouldProxy = shouldProxyHost(hostname);
139
+ const headers = new Headers(request ? request.headers : init?.headers || {});
140
+ const alreadyProxied =
141
+ headers.has("x-target-host") || headers.has("X-Target-Host");
142
+
143
+ if (!shouldProxy || alreadyProxied) {
144
+ return originalFetch(input, init);
145
+ }
146
+
147
+ if (DEBUG) {
148
+ console.log(
149
+ `[cloudflare-proxy] Redirecting fetch://${hostname}${url.pathname}${url.search} -> ${proxy.hostname}`,
150
+ );
151
+ }
152
+
153
+ headers.set("x-target-host", hostname);
154
+ if (PROXY_SHARED_SECRET) {
155
+ headers.set("x-proxy-key", PROXY_SHARED_SECRET);
156
+ }
157
+
158
+ const proxiedUrl = new URL(url.pathname + url.search, proxy);
159
+
160
+ if (request) {
161
+ return originalFetch(
162
+ new Request(proxiedUrl, {
163
+ method: request.method,
164
+ headers,
165
+ body: request.body,
166
+ redirect: request.redirect,
167
+ duplex: "half",
168
+ }),
169
+ );
170
+ }
171
+
172
+ return originalFetch(proxiedUrl, {
173
+ ...init,
174
+ headers,
175
+ });
176
+ };
177
+ }
178
 
179
  if (DEBUG) {
180
  if (PROXY_ALL) {
181
  console.log(
182
+ "[cloudflare-proxy] Transparent proxy active in wildcard mode",
183
  );
184
  } else {
185
  console.log(
 
188
  }
189
  console.log(`[cloudflare-proxy] Target proxy: ${proxy.hostname}`);
190
  }
191
+ } catch (error) {
192
+ if (DEBUG) {
193
+ console.error(
194
+ `[cloudflare-proxy] Failed to initialize: ${error.message}`,
195
+ );
196
+ }
197
  }
198
  }
cloudflare-worker.js CHANGED
@@ -29,10 +29,10 @@ export default {
29
 
30
  const allowedTargetsRaw = (
31
  env.ALLOWED_TARGETS ||
32
- "api.telegram.org,discord.com,discordapp.com,gateway.discord.gg,status.discord.com"
33
  ).trim();
34
  const allowProxyAll =
35
- String(env.ALLOW_PROXY_ALL || "false").toLowerCase() === "true";
36
  const allowedTargets = allowedTargetsRaw
37
  .split(",")
38
  .map((value) => value.trim().toLowerCase())
@@ -77,6 +77,7 @@ export default {
77
 
78
  // Copy headers and remove internal/Cloudflare-specific ones
79
  const headers = new Headers(request.headers);
 
80
  headers.delete("cf-connecting-ip");
81
  headers.delete("cf-ray");
82
  headers.delete("cf-visitor");
@@ -93,7 +94,7 @@ export default {
93
  try {
94
  const response = await fetch(modifiedRequest);
95
 
96
- // Special handling for Discord/Telegram which might return 403 on some CF IPs
97
  // If needed, you can add retry logic here.
98
 
99
  return response;
 
29
 
30
  const allowedTargetsRaw = (
31
  env.ALLOWED_TARGETS ||
32
+ "api.telegram.org,discord.com,discordapp.com,gateway.discord.gg,status.discord.com,web.whatsapp.com,graph.facebook.com,googleapis.com,google.com,googleusercontent.com,gstatic.com"
33
  ).trim();
34
  const allowProxyAll =
35
+ String(env.ALLOW_PROXY_ALL || "true").toLowerCase() === "true";
36
  const allowedTargets = allowedTargetsRaw
37
  .split(",")
38
  .map((value) => value.trim().toLowerCase())
 
77
 
78
  // Copy headers and remove internal/Cloudflare-specific ones
79
  const headers = new Headers(request.headers);
80
+ headers.delete("host");
81
  headers.delete("cf-connecting-ip");
82
  headers.delete("cf-ray");
83
  headers.delete("cf-visitor");
 
94
  try {
95
  const response = await fetch(modifiedRequest);
96
 
97
+ // Special handling for some providers which might return 403 on some CF IPs.
98
  // If needed, you can add retry logic here.
99
 
100
  return response;
dns-fix.js DELETED
@@ -1,124 +0,0 @@
1
- /**
2
- * DNS fix preload script for HF Spaces.
3
- *
4
- * Patches Node.js dns.lookup to:
5
- * 1. Try system DNS first
6
- * 2. Fall back to DNS-over-HTTPS (Cloudflare) if system DNS fails
7
- * (This is needed because HF Spaces intercepts/blocks some domains like
8
- * WhatsApp web or Telegram API via standard UDP DNS).
9
- *
10
- * Loaded via: NODE_OPTIONS="--require /opt/dns-fix.js"
11
- */
12
- "use strict";
13
-
14
- const dns = require("dns");
15
- const https = require("https");
16
-
17
- // In-memory cache for runtime DoH resolutions
18
- const runtimeCache = new Map(); // hostname -> { ip, expiry }
19
-
20
- // DNS-over-HTTPS resolver
21
- function dohResolve(hostname, callback) {
22
- // Check runtime cache
23
- const cached = runtimeCache.get(hostname);
24
- if (cached && cached.expiry > Date.now()) {
25
- return callback(null, cached.ip);
26
- }
27
-
28
- // Use Cloudflare DNS-over-HTTPS via direct IP to avoid DNS lookup for the resolver itself
29
- const options = {
30
- hostname: "1.1.1.1",
31
- port: 443,
32
- path: `/dns-query?name=${encodeURIComponent(hostname)}&type=A`,
33
- method: "GET",
34
- headers: { Accept: "application/dns-json" },
35
- timeout: 10000,
36
- servername: "cloudflare-dns.com", // Set SNI
37
- };
38
-
39
- const req = https.get(options, (res) => {
40
- let body = "";
41
- res.on("data", (c) => (body += c));
42
- res.on("end", () => {
43
- try {
44
- if (res.statusCode !== 200) {
45
- return callback(
46
- new Error(`DoH: server returned status ${res.statusCode}`),
47
- );
48
- }
49
- const data = JSON.parse(body);
50
- const aRecords = (data.Answer || []).filter((a) => a.type === 1);
51
- if (aRecords.length === 0) {
52
- return callback(new Error(`DoH: no A record for ${hostname}`));
53
- }
54
- const ip = aRecords[0].data;
55
- const ttl = Math.max((aRecords[0].TTL || 300) * 1000, 60000);
56
- runtimeCache.set(hostname, { ip, expiry: Date.now() + ttl });
57
- callback(null, ip);
58
- } catch (e) {
59
- callback(new Error(`DoH parse error: ${e.message}`));
60
- }
61
- });
62
- });
63
- req.on("error", (e) =>
64
- callback(new Error(`DoH request failed: ${e.message}`)),
65
- );
66
- req.on("timeout", () => {
67
- req.destroy();
68
- callback(new Error("DoH request timed out"));
69
- });
70
- }
71
-
72
- // Monkey-patch dns.lookup
73
- const origLookup = dns.lookup;
74
- let isResolving = false;
75
-
76
- dns.lookup = function patchedLookup(hostname, options, callback) {
77
- // Normalize arguments
78
- if (typeof options === "function") {
79
- callback = options;
80
- options = {};
81
- }
82
- if (typeof options === "number") {
83
- options = { family: options };
84
- }
85
- options = options || {};
86
-
87
- // Skip patching for localhost, IPs, and internal domains
88
- if (
89
- !hostname ||
90
- hostname === "localhost" ||
91
- hostname === "0.0.0.0" ||
92
- hostname === "127.0.0.1" ||
93
- hostname === "::1" ||
94
- /^\d+\.\d+\.\d+\.\d+$/.test(hostname) ||
95
- /^::/.test(hostname) ||
96
- isResolving // RECURSION GUARD
97
- ) {
98
- return origLookup.call(dns, hostname, options, callback);
99
- }
100
-
101
- // 1) Try system DNS first
102
- origLookup.call(dns, hostname, options, (err, address, family) => {
103
- if (!err && address) {
104
- return callback(null, address, family);
105
- }
106
-
107
- // 2) System DNS failed — fall back to DoH
108
- if (err && (err.code === "ENOTFOUND" || err.code === "EAI_AGAIN")) {
109
- isResolving = true; // Enter guard
110
- dohResolve(hostname, (dohErr, ip) => {
111
- isResolving = false; // Exit guard
112
- if (dohErr || !ip) {
113
- return callback(err); // Return original error
114
- }
115
- if (options.all) {
116
- return callback(null, [{ address: ip, family: 4 }]);
117
- }
118
- callback(null, ip, 4);
119
- });
120
- } else {
121
- callback(err, address, family);
122
- }
123
- });
124
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
start.sh CHANGED
@@ -73,6 +73,17 @@ else
73
  echo "HF_TOKEN is not set. Running without dataset persistence."
74
  fi
75
 
 
 
 
 
 
 
 
 
 
 
 
76
  cleanup() {
77
  echo "Stopping Hugging8n..."
78
  [ -n "${PROXY_PID:-}" ] && kill "$PROXY_PID" 2>/dev/null || true
 
73
  echo "HF_TOKEN is not set. Running without dataset persistence."
74
  fi
75
 
76
+ CF_PROXY_ENV_FILE="/tmp/hugging8n-cloudflare-proxy.env"
77
+ CLOUDFLARE_WORKERS_TOKEN="${CLOUDFLARE_WORKERS_TOKEN:-${CLOUDFLARE_API_TOKEN:-}}"
78
+ export CLOUDFLARE_WORKERS_TOKEN
79
+ if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ] || [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then
80
+ echo "Preparing Cloudflare outbound proxy..."
81
+ python3 "$APP_DIR/cloudflare-proxy-setup.py" || true
82
+ if [ -f "$CF_PROXY_ENV_FILE" ]; then
83
+ . "$CF_PROXY_ENV_FILE"
84
+ fi
85
+ fi
86
+
87
  cleanup() {
88
  echo "Stopping Hugging8n..."
89
  [ -n "${PROXY_PID:-}" ] && kill "$PROXY_PID" 2>/dev/null || true