somratpro commited on
Commit
678f3be
Β·
1 Parent(s): ef257fb

feat: replace DNS fix with automated Cloudflare outbound proxy provisioning for blocked network requests

Browse files
Files changed (8) hide show
  1. CHANGELOG.md +3 -1
  2. Dockerfile +5 -3
  3. README.md +45 -3
  4. cloudflare-proxy-setup.py +202 -0
  5. cloudflare-proxy.js +140 -0
  6. cloudflare-worker.js +89 -0
  7. dns-fix.js +0 -108
  8. start.sh +13 -10
CHANGELOG.md CHANGED
@@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
7
  ### Added
8
 
9
  - **Custom OpenAI-compatible provider registration** β€” HuggingClaw can now register a custom provider at startup with `CUSTOM_PROVIDER_NAME`, `CUSTOM_BASE_URL`, and `CUSTOM_MODEL_ID`, so you can point `LLM_MODEL` at your own OpenAI-compatible endpoint without modifying the OpenClaw CLI
 
10
 
11
  ### Changed
12
 
@@ -14,6 +15,8 @@ All notable changes to this project will be documented in this file.
14
  - **HF username no longer required in most cases** β€” backup namespace resolution now works from `HF_USERNAME`, `SPACE_AUTHOR_NAME`, or the authenticated HF token, so `HF_TOKEN` is usually enough on its own
15
  - **Startup restore path modernized** β€” startup now restores workspace and hidden state through `workspace-sync.py restore` instead of configuring a token-bearing git remote
16
  - **README refreshed for the new backup model** β€” documentation now describes token-only backup setup, the removed git sync assumptions, and the hardened dashboard helper behavior
 
 
17
 
18
  ### Fixed
19
 
@@ -96,4 +99,3 @@ All notable changes to this project will be documented in this file.
96
  - `start.sh` β€” config generator + validation + orchestrator
97
  - `workspace-sync.sh` β€” periodic workspace backup
98
  - `health-server.js` β€” lightweight health endpoint
99
- - `dns-fix.js` β€” DNS override for HF network restrictions
 
7
  ### Added
8
 
9
  - **Custom OpenAI-compatible provider registration** β€” HuggingClaw can now register a custom provider at startup with `CUSTOM_PROVIDER_NAME`, `CUSTOM_BASE_URL`, and `CUSTOM_MODEL_ID`, so you can point `LLM_MODEL` at your own OpenAI-compatible endpoint without modifying the OpenClaw CLI
10
+ - **Automatic Cloudflare outbound proxy setup** β€” HuggingClaw can now provision and use a Cloudflare Worker proxy for blocked outbound traffic from a `CLOUDFLARE_API_TOKEN`, with the same transparent proxy model used in Hugging8n
11
 
12
  ### Changed
13
 
 
15
  - **HF username no longer required in most cases** β€” backup namespace resolution now works from `HF_USERNAME`, `SPACE_AUTHOR_NAME`, or the authenticated HF token, so `HF_TOKEN` is usually enough on its own
16
  - **Startup restore path modernized** β€” startup now restores workspace and hidden state through `workspace-sync.py restore` instead of configuring a token-bearing git remote
17
  - **README refreshed for the new backup model** β€” documentation now describes token-only backup setup, the removed git sync assumptions, and the hardened dashboard helper behavior
18
+ - **Telegram networking simplified** β€” removed the channel-specific Telegram transport tweaks in favor of the generic Cloudflare outbound proxy path
19
+ - **DNS monkey-patch removed** β€” HuggingClaw now relies on the Cloudflare outbound proxy path instead of the old `dns-fix.js` preload
20
 
21
  ### Fixed
22
 
 
99
  - `start.sh` β€” config generator + validation + orchestrator
100
  - `workspace-sync.sh` β€” periodic workspace backup
101
  - `health-server.js` β€” lightweight health endpoint
 
Dockerfile CHANGED
@@ -63,13 +63,15 @@ RUN ln -s /home/node/.openclaw/openclaw-app/openclaw.mjs /usr/local/bin/openclaw
63
  npm install -g openclaw@${OPENCLAW_VERSION}
64
 
65
  # Copy HuggingClaw files
66
- COPY --chown=1000:1000 dns-fix.js /opt/dns-fix.js
 
 
67
  COPY --chown=1000:1000 health-server.js /home/node/app/health-server.js
68
  COPY --chown=1000:1000 iframe-fix.cjs /home/node/app/iframe-fix.cjs
69
  COPY --chown=1000:1000 start.sh /home/node/app/start.sh
70
  COPY --chown=1000:1000 wa-guardian.js /home/node/app/wa-guardian.js
71
  COPY --chown=1000:1000 workspace-sync.py /home/node/app/workspace-sync.py
72
- RUN chmod +x /home/node/app/start.sh
73
 
74
  USER node
75
 
@@ -77,7 +79,7 @@ ENV HOME=/home/node \
77
  OPENCLAW_VERSION=${OPENCLAW_VERSION} \
78
  PATH=/home/node/.local/bin:/usr/local/bin:$PATH \
79
  NODE_PATH=/home/node/browser-deps/node_modules \
80
- NODE_OPTIONS="--require /opt/dns-fix.js"
81
 
82
  WORKDIR /home/node/app
83
 
 
63
  npm install -g openclaw@${OPENCLAW_VERSION}
64
 
65
  # Copy HuggingClaw files
