From 7e4c4d91003c61b5384c92ae47f75a4c0e8fdc09 Mon Sep 17 00:00:00 2001 From: RayExo Date: Thu, 16 Jul 2026 20:32:59 +0530 Subject: [PATCH] CodeX - Upgraded API Tunnel to Cloudflare --- bot/.env.example | 43 ++++++---- bot/CodeX.py | 2 +- bot/api/server.py | 8 -- bot/requirements.txt | 2 +- bot/utils/tunnel.py | 191 +++++++++++++++++++++++++------------------ 5 files changed, 139 insertions(+), 107 deletions(-) diff --git a/bot/.env.example b/bot/.env.example index e241963..f3d9d56 100644 --- a/bot/.env.example +++ b/bot/.env.example @@ -2,10 +2,11 @@ TOKEN = TOKEN_HERE brand_name='ZyroX' NEXT_PUBLIC_BRAND_NAME='ZyroX' -# Owner -OWNER_IDS=870179991462236170,767979794411028491 +# ── Owner / Staff IDs (comma-separated Discord user IDs) ────────────────────── +# Edit this list — no code changes needed anywhere else +OWNER_IDS=870179991462236170,767979794411028491,1432771000629596225,1382744437049790495,1263404140965396555 -# LAVALINK CONFIG +# ── Lavalink ────────────────────────────────────────────────────────────────── LAVALINK_HOST="lavalink.jirayu.net" LAVALINK_PASSWORD="youshallnotpass" @@ -15,28 +16,38 @@ LAVALINK_SECURE="false" # Only needed when LAVALINK_SECURE is false (e.g. 2333) LAVALINK_PORT="13592" - -# EMOJI SYNC CONFIG +# ── Emoji Sync ──────────────────────────────────────────────────────────────── # Set to true to enable automatic application emoji sync on startup EMOJI_SYNC="false" -# API SERVER CONFIG +# ── API / Dashboard Backend ─────────────────────────────────────────────────── # Set to true to enable the dashboard API server API_ENABLED="false" # Port the API server listens on API_PORT="8000" - -# ON CMD RUN -WEBHOOK_URL="https://discord.com/api/webhooks/" - DASHBOARD_API_KEY="ZYROX_SECURE_API_KEY_12345_CHANGE_THIS_ASAP_BY_CODEX_DEVS" +# Extra CORS-allowed origins, comma-separated (optional) +CORS_ORIGINS="" # CODEX DEVS -# HTTPS TUNNEL CONFIG +# ── Webhooks ────────────────────────────────────────────────────────────────── +WEBHOOK_URL="https://discord.com/api/webhooks/" +CMD_WEBHOOK_URL="https://discord.com/api/webhooks/" + + +# ── Cloudflare Tunnel For API Server ───────────────────────────────────────────────────────── +# +# How to get CF_TUNNEL_TOKEN (browser only, no CLI): +# 1. Go to https://one.dash.cloudflare.com +# 2. Networks → Tunnels → Create a tunnel → Choose "Cloudflared" +# 3. Give it a name (e.g. zyrox-api) and save +# 4. On the "Install connector" step, copy the token from the shown command: +# cloudflared tunnel run --token +# 5. Go to "Published Application Routes" tab → Add a Published Application Routes: +# Subdomain: zyrox-api Domain: yourdomain.com Service: http://localhost:8000 +# 6. Paste the token below — that's it. Permanent URL, unlimited traffic. +# TUNNEL_ENABLED="true" -# Get your authtoken from https://dashboard.ngrok.com/get-started/your-authtoken -NGROK_AUTHTOKEN="xxx_xxxx" -# Reserve a free static domain at https://dashboard.ngrok.com/domains -# e.g. xxxx-xxxx-xxxx.ngrok-free.app — leave blank for a random URL each restart -NGROK_DOMAIN="" +CF_TUNNEL_TOKEN="xxx_xxxx" +CF_TUNNEL_URL="https://zyrox-api.yourdomain.com" diff --git a/bot/CodeX.py b/bot/CodeX.py index c30a2db..2b92f32 100644 --- a/bot/CodeX.py +++ b/bot/CodeX.py @@ -334,7 +334,7 @@ def keep_alive(): keep_alive() -# --- ngrok HTTPS Tunnel (for Dashboard API) --- +# --- Cloudflare Tunnel (HTTPS for API) --- from utils.tunnel import start_tunnel start_tunnel() diff --git a/bot/api/server.py b/bot/api/server.py index 8208799..847de17 100644 --- a/bot/api/server.py +++ b/bot/api/server.py @@ -103,14 +103,6 @@ def create_app() -> FastAPI: allow_headers=["*"], ) - # Middleware: add ngrok-skip-browser-warning header to every response - # (prevents ngrok's interstitial warning page from breaking CORS preflight) - @app.middleware("http") - async def add_ngrok_header(request: Request, call_next): - response = await call_next(request) - response.headers["ngrok-skip-browser-warning"] = "true" - return response - # Register Routers app.include_router(bot.router, prefix="/api/v1/bot", tags=["Bot"]) app.include_router(guilds.router, prefix="/api/v1/guilds", tags=["Guilds"]) diff --git a/bot/requirements.txt b/bot/requirements.txt index 130802e..b0d0242 100644 --- a/bot/requirements.txt +++ b/bot/requirements.txt @@ -39,4 +39,4 @@ deep-translator==1.11.4 uvicorn==0.46.0 fastapi slowapi -pyngrok \ No newline at end of file +pycloudflared \ No newline at end of file diff --git a/bot/utils/tunnel.py b/bot/utils/tunnel.py index 6178edc..95f0a5d 100644 --- a/bot/utils/tunnel.py +++ b/bot/utils/tunnel.py @@ -13,38 +13,46 @@ # ╚══════════════════════════════════════════════════════════════════╝ """ -HTTPS Tunnel for the ZyroX API using pyngrok. +HTTPS Tunnel for the ZyroX API — Cloudflare Tunnel via pycloudflared. -pyngrok is a pure Python package that automatically downloads the ngrok binary -on first run — no system installs, works on Pterodactyl out of the box. +Zero manual installs. Just: + 1. pip install pycloudflared (already in requirements.txt) + 2. Get your tunnel token from the Cloudflare dashboard (browser only, no CLI) + 3. Set CF_TUNNEL_TOKEN in .env -With a free ngrok account + a reserved static domain you get the SAME URL on -every restart (free tier gives you 1 static domain). +That's it. The cloudflared binary is downloaded automatically on first run. ────────────────────────────────────────────────────────────────────────────── -One-time setup (do this once) +How to get your CF_TUNNEL_TOKEN (browser only, no CLI needed) ────────────────────────────────────────────────────────────────────────────── -1. Sign up for a free account at https://ngrok.com -2. Copy your authtoken from https://dashboard.ngrok.com/get-started/your-authtoken -3. Reserve a free static domain at https://dashboard.ngrok.com/domains - (looks like: xxxx-xxxx-xxxx.ngrok-free.app) -4. Add these two vars to your .env: - NGROK_AUTHTOKEN = your_token_here - NGROK_DOMAIN = xxxx-xxxx-xxxx.ngrok-free.app -5. Set TUNNEL_ENABLED = "true" +1. Go to https://one.dash.cloudflare.com +2. Networks → Tunnels → Create a tunnel +3. Choose "Cloudflared" as connector type +4. Give it a name (e.g. zyrox-api) and click Save +5. On the "Install connector" step, find the token in the command shown: + cloudflared tunnel run --token + Copy just the token string. +6. Go to "Published Application Routes" tab → Add a Published Application Routes: + Subdomain: zyrox-api Domain: yourdomain.com Service: http://localhost:8000 + (Or use any domain you have on Cloudflare) +7. Paste the token into your .env: + CF_TUNNEL_TOKEN = "eyJhIjoiX..." + CF_TUNNEL_URL = "https://zyrox-api.yourdomain.com" -That's it — pyngrok handles the binary download automatically on first start. +That's the permanent URL — never changes between restarts. +Unlimited bandwidth, unlimited requests, free. ────────────────────────────────────────────────────────────────────────────── """ import os import time import threading +import subprocess # ── env vars ────────────────────────────────────────────────────────────────── -TUNNEL_ENABLED = os.getenv("TUNNEL_ENABLED", "true").strip().lower() == "true" -NGROK_AUTHTOKEN = os.getenv("NGROK_AUTHTOKEN", "").strip() -NGROK_DOMAIN = os.getenv("NGROK_DOMAIN", "").strip() # e.g. xxxx.ngrok-free.app +TUNNEL_ENABLED = os.getenv("TUNNEL_ENABLED", "true").strip().lower() == "true" +CF_TUNNEL_TOKEN = os.getenv("CF_TUNNEL_TOKEN", "").strip() # token from Cloudflare dashboard +CF_TUNNEL_URL = os.getenv("CF_TUNNEL_URL", "").strip() # e.g. https://api.yourdomain.com API_PORT = int(os.getenv("API_PORT", "8000")) # ── colours ─────────────────────────────────────────────────────────────────── @@ -55,99 +63,120 @@ _RED = "\033[31m" _RESET = "\033[0m" -def _run_ngrok(port: int, domain: str) -> None: +def _get_binary() -> str | None: """ - Blocking loop: opens an ngrok tunnel and keeps it alive. - Restarts automatically if the connection drops. + Return path to cloudflared binary. + pycloudflared downloads it automatically on first call. """ try: - from pyngrok import ngrok, conf, exception as ngrok_exc + from pycloudflared import try_cloudflare # noqa: F401 — triggers download + import pycloudflared as _pcf + # pycloudflared stores the binary path in this attribute after download + path = getattr(_pcf, "cloudflared_path", None) + if path and os.path.isfile(path): + return path + # Some versions expose it differently — walk the package dir + pkg_dir = os.path.dirname(_pcf.__file__) + for fname in os.listdir(pkg_dir): + if "cloudflared" in fname.lower() and os.access(os.path.join(pkg_dir, fname), os.X_OK): + return os.path.join(pkg_dir, fname) except ImportError: - print( - f"{_RED}◈ Tunnel: pyngrok is not installed.\n" - f" Run: pip install pyngrok{_RESET}" - ) - return + pass - # Set the authtoken (pyngrok persists it for future runs) - if NGROK_AUTHTOKEN: - conf.get_default().auth_token = NGROK_AUTHTOKEN + # Last resort: system PATH + import shutil + return shutil.which("cloudflared") + +def _run_tunnel(binary: str, token: str, port: int, public_url: str) -> None: + """ + Blocking loop — runs: + cloudflared tunnel --no-autoupdate run --token + Restarts automatically if the process exits. + """ + cmd = [ + binary, + "tunnel", + "--no-autoupdate", + "--url", f"http://localhost:{port}", + "run", + "--token", token, + ] + + first_run = True while True: - tunnel = None + if first_run: + print(f"{_CYAN}◈ Tunnel: connecting to Cloudflare…{_RESET}") + first_run = False + else: + print(f"{_YELLOW}◈ Tunnel: reconnecting…{_RESET}") + try: - options = {"addr": port, "proto": "http"} - if domain: - options["hostname"] = domain # use reserved static domain + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) - # NOTE: request_header_add is NOT supported via the pyngrok API. - # Instead the FastAPI middleware injects the ngrok-skip-browser-warning - # header on every response, which achieves the same result. + announced = False + for line in proc.stdout: + line = line.rstrip() + if not line: + continue - tunnel = ngrok.connect(**options) - public_url = tunnel.public_url.replace("http://", "https://") + # Only surface important lines — suppress the noisy info spam + low = line.lower() + if any(k in low for k in ("error", "warn", "registered", "connected", "failed", "unable")): + print(f"{_CYAN} [cloudflared] {line}{_RESET}") - print(f"{_GREEN}◈ Tunnel: API is live at {public_url}{_RESET}") - print(f"{_CYAN} ↳ set NEXT_PUBLIC_API_URL={public_url}/api/v1 in your dashboard .env{_RESET}") + # Announce the live URL once the tunnel is up + if not announced and ("registered tunnel connection" in low or "connection registered" in low): + announced = True + if public_url: + print(f"{_GREEN}◈ Tunnel: API is live at {public_url}{_RESET}") + print(f"{_CYAN} ↳ NEXT_PUBLIC_API_URL = {public_url}/api/v1{_RESET}") + else: + print(f"{_GREEN}◈ Tunnel: connected — check CF_TUNNEL_URL in .env for your public URL{_RESET}") - # Block until the tunnel process exits - ngrok.get_ngrok_process().proc.wait() + proc.wait() + code = proc.returncode - except ngrok_exc.PyngrokNgrokHTTPError as e: - # Catches auth errors (401) and other ngrok HTTP-level errors - msg = str(e).lower() - if "401" in msg or "auth" in msg or "token" in msg: - print( - f"{_RED}◈ Tunnel: invalid NGROK_AUTHTOKEN.\n" - f" Get your token from https://dashboard.ngrok.com/get-started/your-authtoken{_RESET}" - ) - return # no point retrying with a bad token - print(f"{_RED}◈ Tunnel: ngrok HTTP error — {e}{_RESET}") + except FileNotFoundError: + print(f"{_RED}◈ Tunnel: binary not found at '{binary}' — try: pip install pycloudflared{_RESET}") + return + except Exception as exc: + print(f"{_RED}◈ Tunnel: unexpected error — {exc}{_RESET}") + code = -1 - except ngrok_exc.PyngrokNgrokError as e: - print(f"{_RED}◈ Tunnel: ngrok error — {e}{_RESET}") - - except Exception as e: - print(f"{_RED}◈ Tunnel: unexpected error — {e}{_RESET}") - - finally: - # Clean up before retrying - try: - if tunnel: - ngrok.disconnect(tunnel.public_url) - except Exception: - pass - try: - ngrok.kill() - except Exception: - pass - - print(f"{_YELLOW}◈ Tunnel: connection lost, restarting in 5 s…{_RESET}") + print(f"{_YELLOW}◈ Tunnel: exited (code {code}), restarting in 5 s…{_RESET}") time.sleep(5) def start_tunnel() -> None: """ - Start the ngrok HTTPS tunnel in a background daemon thread. + Start the Cloudflare Tunnel in a background daemon thread. Called from CodeX.py after keep_alive(). """ if not TUNNEL_ENABLED: print(f"{_YELLOW}◈ Tunnel: disabled via TUNNEL_ENABLED=false{_RESET}") return - if not NGROK_AUTHTOKEN: + if not CF_TUNNEL_TOKEN: print( - f"{_YELLOW}◈ Tunnel: NGROK_AUTHTOKEN is not set — tunnel skipped.\n" - f" Get your token from https://dashboard.ngrok.com/get-started/your-authtoken{_RESET}" + f"{_YELLOW}◈ Tunnel: CF_TUNNEL_TOKEN is not set — tunnel skipped.\n" + f" Get your token from https://one.dash.cloudflare.com → Networks → Tunnels{_RESET}" ) return - if not NGROK_DOMAIN: + binary = _get_binary() + if not binary: print( - f"{_YELLOW}◈ Tunnel: NGROK_DOMAIN is not set — a random URL will be used (changes each restart).\n" - f" Reserve a free static domain at https://dashboard.ngrok.com/domains{_RESET}" + f"{_RED}◈ Tunnel: cloudflared binary not found.\n" + f" Make sure pycloudflared is installed: pip install pycloudflared{_RESET}" ) + return - print(f"{_CYAN}◈ Tunnel: starting ngrok tunnel on port {API_PORT}…{_RESET}") - t = threading.Thread(target=_run_ngrok, args=(API_PORT, NGROK_DOMAIN), daemon=True) + print(f"{_CYAN}◈ Tunnel: cloudflared binary ready — starting tunnel on port {API_PORT}…{_RESET}") + t = threading.Thread(target=_run_tunnel, args=(binary, CF_TUNNEL_TOKEN, API_PORT, CF_TUNNEL_URL), daemon=True) t.start()