HuggingPost / cloudflare-proxy-setup.py
somratpro's picture
fix: set FRONTEND_URL to bare origin so CORS allow-origin matches browser
ff3303a
#!/usr/bin/env python3
import json
import os
import re
import secrets
import sys
import urllib.error
import urllib.request
from pathlib import Path
API_BASE = "https://api.cloudflare.com/client/v4"
ENV_FILE = Path("/tmp/huggingpost-cloudflare-proxy.env")
DEFAULT_ALLOWED = [
# Messaging
"api.telegram.org",
"discord.com",
"discordapp.com",
"gateway.discord.gg",
"status.discord.com",
"web.whatsapp.com",
# Social — confirmed/likely blocked by HF firewall
"graph.facebook.com",
"graph.instagram.com",
"api.twitter.com",
"api.x.com",
"upload.twitter.com",
"api.linkedin.com",
"www.linkedin.com",
"open.tiktokapis.com",
"oauth.reddit.com",
# Video
"youtube.com",
"www.youtube.com",
# AI APIs
"api.openai.com",
# Email HTTP APIs (SMTP ports are blocked; use these instead)
"api.resend.com",
"api.sendgrid.com",
"api.mailgun.net",
# Google
"googleapis.com",
"google.com",
"googleusercontent.com",
"gstatic.com",
]
def cf_request(method: str, path: str, token: str, body: bytes | None = None, content_type: str = "application/json"):
req = urllib.request.Request(
f"{API_BASE}{path}",
data=body,
method=method,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": content_type,
},
)
with urllib.request.urlopen(req, timeout=30) as response:
payload = json.loads(response.read().decode("utf-8"))
if not payload.get("success"):
errors = payload.get("errors") or [{"message": "Unknown Cloudflare API error"}]
raise RuntimeError(errors[0].get("message", "Unknown Cloudflare API error"))
return payload["result"]
def slugify(value: str) -> str:
cleaned = re.sub(r"[^a-z0-9-]+", "-", value.lower()).strip("-")
cleaned = re.sub(r"-{2,}", "-", cleaned)
if not cleaned:
cleaned = "huggingpost-proxy"
return cleaned[:63].rstrip("-")
def derive_worker_name() -> str:
explicit = os.environ.get("CLOUDFLARE_WORKER_NAME", "").strip()
if explicit:
return slugify(explicit)
space_host = os.environ.get("SPACE_HOST", "").strip()
if space_host:
base = space_host.replace(".hf.space", "")
return slugify(f"{base}-proxy")
return "huggingpost-proxy"
def render_worker(secret_value: str, allowed_targets: list[str], allow_proxy_all: bool) -> str:
allowed_json = json.dumps(allowed_targets)
allow_all_js = "true" if allow_proxy_all else "false"
secret_json = json.dumps(secret_value)
return f"""addEventListener("fetch", (event) => {{
event.respondWith(handleRequest(event.request));
}});
const PROXY_SHARED_SECRET = {secret_json};
const ALLOW_PROXY_ALL = {allow_all_js};
const ALLOWED_TARGETS = {allowed_json};
function isAllowedHost(hostname) {{
const normalized = String(hostname || "").trim().toLowerCase();
if (!normalized) return false;
if (ALLOW_PROXY_ALL) return true;
return ALLOWED_TARGETS.some(
(domain) => normalized === domain || normalized.endsWith(`.${{domain}}`),
);
}}
async function handleRequest(request) {{
const url = new URL(request.url);
const queryTarget = url.searchParams.get("proxy_target");
const targetHost = request.headers.get("x-target-host") || queryTarget;
if (PROXY_SHARED_SECRET) {{
const providedSecret = request.headers.get("x-proxy-key") || url.searchParams.get("proxy_key") || "";
if (providedSecret !== PROXY_SHARED_SECRET) {{
if (url.pathname.startsWith("/bot") && !targetHost) {{
// Allowed fallback
}} else {{
return new Response("Unauthorized: Invalid proxy key", {{ status: 401 }});
}}
}}
}}
let targetBase = "";
if (targetHost) {{
if (!isAllowedHost(targetHost)) {{
return new Response(`Forbidden: Host ${{targetHost}} is not allowed.`, {{ status: 403 }});
}}
targetBase = `https://${{targetHost}}`;
}} else if (url.pathname.startsWith("/bot")) {{
targetBase = "https://api.telegram.org";
}} else {{
return new Response("Invalid request: No target host provided.", {{ status: 400 }});
}}
const cleanSearch = new URLSearchParams(url.search);
cleanSearch.delete("proxy_target");
cleanSearch.delete("proxy_key");
const searchStr = cleanSearch.toString();
const targetUrl = targetBase + url.pathname + (searchStr ? `?${{searchStr}}` : "");
const headers = new Headers(request.headers);
headers.delete("cf-connecting-ip");
headers.delete("cf-ray");
headers.delete("cf-visitor");
headers.delete("host");
headers.delete("x-real-ip");
headers.delete("x-target-host");
headers.delete("x-proxy-key");
const proxiedRequest = new Request(targetUrl, {{
method: request.method,
headers,
body: request.body,
redirect: "follow",
}});
try {{
return await fetch(proxiedRequest);
}} catch (error) {{
return new Response(`Proxy Error: ${{error.message}}`, {{ status: 502 }});
}}
}}
"""
def write_env(proxy_url: str, proxy_secret: str) -> None:
ENV_FILE.write_text(
"\n".join(
[
f'export CLOUDFLARE_PROXY_URL="{proxy_url}"',
f'export CLOUDFLARE_PROXY_SECRET="{proxy_secret}"',
]
)
+ "\n",
encoding="utf-8",
)
# Belt-and-suspenders: even with umask 0077 on the parent shell, force
# 0600 since the file holds the worker shared secret.
try:
ENV_FILE.chmod(0o600)
except OSError:
pass
def main() -> int:
existing_url = os.environ.get("CLOUDFLARE_PROXY_URL", "").strip()
existing_secret = os.environ.get("CLOUDFLARE_PROXY_SECRET", "").strip()
api_token = os.environ.get("CLOUDFLARE_WORKERS_TOKEN", "").strip()
if existing_url:
# Always write the env file so downstream `. $CF_PROXY_ENV_FILE` in
# start.sh has CLOUDFLARE_PROXY_URL set even when no secret was
# supplied. Empty secret means we send no x-proxy-key header — that
# only works if the deployed worker also has no secret baked in.
write_env(existing_url, existing_secret)
if not existing_secret:
print(
"Warning: CLOUDFLARE_PROXY_URL is set but CLOUDFLARE_PROXY_SECRET "
"is empty. Requests will succeed only if the deployed worker "
"was built without PROXY_SHARED_SECRET; otherwise you'll see "
"401 Unauthorized.",
file=sys.stderr,
)
return 0
if not api_token:
return 0
account_id = os.environ.get("CLOUDFLARE_ACCOUNT_ID", "").strip()
try:
if not account_id:
accounts = cf_request("GET", "/accounts", api_token)
if not accounts:
raise RuntimeError("No Cloudflare account available for this token.")
account_id = accounts[0]["id"]
subdomain_info = cf_request(
"GET",
f"/accounts/{account_id}/workers/subdomain",
api_token,
)
subdomain = (subdomain_info or {}).get("subdomain", "").strip()
if not subdomain:
raise RuntimeError(
"Cloudflare Workers subdomain is not configured. Enable workers.dev in your Cloudflare account first."
)
worker_name = derive_worker_name()
allowed_raw = os.environ.get("CLOUDFLARE_PROXY_DOMAINS", "").strip()
allow_proxy_all = allowed_raw == "*"
if allow_proxy_all:
allowed_targets = DEFAULT_ALLOWED
else:
extra = [v.strip() for v in allowed_raw.split(",") if v.strip()]
seen = set(DEFAULT_ALLOWED)
allowed_targets = list(DEFAULT_ALLOWED)
for domain in extra:
if domain not in seen:
allowed_targets.append(domain)
seen.add(domain)
proxy_secret = existing_secret or secrets.token_urlsafe(24)
worker_source = render_worker(proxy_secret, allowed_targets, allow_proxy_all)
cf_request(
"PUT",
f"/accounts/{account_id}/workers/scripts/{worker_name}",
api_token,
body=worker_source.encode("utf-8"),
content_type="application/javascript",
)
cf_request(
"POST",
f"/accounts/{account_id}/workers/scripts/{worker_name}/subdomain",
api_token,
body=json.dumps({"enabled": True, "previews_enabled": True}).encode("utf-8"),
)
proxy_url = f"https://{worker_name}.{subdomain}.workers.dev"
write_env(proxy_url, proxy_secret)
return 0
except urllib.error.HTTPError as error:
detail = error.read().decode("utf-8", errors="replace")
if error.code == 403 and '"code":9109' in detail:
print(
"Cloudflare proxy setup failed: invalid Workers token. "
"Use a Cloudflare API Token in CLOUDFLARE_WORKERS_TOKEN "
"(not a Global API Key, tunnel token, or worker secret). "
"For auto-setup, it should have account-level 'Workers Scripts: Edit'. "
"The setup can auto-discover your account; CLOUDFLARE_ACCOUNT_ID is not required.",
file=sys.stderr,
)
print(f"Cloudflare proxy setup failed: HTTP {error.code} {detail}", file=sys.stderr)
return 1
except Exception as error:
print(f"Cloudflare proxy setup failed: {error}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())