66
+ COPY --chown=1000:1000 cloudflare-proxy.js /opt/cloudflare-proxy.js
67
+ COPY --chown=1000:1000 cloudflare-proxy-setup.py /home/node/app/cloudflare-proxy-setup.py
68
+ COPY --chown=1000:1000 cloudflare-worker.js /home/node/app/cloudflare-worker.js
69
  COPY --chown=1000:1000 health-server.js /home/node/app/health-server.js
70
  COPY --chown=1000:1000 iframe-fix.cjs /home/node/app/iframe-fix.cjs
71
  COPY --chown=1000:1000 start.sh /home/node/app/start.sh
72
  COPY --chown=1000:1000 wa-guardian.js /home/node/app/wa-guardian.js
73
  COPY --chown=1000:1000 workspace-sync.py /home/node/app/workspace-sync.py
74
+ RUN chmod +x /home/node/app/start.sh /home/node/app/cloudflare-proxy-setup.py
75
 
76
  USER node
77
 
 
79
  OPENCLAW_VERSION=${OPENCLAW_VERSION} \
80
  PATH=/home/node/.local/bin:/usr/local/bin:$PATH \
81
  NODE_PATH=/home/node/browser-deps/node_modules \
82
+ NODE_OPTIONS="--require /opt/cloudflare-proxy.js"
83
 
84
  WORKDIR /home/node/app
85
 
README.md CHANGED
@@ -14,6 +14,8 @@ secrets:
14
  description: The model ID to use, e.g. openai/gpt-4o or google/gemini-2.5-flash.
15
  - name: GATEWAY_TOKEN
16
  description: A strong password or token to secure your OpenClaw Control UI.
 
 
17
  ---
18
 
19
  <!-- Badges -->
