dograh/api/utils/tunnel.py
Abhishek 8e2e5c9327
feat: Enable telephony for OSS (#21)
* fix: fix tooltip bug

* feat: add Twilio with CloudFlare configuration

* chore: update Tella Video
2025-10-04 12:22:50 +05:30

104 lines
3.7 KiB
Python

"""Utility for getting the cloudflared tunnel URL at runtime."""
import asyncio
import os
import re
from typing import Optional
import aiohttp
from loguru import logger
class TunnelURLProvider:
"""Provider for getting the tunnel URL from cloudflared or environment."""
@classmethod
async def get_tunnel_url(cls) -> str:
"""
Get the tunnel URL for external access.
Priority:
1. BACKEND_API_ENDPOINT environment variable (if set)
2. Query cloudflared metrics endpoint
3. Raise error if neither available
Returns:
str: The tunnel domain (without protocol)
Raises:
ValueError: If no tunnel URL can be determined
"""
# First priority: Check environment variable
env_endpoint = os.getenv("BACKEND_API_ENDPOINT")
if env_endpoint:
logger.debug(f"Using BACKEND_API_ENDPOINT from environment: {env_endpoint}")
return env_endpoint
# Second priority: Query cloudflared
try:
# Try to get URL from cloudflared metrics
url = await cls._get_cloudflared_url()
if url:
logger.info(f"Retrieved tunnel URL from cloudflared: {url}")
return url
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_url(cls) -> Optional[str]:
"""
Query cloudflared metrics endpoint to get the tunnel URL.
Returns:
Optional[str]: The tunnel domain (without protocol), 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 hostname
# Alternative: Look for trycloudflare.com domain
match = re.search(r"([a-z0-9-]+\.trycloudflare\.com)", text)
if match:
return match.group(1)
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