dograh/api/utils/tunnel.py
Abhishek 642cc34e8c
feat: add authentication for OSS (#167)
* feat: add authentication for OSS

Fixes #157 and #156

* fix: fix token generation

* fix: limit fastapi workers to 1
2026-02-20 18:21:24 +05:30

95 lines
3.5 KiB
Python

"""Utility for getting the cloudflared tunnel URL at runtime."""
import asyncio
import re
from typing import Optional
import aiohttp
from loguru import logger
class TunnelURLProvider:
"""Provider for getting tunnel URLs from cloudflared service."""
@classmethod
async def get_tunnel_urls(cls) -> tuple[str, str]:
"""
Get the tunnel URLs for external access.
Returns:
tuple[str, str]: (https_url, wss_url) - Both URLs include full protocol
Raises:
ValueError: If no tunnel URL can be determined
"""
try:
# Try to get URL from cloudflared metrics
urls = await cls._get_cloudflared_urls()
if urls:
return urls
except Exception as e:
logger.warning(f"Failed to get tunnel URL from cloudflared: {e}")
raise ValueError(
"No tunnel URL available. Please set BACKEND_API_ENDPOINT environment "
"variable or ensure cloudflared service is running."
)
@classmethod
async def _get_cloudflared_urls(cls) -> Optional[tuple[str, str]]:
"""
Query cloudflared metrics endpoint to get the tunnel URLs.
Returns:
Optional[tuple[str, str]]: (https_url, wss_url) with full protocols, or None if not found
"""
try:
# Try to connect to cloudflared metrics endpoint
# The service name in docker-compose is 'cloudflared'
metrics_url = "http://cloudflared:2000/metrics"
async with aiohttp.ClientSession() as session:
async with session.get(
metrics_url, timeout=aiohttp.ClientTimeout(total=5)
) as response:
if response.status != 200:
logger.warning(
f"Cloudflared metrics returned status {response.status}"
)
return None
text = await response.text()
# Look for the tunnel URL in metrics
# Cloudflared exposes this in the userHostname metric
match = re.search(r'userHostname="([^"]+)"', text)
if match:
hostname = match.group(1)
# Remove https:// or wss:// if present
hostname = hostname.replace("https://", "").replace(
"wss://", ""
)
return "https://" + hostname, "wss://" + hostname
# Alternative: Look for trycloudflare.com domain
match = re.search(r"([a-z0-9-]+\.trycloudflare\.com)", text)
if match:
hostname = match.group(1)
hostname = hostname.replace("https://", "").replace(
"wss://", ""
)
return f"https://{hostname}", f"wss://{hostname}"
logger.warning("Could not find tunnel URL in cloudflared metrics")
return None
except asyncio.TimeoutError:
logger.warning("Timeout connecting to cloudflared metrics endpoint")
return None
except aiohttp.ClientError as e:
logger.warning(f"Error connecting to cloudflared: {e}")
return None
except Exception as e:
logger.error(f"Unexpected error getting cloudflared URL: {e}")
return None