@@ -30,6 +32,7 @@ secrets:
30
  - [πŸŽ₯ Video Tutorial](#-video-tutorial)
31
  - [πŸš€ Quick Start](#-quick-start)
32
  - [πŸ“± Telegram Setup *(Optional)*](#-telegram-setup-optional)
 
33
  - [πŸ’¬ WhatsApp Setup *(Optional)*](#-whatsapp-setup-optional)
34
  - [πŸ’Ύ Workspace Backup *(Optional)*](#-workspace-backup-optional)
35
  - [πŸ”” Webhooks *(Optional)*](#-webhooks-optional)
@@ -49,7 +52,7 @@ secrets:
49
  - πŸ”Œ **Any LLM:** Use Claude, OpenAI GPT, Google Gemini, Grok, DeepSeek, Qwen, and 40+ providers (set `LLM_API_KEY` and `LLM_MODEL` accordingly).
50
  - ⚑ **Zero Config:** Duplicate this Space and set **just three** secrets (LLM_API_KEY, LLM_MODEL, GATEWAY_TOKEN) – no other setup needed.
51
  - 🐳 **Fast Builds:** Uses a pre-built OpenClaw Docker image to deploy in minutes.
52
- - 🌐 **Built-In Browser:** Headless Chromium is included in the Space, so browser actions work from the start.
53
  - πŸ’Ύ **Workspace Backup:** Chats, settings, and WhatsApp session state sync to a private HF Dataset via the `huggingface_hub`, preserving data automatically without storing your HF token in a git remote.
54
  - ⏰ **External Keep-Alive:** Set up a one-time UptimeRobot monitor from the dashboard to help keep free HF Spaces awake.
55
  - πŸ‘₯ **Multi-User Messaging:** Support for Telegram (multi-user) and WhatsApp (pairing).
@@ -102,7 +105,8 @@ To chat via Telegram:
102
 
103
  1. Create a bot via [@BotFather](https://t.me/BotFather): send `/newbot`, follow prompts, and copy the bot token.
104
  2. Find your Telegram user ID with [@userinfobot](https://t.me/userinfobot).
105
- 3. Add these secrets in Settings β†’ Secrets. After restarting, the bot should appear online on Telegram.
 
106
 
107
  | Variable | Default | Description |
108
  | :--- | :--- | :--- |
@@ -110,6 +114,44 @@ To chat via Telegram:
110
  | `TELEGRAM_USER_ID` | β€” | Single Telegram user ID allowlist |
111
  | `TELEGRAM_USER_IDS` | β€” | Comma-separated Telegram user IDs for team access |
112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  ## πŸ’¬ WhatsApp Setup *(Optional)*
114
 
115
  To use WhatsApp, enable the channel and scan the QR code from the Control UI (**Channels** β†’ **WhatsApp** β†’ **Login**):
@@ -293,7 +335,7 @@ HuggingClaw/
293
  β”œβ”€β”€ start.sh # Config generator, validator, and orchestrator
294
  β”œβ”€β”€ workspace-sync.py # Syncs workspace/state to HF Datasets via huggingface_hub
295
  β”œβ”€β”€ health-server.js # /health endpoint for uptime checks
296
- β”œβ”€β”€ dns-fix.js # DNS-over-HTTPS fallback (for blocked domains)
297
  β”œβ”€β”€ .env.example # Environment variable reference
298
  └── README.md # (this file)
299
 
 
14
  description: The model ID to use, e.g. openai/gpt-4o or google/gemini-2.5-flash.
15
  - name: GATEWAY_TOKEN
16
  description: A strong password or token to secure your OpenClaw Control UI.
17
+ - name: CLOUDFLARE_API_TOKEN
18
+ description: Optional Cloudflare API token for automatic outbound proxy setup.
19
  ---
20
 
21
  <!-- Badges -->
 
32
  - [πŸŽ₯ Video Tutorial](#-video-tutorial)
33
  - [πŸš€ Quick Start](#-quick-start)
34
  - [πŸ“± Telegram Setup *(Optional)*](#-telegram-setup-optional)
35
+ - [🌐 Cloudflare Proxy *(Optional)*](#-cloudflare-proxy-optional)
36
  - [πŸ’¬ WhatsApp Setup *(Optional)*](#-whatsapp-setup-optional)
37
  - [πŸ’Ύ Workspace Backup *(Optional)*](#-workspace-backup-optional)
38
  - [πŸ”” Webhooks *(Optional)*](#-webhooks-optional)
 
52
  - πŸ”Œ **Any LLM:** Use Claude, OpenAI GPT, Google Gemini, Grok, DeepSeek, Qwen, and 40+ providers (set `LLM_API_KEY` and `LLM_MODEL` accordingly).
53
  - ⚑ **Zero Config:** Duplicate this Space and set **just three** secrets (LLM_API_KEY, LLM_MODEL, GATEWAY_TOKEN) – no other setup needed.
54
  - 🐳 **Fast Builds:** Uses a pre-built OpenClaw Docker image to deploy in minutes.
55
+ - 🌐 **Cloudflare Outbound Proxy:** HuggingClaw can automatically provision a Cloudflare Worker proxy for blocked outbound traffic such as Telegram API requests.
56
  - πŸ’Ύ **Workspace Backup:** Chats, settings, and WhatsApp session state sync to a private HF Dataset via the `huggingface_hub`, preserving data automatically without storing your HF token in a git remote.
57
  - ⏰ **External Keep-Alive:** Set up a one-time UptimeRobot monitor from the dashboard to help keep free HF Spaces awake.
58
  - πŸ‘₯ **Multi-User Messaging:** Support for Telegram (multi-user) and WhatsApp (pairing).
 
105
 
106
  1. Create a bot via [@BotFather](https://t.me/BotFather): send `/newbot`, follow prompts, and copy the bot token.
107
  2. Find your Telegram user ID with [@userinfobot](https://t.me/userinfobot).
108
+ 3. Add `CLOUDFLARE_API_TOKEN` in Space secrets to let HuggingClaw auto-provision the outbound proxy, or set `CLOUDFLARE_PROXY_URL` manually if you already have a Worker.
109
+ 4. Add these secrets in Settings β†’ Secrets. After restarting, the bot should appear online on Telegram.
110
 
111
  | Variable | Default | Description |
112
  | :--- | :--- | :--- |
 
114
  | `TELEGRAM_USER_ID` | β€” | Single Telegram user ID allowlist |
115
  | `TELEGRAM_USER_IDS` | β€” | Comma-separated Telegram user IDs for team access |
116
 
117
+ ## 🌐 Cloudflare Proxy *(Optional)*
118
+
119
+ Hugging Face Spaces sometimes blocks outgoing connections to Telegram, WhatsApp-related APIs, Google integrations, and other external services. HuggingClaw includes the same transparent Cloudflare proxy approach used in Hugging8n.
120
+
121
+ Automatic setup:
122
+
123
+ 1. Create a Cloudflare API token with Workers edit permissions.
124
+ 2. Add `CLOUDFLARE_API_TOKEN` as a Space secret.
125
+ 3. Restart the Space.
126
+
127
+ HuggingClaw will:
128
+
129
+ - create or update a Worker named from your Space host
130
+ - generate a private shared secret automatically
131
+ - export `CLOUDFLARE_PROXY_URL` and `CLOUDFLARE_PROXY_SECRET` before OpenClaw starts
132
+ - transparently proxy outbound external requests through Cloudflare by default
133
+
134
+ Default behavior:
135
+
136
+ - `CLOUDFLARE_PROXY_DOMAINS=*`
137
+ - all external traffic is proxied
138
+ - Hugging Face internal hosts stay direct automatically
139
+
140
+ That wider default is intentional so Telegram, WhatsApp-related APIs, Google integrations, and other external providers work without extra domain tuning.
141
+
142
+ Optional variables:
143
+
144
+ | Variable | Default | Description |
145
+ | :--- | :--- | :--- |
146
+ | `CLOUDFLARE_API_TOKEN` | β€” | Cloudflare API token for automatic Worker setup |
147
+ | `CLOUDFLARE_ACCOUNT_ID` | auto | Optional Cloudflare account override |
148
+ | `CLOUDFLARE_WORKER_NAME` | derived from Space host | Optional Worker script name |
149
+ | `CLOUDFLARE_PROXY_URL` | auto | Use an existing Worker URL instead of auto-provisioning |
150
+ | `CLOUDFLARE_PROXY_SECRET` | auto | Optional shared secret override |
151
+ | `CLOUDFLARE_PROXY_DOMAINS` | `*` | Comma-separated proxied domains or `*` for all external traffic |
152
+
153
+ Manual setup is also available with [cloudflare-worker.js](/Users/somrat/Development/hf-space/HuggingClaw/cloudflare-worker.js) if you prefer to deploy the Worker yourself.
154
+
155
  ## πŸ’¬ WhatsApp Setup *(Optional)*
156
 
157
  To use WhatsApp, enable the channel and scan the QR code from the Control UI (**Channels** β†’ **WhatsApp** β†’ **Login**):
 
335
  β”œβ”€β”€ start.sh # Config generator, validator, and orchestrator
336
  β”œβ”€β”€ workspace-sync.py # Syncs workspace/state to HF Datasets via huggingface_hub
337
  β”œβ”€β”€ health-server.js # /health endpoint for uptime checks
338
+ β”œβ”€β”€ cloudflare-proxy.js # Transparent outbound proxy for blocked domains
339
  β”œβ”€β”€ .env.example # Environment variable reference
340
  └── README.md # (this file)
341
 
cloudflare-proxy-setup.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/huggingclaw-cloudflare-proxy.env")
14
+ DEFAULT_ALLOWED = [
15
+ "api.telegram.org",
16
+ "web.whatsapp.com",
17
+ ]
18
+
19
+
20
+ def cf_request(method: str, path: str, token: str, body: bytes | None = None, content_type: str = "application/json"):
21
+ req = urllib.request.Request(
22
+ f"{API_BASE}{path}",
23
+ data=body,
24
+ method=method,
25
+ headers={
26
+ "Authorization": f"Bearer {token}",
27
+ "Content-Type": content_type,
28
+ },
29
+ )
30
+ with urllib.request.urlopen(req, timeout=30) as response:
31
+ payload = json.loads(response.read().decode("utf-8"))
32
+ if not payload.get("success"):
33
+ errors = payload.get("errors") or [{"message": "Unknown Cloudflare API error"}]
34
+ raise RuntimeError(errors[0].get("message", "Unknown Cloudflare API error"))
35
+ return payload["result"]
36
+
37
+
38
+ def slugify(value: str) -> str:
39
+ cleaned = re.sub(r"[^a-z0-9-]+", "-", value.lower()).strip("-")
40
+ cleaned = re.sub(r"-{2,}", "-", cleaned)
41
+ if not cleaned:
42
+ cleaned = "huggingclaw-proxy"
43
+ return cleaned[:63].rstrip("-")
44
+
45
+
46
+ def derive_worker_name() -> str:
47
+ explicit = os.environ.get("CLOUDFLARE_WORKER_NAME", "").strip()
48
+ if explicit:
49
+ return slugify(explicit)
50
+ space_host = os.environ.get("SPACE_HOST", "").strip()
51
+ if space_host:
52
+ base = space_host.replace(".hf.space", "")
53
+ return slugify(f"{base}-proxy")
54
+ return "huggingclaw-proxy"
55
+
56
+
57
+ def render_worker(secret_value: str, allowed_targets: list[str], allow_proxy_all: bool) -> str:
58
+ allowed_json = json.dumps(allowed_targets)
59
+ allow_all_js = "true" if allow_proxy_all else "false"
60
+ secret_json = json.dumps(secret_value)
61
+ return f"""addEventListener("fetch", (event) => {{
62
+ event.respondWith(handleRequest(event.request));
63
+ }});
64
+
65
+ const PROXY_SHARED_SECRET = {secret_json};
66
+ const ALLOW_PROXY_ALL = {allow_all_js};
67
+ const ALLOWED_TARGETS = {allowed_json};
68
+
69
+ function isAllowedHost(hostname) {{
70
+ const normalized = String(hostname || "").trim().toLowerCase();
71
+ if (!normalized) return false;
72
+ if (ALLOW_PROXY_ALL) return true;
73
+ return ALLOWED_TARGETS.some(
74
+ (domain) => normalized === domain || normalized.endsWith(`.${{domain}}`),
75
+ );
76
+ }}
77
+
78
+ async function handleRequest(request) {{
79
+ const url = new URL(request.url);
80
+ const targetHost = request.headers.get("x-target-host");
81
+
82
+ if (PROXY_SHARED_SECRET) {{
83
+ const providedSecret = request.headers.get("x-proxy-key") || "";
84
+ if (providedSecret !== PROXY_SHARED_SECRET) {{
85
+ return new Response("Unauthorized", {{ status: 401 }});
86
+ }}
87
+ }}
88
+
89
+ let targetBase = "";
90
+ if (targetHost) {{
91
+ if (!isAllowedHost(targetHost)) {{
92
+ return new Response("Target host is not allowed.", {{ status: 403 }});
93
+ }}
94
+ targetBase = `https://${{targetHost}}`;
95
+ }} else if (url.pathname.startsWith("/bot")) {{
96
+ targetBase = "https://api.telegram.org";
97
+ }} else {{
98
+ return new Response("Invalid request.", {{ status: 400 }});
99
+ }}
100
+
101
+ const targetUrl = targetBase + url.pathname + url.search;
102
+ const headers = new Headers(request.headers);
103
+ headers.delete("cf-connecting-ip");
104
+ headers.delete("cf-ray");
105
+ headers.delete("cf-visitor");
106
+ headers.delete("x-real-ip");
107
+ headers.delete("x-target-host");
108
+
109
+ const proxiedRequest = new Request(targetUrl, {{
110
+ method: request.method,
111
+ headers,
112
+ body: request.body,
113
+ redirect: "follow",
114
+ }});
115
+
116
+ try {{
117
+ return await fetch(proxiedRequest);
118
+ }} catch (error) {{
119
+ return new Response(`Proxy Error: ${{error.message}}`, {{ status: 502 }});
120
+ }}
121
+ }}
122
+ """
123
+
124
+
125
+ def write_env(proxy_url: str, proxy_secret: str) -> None:
126
+ ENV_FILE.write_text(
127
+ "\n".join(
128
+ [
129
+ f'export CLOUDFLARE_PROXY_URL="{proxy_url}"',
130
+ f'export CLOUDFLARE_PROXY_SECRET="{proxy_secret}"',
131
+ ]
132
+ )
133
+ + "\n",
134
+ encoding="utf-8",
135
+ )
136
+
137
+
138
+ def main() -> int:
139
+ existing_url = os.environ.get("CLOUDFLARE_PROXY_URL", "").strip()
140
+ existing_secret = os.environ.get("CLOUDFLARE_PROXY_SECRET", "").strip()
141
+ api_token = os.environ.get("CLOUDFLARE_API_TOKEN", "").strip()
142
+
143
+ if existing_url:
144
+ if existing_secret:
145
+ write_env(existing_url, existing_secret)
146
+ print(f"☁️ Using configured Cloudflare proxy: {existing_url}")
147
+ return 0
148
+
149
+ if not api_token:
150
+ return 0
151
+
152
+ account_id = os.environ.get("CLOUDFLARE_ACCOUNT_ID", "").strip()
153
+ try:
154
+ if not account_id:
155
+ accounts = cf_request("GET", "/accounts", api_token)
156
+ if not accounts:
157
+ raise RuntimeError("No Cloudflare account available for this token.")
158
+ account_id = accounts[0]["id"]
159
+
160
+ subdomain_info = cf_request(
161
+ "GET",
162
+ f"/accounts/{account_id}/workers/subdomain",
163
+ api_token,
164
+ )
165
+ subdomain = (subdomain_info or {}).get("subdomain", "").strip()
166
+ if not subdomain:
167
+ raise RuntimeError(
168
+ "Cloudflare Workers subdomain is not configured. Enable workers.dev in your Cloudflare account first."
169
+ )
170
+
171
+ worker_name = derive_worker_name()
172
+ allowed_raw = os.environ.get("CLOUDFLARE_PROXY_DOMAINS", "").strip()
173
+ allow_proxy_all = not allowed_raw or allowed_raw == "*"
174
+ allowed_targets = DEFAULT_ALLOWED if not allowed_raw or allow_proxy_all else [
175
+ value.strip() for value in allowed_raw.split(",") if value.strip()
176
+ ]
177
+ proxy_secret = existing_secret or secrets.token_urlsafe(24)
178
+ worker_source = render_worker(proxy_secret, allowed_targets, allow_proxy_all)
179
+
180
+ cf_request(
181
+ "PUT",
182
+ f"/accounts/{account_id}/workers/scripts/{worker_name}",
183
+ api_token,
184
+ body=worker_source.encode("utf-8"),
185
+ content_type="application/javascript",
186
+ )
187
+
188
+ proxy_url = f"https://{worker_name}.{subdomain}.workers.dev"
189
+ write_env(proxy_url, proxy_secret)
190
+ print(f"☁️ Cloudflare proxy ready: {proxy_url}")
191
+ return 0
192
+ except urllib.error.HTTPError as error:
193
+ detail = error.read().decode("utf-8", errors="replace")
194
+ print(f"☁️ Cloudflare proxy setup failed: HTTP {error.code} {detail}")
195
+ return 1
196
+ except Exception as error:
197
+ print(f"☁️ Cloudflare proxy setup failed: {error}")
198
+ return 1
199
+
200
+
201
+ if __name__ == "__main__":
202
+ raise SystemExit(main())
cloudflare-proxy.js ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Cloudflare Proxy: Transparent Fix for Blocked Domains
3
+ *
4
+ * Patches https.request/http.request to redirect traffic for blocked hosts
5
+ * through a Cloudflare Worker proxy.
6
+ */
7
+ "use strict";
8
+
9
+ const https = require("https");
10
+ const http = require("http");
11
+
12
+ let PROXY_URL = process.env.CLOUDFLARE_PROXY_URL;
13
+ if (
14
+ PROXY_URL &&
15
+ !PROXY_URL.startsWith("http://") &&
16
+ !PROXY_URL.startsWith("https://")
17
+ ) {
18
+ PROXY_URL = `https://${PROXY_URL}`;
19
+ }
20
+
21
+ const DEBUG = process.env.CLOUDFLARE_PROXY_DEBUG === "true";
22
+ const PROXY_SHARED_SECRET = (process.env.CLOUDFLARE_PROXY_SECRET || "").trim();
23
+ // Default to wildcard mode so Google, WhatsApp, Telegram, Discord, and other
24
+ // outbound integrations all work unless they are HF-internal hosts.
25
+ const PROXY_DOMAINS = process.env.CLOUDFLARE_PROXY_DOMAINS || "*";
26
+ const BLOCKED_DOMAINS = PROXY_DOMAINS.split(",")
27
+ .map((domain) => domain.trim())
28
+ .filter(Boolean);
29
+ const PROXY_ALL = PROXY_DOMAINS === "*";
30
+
31
+ if (PROXY_URL) {
32
+ try {
33
+ const proxy = new URL(PROXY_URL);
34
+ const originalHttpsRequest = https.request;
35
+ const originalHttpRequest = http.request;
36
+
37
+ const patch = (original, originalModuleName) => {
38
+ return function patchedRequest(options, callback) {
39
+ let hostname = "";
40
+ let path = "";
41
+ let headers = {};
42
+
43
+ if (typeof options === "string") {
44
+ const parsed = new URL(options);
45
+ hostname = parsed.hostname;
46
+ path = parsed.pathname + parsed.search;
47
+ } else if (options instanceof URL) {
48
+ hostname = options.hostname;
49
+ path = options.pathname + options.search;
50
+ headers = options.headers || {};
51
+ } else {
52
+ hostname =
53
+ options.hostname ||
54
+ (options.host ? String(options.host).split(":")[0] : "");
55
+ path = options.path || "/";
56
+ headers = options.headers || {};
57
+ }
58
+
59
+ const isInternal =
60
+ hostname === "localhost" ||
61
+ hostname === "127.0.0.1" ||
62
+ hostname.endsWith(".hf.space") ||
63
+ hostname.endsWith(".huggingface.co") ||
64
+ hostname === "huggingface.co";
65
+
66
+ let shouldProxy = false;
67
+ if (PROXY_ALL) {
68
+ shouldProxy = !isInternal;
69
+ } else {
70
+ shouldProxy = BLOCKED_DOMAINS.some(
71
+ (domain) => hostname === domain || hostname.endsWith(`.${domain}`),
72
+ );
73
+ }
74
+
75
+ const alreadyProxied =
76
+ options && typeof options === "object" && options._proxied;
77
+ const hasTargetHeader =
78
+ headers &&
79
+ (headers["x-target-host"] || headers["X-Target-Host"]);
80
+
81
+ if (shouldProxy && !alreadyProxied && !hasTargetHeader) {
82
+ if (DEBUG) {
83
+ console.log(
84
+ `[cloudflare-proxy] Redirecting ${originalModuleName}://${hostname}${path} -> ${proxy.hostname}`,
85
+ );
86
+ }
87
+
88
+ const newOptions =
89
+ typeof options === "string" || options instanceof URL
90
+ ? { protocol: "https:", path }
91
+ : { ...options };
92
+
93
+ newOptions._proxied = true;
94
+ newOptions.protocol = "https:";
95
+ newOptions.hostname = proxy.hostname;
96
+ newOptions.port = proxy.port || 443;
97
+ newOptions.servername = proxy.hostname;
98
+ delete newOptions.host;
99
+ delete newOptions.agent;
100
+
101
+ newOptions.headers = {
102
+ ...(newOptions.headers || {}),
103
+ host: proxy.host,
104
+ "x-target-host": hostname,
105
+ };
106
+
107
+ if (PROXY_SHARED_SECRET) {
108
+ newOptions.headers["x-proxy-key"] = PROXY_SHARED_SECRET;
109
+ }
110
+
111
+ return originalHttpsRequest.call(https, newOptions, callback);
112
+ }
113
+
114
+ return original.call(this, options, callback);
115
+ };
116
+ };
117
+
118
+ https.request = patch(originalHttpsRequest, "https");
119
+ http.request = patch(originalHttpRequest, "http");
120
+
121
+ if (DEBUG) {
122
+ if (PROXY_ALL) {
123
+ console.log(
124
+ "[cloudflare-proxy] Transparent proxy active in wildcard mode",
125
+ );
126
+ } else {
127
+ console.log(
128
+ `[cloudflare-proxy] Transparent proxy active for: ${BLOCKED_DOMAINS.join(", ")}`,
129
+ );
130
+ }
131
+ console.log(`[cloudflare-proxy] Target proxy: ${proxy.hostname}`);
132
+ }
133
+ } catch (error) {
134
+ if (DEBUG) {
135
+ console.error(
136
+ `[cloudflare-proxy] Failed to initialize: ${error.message}`,
137
+ );
138
+ }
139
+ }
140
+ }
cloudflare-worker.js ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Cloudflare Worker: Universal Outbound Proxy
3
+ *
4
+ * Manual setup:
5
+ * 1. Create a Cloudflare Worker.
6
+ * 2. Paste this file and deploy it.
7
+ * 3. Use the worker URL as CLOUDFLARE_PROXY_URL.
8
+ *
9
+ * Optional worker vars:
10
+ * - PROXY_SHARED_SECRET
11
+ * - ALLOWED_TARGETS
12
+ * - ALLOW_PROXY_ALL
13
+ */
14
+
15
+ function normalizeList(raw) {
16
+ return String(raw || "")
17
+ .split(",")
18
+ .map((value) => value.trim().toLowerCase())
19
+ .filter(Boolean);
20
+ }
21
+
22
+ export default {
23
+ async fetch(request, env) {
24
+ const url = new URL(request.url);
25
+ const targetHost = request.headers.get("x-target-host");
26
+ const proxySecret = (
27
+ env.PROXY_SHARED_SECRET ||
28
+ env.CLOUDFLARE_PROXY_SECRET ||
29
+ ""
30
+ ).trim();
31
+
32
+ if (proxySecret) {
33
+ const providedSecret = request.headers.get("x-proxy-key") || "";
34
+ if (providedSecret !== proxySecret) {
35
+ return new Response("Unauthorized", { status: 401 });
36
+ }
37
+ }
38
+
39
+ const allowProxyAll =
40
+ String(env.ALLOW_PROXY_ALL || "true").toLowerCase() === "true";
41
+ const allowedTargets = normalizeList(
42
+ env.ALLOWED_TARGETS || "api.telegram.org,web.whatsapp.com",
43
+ );
44
+
45
+ const isAllowedHost = (hostname) => {
46
+ const normalized = String(hostname || "")
47
+ .trim()
48
+ .toLowerCase();
49
+ if (!normalized) return false;
50
+ if (allowProxyAll) return true;
51
+ return allowedTargets.some(
52
+ (domain) => normalized === domain || normalized.endsWith(`.${domain}`),
53
+ );
54
+ };
55
+
56
+ let targetBase = "";
57
+ if (targetHost) {
58
+ if (!isAllowedHost(targetHost)) {
59
+ return new Response("Target host is not allowed.", { status: 403 });
60
+ }
61
+ targetBase = `https://${targetHost}`;
62
+ } else if (url.pathname.startsWith("/bot")) {
63
+ targetBase = "https://api.telegram.org";
64
+ } else {
65
+ return new Response("Invalid request.", { status: 400 });
66
+ }
67
+
68
+ const targetUrl = targetBase + url.pathname + url.search;
69
+ const headers = new Headers(request.headers);
70
+ headers.delete("cf-connecting-ip");
71
+ headers.delete("cf-ray");
72
+ headers.delete("cf-visitor");
73
+ headers.delete("x-real-ip");
74
+ headers.delete("x-target-host");
75
+
76
+ const proxiedRequest = new Request(targetUrl, {
77
+ method: request.method,
78
+ headers,
79
+ body: request.body,
80
+ redirect: "follow",
81
+ });
82
+
83
+ try {
84
+ return await fetch(proxiedRequest);
85
+ } catch (error) {
86
+ return new Response(`Proxy Error: ${error.message}`, { status: 502 });
87
+ }
88
+ },
89
+ };
dns-fix.js DELETED
@@ -1,108 +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
- const url = `https://1.1.1.1/dns-query?name=${encodeURIComponent(hostname)}&type=A`;
29
- const req = https.get(
30
- url,
31
- { headers: { Accept: "application/dns-json" }, timeout: 15000 },
32
- (res) => {
33
- let body = "";
34
- res.on("data", (c) => (body += c));
35
- res.on("end", () => {
36
- try {
37
- const data = JSON.parse(body);
38
- const aRecords = (data.Answer || []).filter((a) => a.type === 1);
39
- if (aRecords.length === 0) {
40
- return callback(new Error(`DoH: no A record for ${hostname}`));
41
- }
42
- const ip = aRecords[0].data;
43
- const ttl = Math.max((aRecords[0].TTL || 300) * 1000, 60000);
44
- runtimeCache.set(hostname, { ip, expiry: Date.now() + ttl });
45
- callback(null, ip);
46
- } catch (e) {
47
- callback(new Error(`DoH parse error: ${e.message}`));
48
- }
49
- });
50
- }
51
- );
52
- req.on("error", (e) => callback(new Error(`DoH request failed: ${e.message}`)));
53
- req.on("timeout", () => {
54
- req.destroy();
55
- callback(new Error("DoH request timed out"));
56
- });
57
- }
58
-
59
- // Monkey-patch dns.lookup
60
- const origLookup = dns.lookup;
61
-
62
- dns.lookup = function patchedLookup(hostname, options, callback) {
63
- // Normalize arguments (options is optional, can be number or object)
64
- if (typeof options === "function") {
65
- callback = options;
66
- options = {};
67
- }
68
- if (typeof options === "number") {
69
- options = { family: options };
70
- }
71
- options = options || {};
72
-
73
- // Skip patching for localhost, IPs, and internal domains
74
- if (
75
- !hostname ||
76
- hostname === "localhost" ||
77
- hostname === "0.0.0.0" ||
78
- hostname === "127.0.0.1" ||
79
- hostname === "::1" ||
80
- /^\d+\.\d+\.\d+\.\d+$/.test(hostname) ||
81
- /^::/.test(hostname)
82
- ) {
83
- return origLookup.call(dns, hostname, options, callback);
84
- }
85
-
86
- // 1) Try system DNS first
87
- origLookup.call(dns, hostname, options, (err, address, family) => {
88
- if (!err && address) {
89
- return callback(null, address, family);
90
- }
91
-
92
- // 2) System DNS failed with ENOTFOUND or EAI_AGAIN β€” fall back to DoH
93
- if (err && (err.code === "ENOTFOUND" || err.code === "EAI_AGAIN")) {
94
- dohResolve(hostname, (dohErr, ip) => {
95
- if (dohErr || !ip) {
96
- return callback(err); // Return original error
97
- }
98
- if (options.all) {
99
- return callback(null, [{ address: ip, family: 4 }]);
100
- }
101
- callback(null, ip, 4);
102
- });
103
- } else {
104
- // Other DNS errors β€” pass through
105
- callback(err, address, family);
106
- }
107
- });
108
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
start.sh CHANGED
@@ -18,8 +18,6 @@ if [ -n "${SPACE_HOST:-}" ]; then
18
  OPENCLAW_CONSOLE_LOG_LEVEL="${OPENCLAW_CONSOLE_LOG_LEVEL:-warn}"
19
  OPENCLAW_FILE_LOG_LEVEL="${OPENCLAW_FILE_LOG_LEVEL:-info}"
20
  OPENCLAW_CONSOLE_LOG_STYLE="${OPENCLAW_CONSOLE_LOG_STYLE:-compact}"
21
- TELEGRAM_NATIVE_COMMANDS="${TELEGRAM_NATIVE_COMMANDS:-}"
22
- TELEGRAM_AUTO_SELECT_FAMILY="${TELEGRAM_AUTO_SELECT_FAMILY:-false}"
23
  BROWSER_PLUGIN_MODE="${BROWSER_PLUGIN_MODE:-disabled}"
24
  # HF Spaces does not benefit from Bonjour discovery, and the retries add noise.
25
  export OPENCLAW_DISABLE_BONJOUR="${OPENCLAW_DISABLE_BONJOUR:-1}"
@@ -27,8 +25,6 @@ else
27
  OPENCLAW_CONSOLE_LOG_LEVEL="${OPENCLAW_CONSOLE_LOG_LEVEL:-info}"
28
  OPENCLAW_FILE_LOG_LEVEL="${OPENCLAW_FILE_LOG_LEVEL:-info}"
29
  OPENCLAW_CONSOLE_LOG_STYLE="${OPENCLAW_CONSOLE_LOG_STYLE:-pretty}"
30
- TELEGRAM_NATIVE_COMMANDS="${TELEGRAM_NATIVE_COMMANDS:-auto}"
31
- TELEGRAM_AUTO_SELECT_FAMILY="${TELEGRAM_AUTO_SELECT_FAMILY:-true}"
32
  BROWSER_PLUGIN_MODE="${BROWSER_PLUGIN_MODE:-auto}"
33
  fi
34
  echo ""
@@ -155,6 +151,15 @@ else
155
  echo "HF_TOKEN is not set. Running without dataset persistence."
156
  fi
157
 
 
 
 
 
 
 
 
 
 
158
  # ── Build config ──
159
  CONFIG_JSON=$(cat <<'CONFIGEOF'
160
  {
@@ -332,12 +337,7 @@ fi
332
  if [ -n "${TELEGRAM_BOT_TOKEN:-}" ]; then
333
  CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.plugins.entries.telegram = {"enabled": true}')
334
  export TELEGRAM_BOT_TOKEN="$TELEGRAM_BOT_TOKEN"
335
- CONFIG_JSON=$(echo "$CONFIG_JSON" | jq ".channels.telegram.enabled = true")
336
- CONFIG_JSON=$(echo "$CONFIG_JSON" | jq ".channels.telegram.botToken = \"$TELEGRAM_BOT_TOKEN\"")
337
- CONFIG_JSON=$(echo "$CONFIG_JSON" | jq ".channels.telegram.commands.native = \"$TELEGRAM_NATIVE_COMMANDS\"")
338
- CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.channels.telegram.timeoutSeconds = 60')
339
- CONFIG_JSON=$(echo "$CONFIG_JSON" | jq ".channels.telegram.network.autoSelectFamily = ${TELEGRAM_AUTO_SELECT_FAMILY}")
340
- CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.channels.telegram.retry = {"attempts": 5, "minDelayMs": 800, "maxDelayMs": 30000, "jitter": 0.2}')
341
 
342
  if [ -n "${TELEGRAM_USER_IDS:-}" ]; then
343
  # Convert comma-separated IDs to JSON array
@@ -392,6 +392,9 @@ printf " β”‚ %-40s β”‚\n" "Backup: βœ… ${BACKUP_DATASET:-huggingclaw-backup} (
392
  else
393
  printf " β”‚ %-40s β”‚\n" "Backup: ❌ not configured"
394
  fi
 
 
 
395
  if [ -n "${OPENCLAW_PASSWORD:-}" ]; then
396
  printf " β”‚ %-40s β”‚\n" "Auth: πŸ”‘ password"
397
  else
 
18
  OPENCLAW_CONSOLE_LOG_LEVEL="${OPENCLAW_CONSOLE_LOG_LEVEL:-warn}"
19
  OPENCLAW_FILE_LOG_LEVEL="${OPENCLAW_FILE_LOG_LEVEL:-info}"
20
  OPENCLAW_CONSOLE_LOG_STYLE="${OPENCLAW_CONSOLE_LOG_STYLE:-compact}"
 
 
21
  BROWSER_PLUGIN_MODE="${BROWSER_PLUGIN_MODE:-disabled}"
22
  # HF Spaces does not benefit from Bonjour discovery, and the retries add noise.
23
  export OPENCLAW_DISABLE_BONJOUR="${OPENCLAW_DISABLE_BONJOUR:-1}"
 
25
  OPENCLAW_CONSOLE_LOG_LEVEL="${OPENCLAW_CONSOLE_LOG_LEVEL:-info}"
26
  OPENCLAW_FILE_LOG_LEVEL="${OPENCLAW_FILE_LOG_LEVEL:-info}"
27
  OPENCLAW_CONSOLE_LOG_STYLE="${OPENCLAW_CONSOLE_LOG_STYLE:-pretty}"
 
 
28
  BROWSER_PLUGIN_MODE="${BROWSER_PLUGIN_MODE:-auto}"
29
  fi
30
  echo ""
 
151
  echo "HF_TOKEN is not set. Running without dataset persistence."
152
  fi
153
 
154
+ CF_PROXY_ENV_FILE="/tmp/huggingclaw-cloudflare-proxy.env"
155
+ if [ -n "${CLOUDFLARE_API_TOKEN:-}" ] || [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then
156
+ echo "☁️ Preparing Cloudflare outbound proxy..."
157
+ python3 /home/node/app/cloudflare-proxy-setup.py || true
158
+ if [ -f "$CF_PROXY_ENV_FILE" ]; then
159
+ . "$CF_PROXY_ENV_FILE"
160
+ fi
161
+ fi
162
+
163
  # ── Build config ──
164
  CONFIG_JSON=$(cat <<'CONFIGEOF'
165
  {
 
337
  if [ -n "${TELEGRAM_BOT_TOKEN:-}" ]; then
338
  CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.plugins.entries.telegram = {"enabled": true}')
339
  export TELEGRAM_BOT_TOKEN="$TELEGRAM_BOT_TOKEN"
340
+ CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.channels.telegram.enabled = true')
 
 
 
 
 
341
 
342
  if [ -n "${TELEGRAM_USER_IDS:-}" ]; then
343
  # Convert comma-separated IDs to JSON array
 
392
  else
393
  printf " β”‚ %-40s β”‚\n" "Backup: ❌ not configured"
394
  fi
395
+ if [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then
396
+ printf " β”‚ %-40s β”‚\n" "Proxy: ☁️ ${CLOUDFLARE_PROXY_URL}"
397
+ fi
398
  if [ -n "${OPENCLAW_PASSWORD:-}" ]; then
399
  printf " β”‚ %-40s β”‚\n" "Auth: πŸ”‘ password"
400
  else