Spaces:
Running
Running
feat: replace DNS fix with automated Cloudflare outbound proxy provisioning for blocked network requests
Browse files- CHANGELOG.md +3 -1
- Dockerfile +5 -3
- README.md +45 -3
- cloudflare-proxy-setup.py +202 -0
- cloudflare-proxy.js +140 -0
- cloudflare-worker.js +89 -0
- dns-fix.js +0 -108
- 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
|
|
|
|
|
|
|
| 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/
|
| 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 |
-
- π **
|
| 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
|
|
|
|
| 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 |
-
βββ
|
| 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
|
| 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
|