dograh/api/utils/tunnel.py

96 lines
3.5 KiB
Python
Raw Normal View History

"""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