feat: extend cloudflare-proxy to support fetch interception and simplify configuration, while removing redundant dns-fix utility.
Browse files- CHANGELOG.md +3 -3
- Dockerfile +4 -3
- README.md +45 -9
- cloudflare-proxy-setup.py +223 -0
- cloudflare-proxy.js +109 -55
- cloudflare-worker.js +4 -3
- dns-fix.js +0 -124
- 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
|
| 14 |
-
- **
|
|
|
|
| 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/
|
| 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:
|
| 14 |
-
description:
|
|
|
|
| 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
|
| 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 |
-
- `
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 99 |
-
- `ALLOW_PROXY_ALL` (`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
| `
|
|
|
|
| 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/
|
| 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
|
| 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 |
-
|
| 25 |
-
|
| 26 |
-
|
| 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,
|
| 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
|
| 46 |
-
hostname =
|
| 47 |
-
path =
|
| 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 |
-
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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;
|
| 104 |
-
delete newOptions.agent;
|
| 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.
|
| 122 |
};
|
| 123 |
};
|
| 124 |
|
| 125 |
-
https.request = patch(originalHttpsRequest,
|
| 126 |
-
http.request = patch(originalHttpRequest,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
if (DEBUG) {
|
| 129 |
if (PROXY_ALL) {
|
| 130 |
console.log(
|
| 131 |
-
|
| 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 (
|
| 141 |
-
if (DEBUG)
|
| 142 |
-
console.error(
|
|
|
|
|
|
|
|
|
|
| 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 || "
|
| 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
|
| 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
|