mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-07 07:55:16 +02:00
feat: add coturn configurations (#143)
* feat: add coturn changes * add turn credentials and config * fix: fix setup_remote script and docker compose
This commit is contained in:
parent
7e438ad049
commit
bf972fcfec
13 changed files with 470 additions and 86 deletions
|
|
@ -32,3 +32,12 @@ ENABLE_TRACING=false
|
||||||
# LANGFUSE_SECRET_KEY="sk-lf-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
# LANGFUSE_SECRET_KEY="sk-lf-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||||
# LANGFUSE_PUBLIC_KEY="pk-lf-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
# LANGFUSE_PUBLIC_KEY="pk-lf-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||||
# LANGFUSE_HOST="https://cloud.langfuse.com"
|
# LANGFUSE_HOST="https://cloud.langfuse.com"
|
||||||
|
|
||||||
|
# TURN Server Configuration (for WebRTC NAT traversal)
|
||||||
|
# Required for reliable WebRTC connections behind firewalls/NAT
|
||||||
|
# Uses time-limited credentials (TURN REST API) for security
|
||||||
|
TURN_HOST=localhost
|
||||||
|
TURN_SECRET=dograh-turn-secret-change-in-production
|
||||||
|
# TURN_PORT=3478 # Default: 3478
|
||||||
|
# TURN_TLS_PORT=5349 # Default: 5349
|
||||||
|
# TURN_CREDENTIAL_TTL=86400 # Default: 24 hours in seconds
|
||||||
|
|
|
||||||
|
|
@ -102,3 +102,10 @@ DEFAULT_CAMPAIGN_RETRY_CONFIG = {
|
||||||
"retry_on_no_answer": True,
|
"retry_on_no_answer": True,
|
||||||
"retry_on_voicemail": False,
|
"retry_on_voicemail": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TURN_SECRET = os.getenv("TURN_SECRET")
|
||||||
|
TURN_HOST = os.getenv("TURN_HOST", "localhost")
|
||||||
|
TURN_PORT = int(os.getenv("TURN_PORT", "3478"))
|
||||||
|
TURN_TLS_PORT = int(os.getenv("TURN_TLS_PORT", "5349"))
|
||||||
|
TURN_CREDENTIAL_TTL = int(os.getenv("TURN_CREDENTIAL_TTL", "86400"))
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ from api.routes.service_keys import router as service_keys_router
|
||||||
from api.routes.superuser import router as superuser_router
|
from api.routes.superuser import router as superuser_router
|
||||||
from api.routes.telephony import router as telephony_router
|
from api.routes.telephony import router as telephony_router
|
||||||
from api.routes.tool import router as tool_router
|
from api.routes.tool import router as tool_router
|
||||||
|
from api.routes.turn_credentials import router as turn_credentials_router
|
||||||
from api.routes.user import router as user_router
|
from api.routes.user import router as user_router
|
||||||
from api.routes.webrtc_signaling import router as webrtc_signaling_router
|
from api.routes.webrtc_signaling import router as webrtc_signaling_router
|
||||||
from api.routes.workflow import router as workflow_router
|
from api.routes.workflow import router as workflow_router
|
||||||
|
|
@ -43,6 +44,7 @@ router.include_router(looptalk_router)
|
||||||
router.include_router(organization_usage_router)
|
router.include_router(organization_usage_router)
|
||||||
router.include_router(reports_router)
|
router.include_router(reports_router)
|
||||||
router.include_router(webrtc_signaling_router)
|
router.include_router(webrtc_signaling_router)
|
||||||
|
router.include_router(turn_credentials_router)
|
||||||
router.include_router(public_embed_router)
|
router.include_router(public_embed_router)
|
||||||
router.include_router(public_agent_router)
|
router.include_router(public_agent_router)
|
||||||
router.include_router(public_download_router)
|
router.include_router(public_download_router)
|
||||||
|
|
|
||||||
163
api/routes/turn_credentials.py
Normal file
163
api/routes/turn_credentials.py
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
"""TURN credentials endpoint for time-limited WebRTC authentication.
|
||||||
|
|
||||||
|
This module implements the TURN REST API credential generation as specified in
|
||||||
|
draft-uberti-behave-turn-rest-00. It generates ephemeral credentials that are
|
||||||
|
valid for a configurable TTL and are cryptographically bound to the user.
|
||||||
|
|
||||||
|
The credential format:
|
||||||
|
- Username: {expiration_timestamp}:{user_id}
|
||||||
|
- Password: base64(hmac-sha1(shared_secret, username))
|
||||||
|
|
||||||
|
References:
|
||||||
|
- https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00
|
||||||
|
- https://github.com/coturn/coturn/wiki/turnserver#turn-rest-api
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import time
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from loguru import logger
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from api.constants import (
|
||||||
|
ENVIRONMENT,
|
||||||
|
TURN_CREDENTIAL_TTL,
|
||||||
|
TURN_HOST,
|
||||||
|
TURN_PORT,
|
||||||
|
TURN_SECRET,
|
||||||
|
TURN_TLS_PORT,
|
||||||
|
)
|
||||||
|
from api.db.models import UserModel
|
||||||
|
from api.enums import Environment
|
||||||
|
from api.services.auth.depends import get_user
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/turn", tags=["turn"])
|
||||||
|
|
||||||
|
|
||||||
|
class TurnCredentialsResponse(BaseModel):
|
||||||
|
"""Response model for TURN credentials."""
|
||||||
|
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
ttl: int
|
||||||
|
uris: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
class TurnConfigResponse(BaseModel):
|
||||||
|
"""Response model for TURN configuration status."""
|
||||||
|
|
||||||
|
enabled: bool
|
||||||
|
host: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
def generate_turn_credentials(user_id: str, ttl: int = TURN_CREDENTIAL_TTL) -> dict:
|
||||||
|
"""Generate time-limited TURN credentials using HMAC-SHA1.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: Unique identifier for the user (for auditing)
|
||||||
|
ttl: Time-to-live in seconds for the credentials
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with username, password, ttl, and TURN URIs
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If TURN_SECRET is not configured
|
||||||
|
"""
|
||||||
|
if not TURN_SECRET:
|
||||||
|
raise ValueError("TURN_SECRET is not configured")
|
||||||
|
|
||||||
|
# Calculate expiration timestamp
|
||||||
|
expiration = int(time.time()) + ttl
|
||||||
|
|
||||||
|
# Username format: {expiration}:{user_id}
|
||||||
|
# This allows the TURN server to:
|
||||||
|
# 1. Verify the credential hasn't expired
|
||||||
|
# 2. Track usage per user for auditing
|
||||||
|
username = f"{expiration}:{user_id}"
|
||||||
|
|
||||||
|
# Password: base64(hmac-sha1(secret, username))
|
||||||
|
# This is the standard TURN REST API algorithm
|
||||||
|
password = base64.b64encode(
|
||||||
|
hmac.new(
|
||||||
|
TURN_SECRET.encode("utf-8"),
|
||||||
|
username.encode("utf-8"),
|
||||||
|
hashlib.sha1,
|
||||||
|
).digest()
|
||||||
|
).decode("utf-8")
|
||||||
|
|
||||||
|
# Build TURN URIs
|
||||||
|
# Note: aiortc only uses the FIRST valid TURN URI, so ordering matters.
|
||||||
|
# Priority:
|
||||||
|
# 1. TURNS (TLS) if configured - most secure
|
||||||
|
# 2. TURN TCP for LOCAL env (macOS Docker compatibility)
|
||||||
|
# 3. TURN UDP for production (more efficient)
|
||||||
|
uris = []
|
||||||
|
|
||||||
|
# Add non-TLS TURN as fallback, ordered by environment
|
||||||
|
if ENVIRONMENT == Environment.LOCAL.value:
|
||||||
|
uris.extend(
|
||||||
|
[
|
||||||
|
f"turn:{TURN_HOST}:{TURN_PORT}?transport=tcp", # TCP for macOS Docker
|
||||||
|
f"turn:{TURN_HOST}:{TURN_PORT}", # UDP fallback
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
uris.extend(
|
||||||
|
[
|
||||||
|
f"turn:{TURN_HOST}:{TURN_PORT}", # UDP preferred for other environments
|
||||||
|
f"turn:{TURN_HOST}:{TURN_PORT}?transport=tcp", # TCP fallback
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add TLS URIs if TLS port is configured
|
||||||
|
if TURN_TLS_PORT:
|
||||||
|
uris.extend(
|
||||||
|
[
|
||||||
|
f"turns:{TURN_HOST}:{TURN_TLS_PORT}", # TURN over TLS
|
||||||
|
f"turns:{TURN_HOST}:{TURN_TLS_PORT}?transport=tcp", # TURN over TLS+TCP
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
"ttl": ttl,
|
||||||
|
"uris": uris,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/credentials", response_model=TurnCredentialsResponse)
|
||||||
|
async def get_turn_credentials(
|
||||||
|
user: UserModel = Depends(get_user),
|
||||||
|
) -> TurnCredentialsResponse:
|
||||||
|
"""Get time-limited TURN credentials for WebRTC connections.
|
||||||
|
|
||||||
|
This endpoint generates ephemeral TURN credentials that are:
|
||||||
|
- Valid for the configured TTL (default: 24 hours)
|
||||||
|
- Cryptographically bound to the user via HMAC
|
||||||
|
- Compatible with coturn's use-auth-secret mode
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TurnCredentialsResponse with username, password, ttl, and TURN URIs
|
||||||
|
"""
|
||||||
|
if not TURN_SECRET:
|
||||||
|
logger.warning("TURN credentials requested but TURN_SECRET not configured")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=503,
|
||||||
|
detail="TURN server not configured",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
credentials = generate_turn_credentials(str(user.id))
|
||||||
|
logger.debug(f"Generated TURN credentials for user {user.id}")
|
||||||
|
return TurnCredentialsResponse(**credentials)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to generate TURN credentials: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Failed to generate TURN credentials",
|
||||||
|
)
|
||||||
|
|
@ -7,12 +7,17 @@ Uses the SmallWebRTC API contract:
|
||||||
- SmallWebRTCConnection for peer connection management
|
- SmallWebRTCConnection for peer connection management
|
||||||
- candidate_from_sdp() for parsing ICE candidates
|
- candidate_from_sdp() for parsing ICE candidates
|
||||||
- add_ice_candidate() for trickling support
|
- add_ice_candidate() for trickling support
|
||||||
|
|
||||||
|
TURN Authentication:
|
||||||
|
- Uses time-limited credentials (TURN REST API) when TURN_SECRET is configured
|
||||||
|
- Credentials are generated per-connection using HMAC-SHA1
|
||||||
|
- Falls back to static credentials if TURN_SECRET is not set (legacy mode)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Dict, List
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from aiortc import RTCIceServer
|
from aiortc import RTCIceServer
|
||||||
from aiortc.sdp import candidate_from_sdp
|
from aiortc.sdp import candidate_from_sdp
|
||||||
|
|
@ -22,6 +27,12 @@ from starlette.websockets import WebSocketState
|
||||||
|
|
||||||
from api.db import db_client
|
from api.db import db_client
|
||||||
from api.db.models import UserModel
|
from api.db.models import UserModel
|
||||||
|
from api.routes.turn_credentials import (
|
||||||
|
TURN_HOST,
|
||||||
|
TURN_PORT,
|
||||||
|
TURN_SECRET,
|
||||||
|
generate_turn_credentials,
|
||||||
|
)
|
||||||
from api.services.auth.depends import get_user_ws
|
from api.services.auth.depends import get_user_ws
|
||||||
from api.services.pipecat.run_pipeline import run_pipeline_smallwebrtc
|
from api.services.pipecat.run_pipeline import run_pipeline_smallwebrtc
|
||||||
from api.services.pipecat.ws_sender_registry import (
|
from api.services.pipecat.ws_sender_registry import (
|
||||||
|
|
@ -35,35 +46,62 @@ from pipecat.utils.context import set_current_run_id
|
||||||
router = APIRouter(prefix="/ws")
|
router = APIRouter(prefix="/ws")
|
||||||
|
|
||||||
|
|
||||||
def get_ice_servers() -> List[RTCIceServer]:
|
def get_ice_servers(user_id: Optional[str] = None) -> List[RTCIceServer]:
|
||||||
"""Build ICE servers configuration including TURN if configured."""
|
"""Build ICE servers configuration including TURN if configured.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: Optional user ID for generating time-limited TURN credentials.
|
||||||
|
If provided and TURN_SECRET is configured, uses TURN REST API.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of RTCIceServer configurations for WebRTC peer connection.
|
||||||
|
"""
|
||||||
servers: List[RTCIceServer] = [RTCIceServer(urls="stun:stun.l.google.com:19302")]
|
servers: List[RTCIceServer] = [RTCIceServer(urls="stun:stun.l.google.com:19302")]
|
||||||
|
|
||||||
# Add TURN server if configured
|
# Check if TURN is configured
|
||||||
turn_host = os.getenv("TURN_HOST")
|
if not TURN_HOST:
|
||||||
|
return servers
|
||||||
|
|
||||||
|
# Use time-limited credentials if TURN_SECRET is configured (recommended)
|
||||||
|
if TURN_SECRET and user_id:
|
||||||
|
try:
|
||||||
|
credentials = generate_turn_credentials(user_id)
|
||||||
|
servers.append(
|
||||||
|
RTCIceServer(
|
||||||
|
urls=credentials["uris"],
|
||||||
|
username=credentials["username"],
|
||||||
|
credential=credentials["password"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"TURN server configured with time-limited credentials, TTL: {credentials['ttl']}s"
|
||||||
|
)
|
||||||
|
return servers
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to generate TURN credentials: {e}")
|
||||||
|
|
||||||
|
# Fallback to static credentials (legacy mode - not recommended for production)
|
||||||
turn_username = os.getenv("TURN_USERNAME")
|
turn_username = os.getenv("TURN_USERNAME")
|
||||||
turn_password = os.getenv("TURN_PASSWORD")
|
turn_password = os.getenv("TURN_PASSWORD")
|
||||||
|
|
||||||
if turn_host and turn_username and turn_password:
|
if turn_username and turn_password:
|
||||||
servers.append(
|
servers.append(
|
||||||
RTCIceServer(
|
RTCIceServer(
|
||||||
urls=[
|
urls=[
|
||||||
f"turn:{turn_host}:3478",
|
f"turn:{TURN_HOST}:{TURN_PORT}",
|
||||||
f"turn:{turn_host}:3478?transport=tcp",
|
f"turn:{TURN_HOST}:{TURN_PORT}?transport=tcp",
|
||||||
],
|
],
|
||||||
username=turn_username,
|
username=turn_username,
|
||||||
credential=turn_password,
|
credential=turn_password,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
logger.info(f"TURN server configured: {turn_host}:3478")
|
logger.warning(
|
||||||
|
f"TURN server configured with static credentials (consider using TURN_SECRET for time-limited auth)"
|
||||||
|
)
|
||||||
|
|
||||||
return servers
|
return servers
|
||||||
|
|
||||||
|
|
||||||
# ICE servers configuration
|
|
||||||
ice_servers = get_ice_servers()
|
|
||||||
|
|
||||||
|
|
||||||
class SignalingManager:
|
class SignalingManager:
|
||||||
"""Manages WebSocket connections and WebRTC peer connections."""
|
"""Manages WebSocket connections and WebRTC peer connections."""
|
||||||
|
|
||||||
|
|
@ -178,8 +216,10 @@ class SignalingManager:
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Create new connection using correct SmallWebRTC API
|
# Create new connection using correct SmallWebRTC API
|
||||||
|
# Generate ICE servers with time-limited TURN credentials for this user
|
||||||
|
user_ice_servers = get_ice_servers(user_id=str(user.id))
|
||||||
pc = SmallWebRTCConnection(
|
pc = SmallWebRTCConnection(
|
||||||
ice_servers=ice_servers, connection_timeout_secs=60
|
ice_servers=user_ice_servers, connection_timeout_secs=60
|
||||||
)
|
)
|
||||||
# Set the pc_id before initialization so it's available in get_answer()
|
# Set the pc_id before initialization so it's available in get_answer()
|
||||||
pc._pc_id = pc_id
|
pc._pc_id = pc_id
|
||||||
|
|
|
||||||
105
config/coturn/turnserver.conf
Normal file
105
config/coturn/turnserver.conf
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
# Coturn TURN Server Configuration
|
||||||
|
# For use with time-limited credentials (TURN REST API)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Listener Configuration
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# TURN listener port for UDP and TCP
|
||||||
|
listening-port=3478
|
||||||
|
|
||||||
|
# TURN listener port for TLS and DTLS
|
||||||
|
tls-listening-port=5349
|
||||||
|
|
||||||
|
# Relay port range for media
|
||||||
|
# These ports are used for actual media relay between peers
|
||||||
|
min-port=49152
|
||||||
|
max-port=49200
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Authentication Configuration (TURN REST API)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Enable TURN REST API authentication (time-limited credentials)
|
||||||
|
# This uses HMAC-SHA1 based authentication with a shared secret
|
||||||
|
# Username format: {expiration_timestamp}:{user_id}
|
||||||
|
# Password: base64(hmac-sha1(secret, username))
|
||||||
|
use-auth-secret
|
||||||
|
|
||||||
|
# Shared secret for TURN REST API authentication
|
||||||
|
# IMPORTANT: Change this value in production!
|
||||||
|
# This must match the TURN_SECRET environment variable in the backend API
|
||||||
|
static-auth-secret=dograh-turn-secret-change-in-production
|
||||||
|
|
||||||
|
# Realm for the TURN server (required for long-term credential mechanism)
|
||||||
|
# This is sent to clients in 401 challenges and used in credential validation
|
||||||
|
realm=dograh.local
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Security Settings
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Use fingerprint in TURN messages for additional security
|
||||||
|
fingerprint
|
||||||
|
|
||||||
|
# Disable multicast peers (security best practice)
|
||||||
|
no-multicast-peers
|
||||||
|
|
||||||
|
# Disable CLI interface (not needed in containerized deployment)
|
||||||
|
no-cli
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Logging Configuration
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Log to stdout for Docker compatibility
|
||||||
|
log-file=stdout
|
||||||
|
|
||||||
|
# Enable verbose logging (comment out in production for performance)
|
||||||
|
verbose
|
||||||
|
|
||||||
|
# Use ISO-8601 timestamps in logs
|
||||||
|
new-log-timestamp
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Performance & Limits
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Total allocation quota (0 = unlimited)
|
||||||
|
# total-quota=0
|
||||||
|
|
||||||
|
# Per-user allocation quota (0 = unlimited)
|
||||||
|
# user-quota=0
|
||||||
|
|
||||||
|
# Max bandwidth per session in bytes per second (0 = unlimited)
|
||||||
|
# max-bps=0
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Network Configuration
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# For Docker with host networking, external-ip is auto-detected
|
||||||
|
# For cloud deployments (AWS, GCP), set external IP explicitly:
|
||||||
|
# external-ip=<PUBLIC_IP>/<PRIVATE_IP>
|
||||||
|
|
||||||
|
# Explicitly bind to all interfaces (required for Docker port mapping)
|
||||||
|
listening-ip=0.0.0.0
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# STUN Configuration
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Allow STUN binding requests (enabled by default)
|
||||||
|
# no-stun
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# TLS Configuration (optional - for TURNS)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Uncomment and set paths for TLS support:
|
||||||
|
# cert=/etc/coturn/certs/turn_server_cert.pem
|
||||||
|
# pkey=/etc/coturn/certs/turn_server_pkey.pem
|
||||||
|
|
||||||
|
# Disable TLS/DTLS if not using certificates
|
||||||
|
no-tls
|
||||||
|
no-dtls
|
||||||
|
|
@ -58,6 +58,23 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
|
|
||||||
|
coturn:
|
||||||
|
image: coturn/coturn:4.8.0
|
||||||
|
container_name: coturn
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3478:3478/udp" # TURN/STUN UDP
|
||||||
|
- "3478:3478/tcp" # TURN/STUN TCP
|
||||||
|
- "5349:5349/tcp" # TURNS (TLS)
|
||||||
|
- "49152-49200:49152-49200/udp" # Relay ports
|
||||||
|
volumes:
|
||||||
|
- ./config/coturn/turnserver.conf:/etc/coturn/turnserver.conf:ro
|
||||||
|
command:
|
||||||
|
- "-c"
|
||||||
|
- "/etc/coturn/turnserver.conf"
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
redis_data:
|
redis_data:
|
||||||
|
|
|
||||||
|
|
@ -114,9 +114,9 @@ services:
|
||||||
SENTRY_DSN: "https://3acdb63d5f1f70430953353b82de61e0@o4509486225096704.ingest.us.sentry.io/4510152922693632"
|
SENTRY_DSN: "https://3acdb63d5f1f70430953353b82de61e0@o4509486225096704.ingest.us.sentry.io/4510152922693632"
|
||||||
|
|
||||||
# TURN server configuration (for WebRTC NAT traversal in remote server)
|
# TURN server configuration (for WebRTC NAT traversal in remote server)
|
||||||
|
# Uses time-limited credentials via TURN REST API (HMAC-SHA1)
|
||||||
TURN_HOST: "${TURN_HOST:-}"
|
TURN_HOST: "${TURN_HOST:-}"
|
||||||
TURN_USERNAME: "${TURN_USERNAME:-}"
|
TURN_SECRET: "${TURN_SECRET:-}"
|
||||||
TURN_PASSWORD: "${TURN_PASSWORD:-}"
|
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
|
|
@ -158,12 +158,6 @@ services:
|
||||||
|
|
||||||
# Sentry
|
# Sentry
|
||||||
SENTRY_DSN: "https://d9387fed5f80e90781f1dbd9b2c0994c@o4509486225096704.ingest.us.sentry.io/4510124708200448"
|
SENTRY_DSN: "https://d9387fed5f80e90781f1dbd9b2c0994c@o4509486225096704.ingest.us.sentry.io/4510124708200448"
|
||||||
|
|
||||||
# TURN server configuration (for WebRTC NAT traversal in remote server)
|
|
||||||
# Fetched at runtime via /api/config/turn endpoint
|
|
||||||
TURN_HOST: "${TURN_HOST:-}"
|
|
||||||
TURN_USERNAME: "${TURN_USERNAME:-}"
|
|
||||||
TURN_PASSWORD: "${TURN_PASSWORD:-}"
|
|
||||||
ports:
|
ports:
|
||||||
- "3010:3010"
|
- "3010:3010"
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
@ -196,10 +190,15 @@ services:
|
||||||
container_name: coturn
|
container_name: coturn
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
profiles: ["remote"]
|
profiles: ["remote"]
|
||||||
network_mode: host
|
ports:
|
||||||
|
- "3478:3478/udp"
|
||||||
|
- "3478:3478/tcp"
|
||||||
|
- "5349:5349/udp"
|
||||||
|
- "5349:5349/tcp"
|
||||||
|
- "49152-49200:49152-49200/udp"
|
||||||
environment:
|
environment:
|
||||||
TURN_USERNAME: ${TURN_USERNAME}
|
TURN_SECRET: ${TURN_SECRET}
|
||||||
TURN_PASSWORD: ${TURN_PASSWORD}
|
TURN_HOST: ${TURN_HOST}
|
||||||
command: >
|
command: >
|
||||||
-n
|
-n
|
||||||
--listening-port=3478
|
--listening-port=3478
|
||||||
|
|
@ -207,8 +206,8 @@ services:
|
||||||
--min-port=49152
|
--min-port=49152
|
||||||
--max-port=49200
|
--max-port=49200
|
||||||
--realm=${TURN_REALM:-dograh.com}
|
--realm=${TURN_REALM:-dograh.com}
|
||||||
--user=${TURN_USERNAME}:${TURN_PASSWORD}
|
--use-auth-secret
|
||||||
--lt-cred-mech
|
--static-auth-secret=${TURN_SECRET}
|
||||||
--fingerprint
|
--fingerprint
|
||||||
--no-cli
|
--no-cli
|
||||||
--log-file=stdout
|
--log-file=stdout
|
||||||
|
|
@ -216,6 +215,8 @@ services:
|
||||||
--no-tlsv1
|
--no-tlsv1
|
||||||
--no-tlsv1_1
|
--no-tlsv1_1
|
||||||
${TURN_HOST:+--external-ip=$TURN_HOST}
|
${TURN_HOST:+--external-ip=$TURN_HOST}
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
|
|
||||||
|
|
@ -30,20 +30,20 @@ if ! [[ "$SERVER_IP" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Get the TURN password
|
# Get the TURN secret
|
||||||
echo -e "${YELLOW}Enter a password for the TURN server (press Enter for default 'dograh-turn-secret'):${NC}"
|
echo -e "${YELLOW}Enter a shared secret for the TURN server (press Enter to generate a random one):${NC}"
|
||||||
read -sp "> " TURN_PASSWORD
|
read -sp "> " TURN_SECRET
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [[ -z "$TURN_PASSWORD" ]]; then
|
if [[ -z "$TURN_SECRET" ]]; then
|
||||||
TURN_PASSWORD="dograh-turn-secret"
|
TURN_SECRET=$(openssl rand -hex 32)
|
||||||
echo -e "${BLUE}Using default TURN password${NC}"
|
echo -e "${BLUE}Generated random TURN secret${NC}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}Configuration:${NC}"
|
echo -e "${GREEN}Configuration:${NC}"
|
||||||
echo -e " Server IP: ${BLUE}$SERVER_IP${NC}"
|
echo -e " Server IP: ${BLUE}$SERVER_IP${NC}"
|
||||||
echo -e " TURN Password: ${BLUE}********${NC}"
|
echo -e " TURN Secret: ${BLUE}********${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Create project directory if it doesn't exist
|
# Create project directory if it doesn't exist
|
||||||
|
|
@ -135,10 +135,9 @@ echo -e "${GREEN}✓ SSL certificates generated${NC}"
|
||||||
|
|
||||||
echo -e "${BLUE}[5/5] Creating environment file...${NC}"
|
echo -e "${BLUE}[5/5] Creating environment file...${NC}"
|
||||||
cat > .env << ENV_EOF
|
cat > .env << ENV_EOF
|
||||||
# TURN Server Configuration
|
# TURN Server Configuration (time-limited credentials via TURN REST API)
|
||||||
TURN_HOST=$SERVER_IP
|
TURN_HOST=$SERVER_IP
|
||||||
TURN_USERNAME=dograh
|
TURN_SECRET=$TURN_SECRET
|
||||||
TURN_PASSWORD=$TURN_PASSWORD
|
|
||||||
|
|
||||||
# Telemetry (set to false to disable)
|
# Telemetry (set to false to disable)
|
||||||
ENABLE_TELEMETRY=true
|
ENABLE_TELEMETRY=true
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
/*
|
|
||||||
Route to provide TURN server configuration at runtime.
|
|
||||||
This allows OSS users to configure TURN servers via docker-compose.yaml
|
|
||||||
environment variables, since NEXT_PUBLIC_* keys are injected at build time.
|
|
||||||
*/
|
|
||||||
import { NextResponse } from 'next/server';
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
const host = process.env.TURN_HOST || '';
|
|
||||||
const username = process.env.TURN_USERNAME || '';
|
|
||||||
const password = process.env.TURN_PASSWORD || '';
|
|
||||||
|
|
||||||
// Only return enabled: true if all required fields are set
|
|
||||||
const enabled = !!(host && username && password);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
enabled,
|
|
||||||
host,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { client } from "@/client/client.gen";
|
import { client } from "@/client/client.gen";
|
||||||
import { validateUserConfigurationsApiV1UserConfigurationsUserValidateGet, validateWorkflowApiV1WorkflowWorkflowIdValidatePost } from "@/client/sdk.gen";
|
import { getTurnCredentialsApiV1TurnCredentialsGet, validateUserConfigurationsApiV1UserConfigurationsUserValidateGet, validateWorkflowApiV1WorkflowWorkflowIdValidatePost } from "@/client/sdk.gen";
|
||||||
|
import { TurnCredentialsResponse } from "@/client/types.gen";
|
||||||
import { WorkflowValidationError } from "@/components/flow/types";
|
import { WorkflowValidationError } from "@/components/flow/types";
|
||||||
import logger from '@/lib/logger';
|
import logger from '@/lib/logger';
|
||||||
|
|
||||||
|
|
@ -57,13 +58,9 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia
|
||||||
const useAudio = true;
|
const useAudio = true;
|
||||||
const audioCodec = 'default';
|
const audioCodec = 'default';
|
||||||
|
|
||||||
// TURN server configuration fetched at runtime from /api/config/turn
|
// TURN server credentials fetched at runtime from backend API
|
||||||
const turnConfigRef = useRef<{
|
// Uses time-limited credentials (TURN REST API) for security
|
||||||
enabled: boolean;
|
const turnCredentialsRef = useRef<TurnCredentialsResponse | null>(null);
|
||||||
host: string;
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
const audioRef = useRef<HTMLAudioElement>(null);
|
const audioRef = useRef<HTMLAudioElement>(null);
|
||||||
const pcRef = useRef<RTCPeerConnection | null>(null);
|
const pcRef = useRef<RTCPeerConnection | null>(null);
|
||||||
|
|
@ -100,19 +97,16 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia
|
||||||
iceServers.push({ urls: ['stun:stun.l.google.com:19302'] });
|
iceServers.push({ urls: ['stun:stun.l.google.com:19302'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add TURN server if configured (fetched from /api/config/turn)
|
// Add TURN server if credentials are available (time-limited credentials from backend)
|
||||||
const turnConfig = turnConfigRef.current;
|
const turnCredentials = turnCredentialsRef.current;
|
||||||
if (turnConfig?.enabled) {
|
if (turnCredentials?.uris && turnCredentials.uris.length > 0) {
|
||||||
iceServers.push({
|
iceServers.push({
|
||||||
urls: [
|
urls: turnCredentials.uris,
|
||||||
`turn:${turnConfig.host}:3478`, // TURN over UDP
|
username: turnCredentials.username,
|
||||||
`turn:${turnConfig.host}:3478?transport=tcp`, // TURN over TCP
|
credential: turnCredentials.password
|
||||||
],
|
|
||||||
username: turnConfig.username,
|
|
||||||
credential: turnConfig.password
|
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`TURN server configured: ${turnConfig.host}:3478`);
|
logger.info(`TURN server configured with ${turnCredentials.uris.length} URIs, TTL: ${turnCredentials.ttl}s`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const config: RTCConfiguration = {
|
const config: RTCConfiguration = {
|
||||||
|
|
@ -467,17 +461,24 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia
|
||||||
setConnectionStatus('connecting');
|
setConnectionStatus('connecting');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch TURN configuration at runtime
|
// Fetch time-limited TURN credentials from backend API
|
||||||
try {
|
try {
|
||||||
const turnResponse = await fetch('/api/config/turn');
|
const turnResponse = await getTurnCredentialsApiV1TurnCredentialsGet({
|
||||||
if (turnResponse.ok) {
|
headers: {
|
||||||
turnConfigRef.current = await turnResponse.json();
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
if (turnConfigRef.current?.enabled) {
|
},
|
||||||
logger.info('TURN server enabled via runtime config');
|
});
|
||||||
}
|
if (turnResponse.data) {
|
||||||
|
turnCredentialsRef.current = turnResponse.data;
|
||||||
|
logger.info(`TURN credentials obtained, TTL: ${turnCredentialsRef.current.ttl}s`);
|
||||||
|
} else if (turnResponse.response.status === 503) {
|
||||||
|
// TURN not configured on server - this is OK, we'll use STUN only
|
||||||
|
logger.info('TURN server not configured, using STUN only');
|
||||||
|
} else {
|
||||||
|
logger.warn(`Failed to fetch TURN credentials: ${turnResponse.response.status}`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn('Failed to fetch TURN config, continuing without TURN:', e);
|
logger.warn('Failed to fetch TURN credentials, continuing without TURN:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate API keys
|
// Validate API keys
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -879,6 +879,16 @@ export type TriggerCallResponse = {
|
||||||
workflow_run_name: string;
|
workflow_run_name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response model for TURN credentials.
|
||||||
|
*/
|
||||||
|
export type TurnCredentialsResponse = {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
ttl: number;
|
||||||
|
uris: Array<string>;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request schema for Twilio configuration.
|
* Request schema for Twilio configuration.
|
||||||
*/
|
*/
|
||||||
|
|
@ -4201,6 +4211,39 @@ export type GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponses = {
|
||||||
|
|
||||||
export type GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponse = GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponses[keyof GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponses];
|
export type GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponse = GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponses[keyof GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponses];
|
||||||
|
|
||||||
|
export type GetTurnCredentialsApiV1TurnCredentialsGetData = {
|
||||||
|
body?: never;
|
||||||
|
headers?: {
|
||||||
|
authorization?: string | null;
|
||||||
|
'X-API-Key'?: string | null;
|
||||||
|
};
|
||||||
|
path?: never;
|
||||||
|
query?: never;
|
||||||
|
url: '/api/v1/turn/credentials';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetTurnCredentialsApiV1TurnCredentialsGetErrors = {
|
||||||
|
/**
|
||||||
|
* Not found
|
||||||
|
*/
|
||||||
|
404: unknown;
|
||||||
|
/**
|
||||||
|
* Validation Error
|
||||||
|
*/
|
||||||
|
422: HttpValidationError;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetTurnCredentialsApiV1TurnCredentialsGetError = GetTurnCredentialsApiV1TurnCredentialsGetErrors[keyof GetTurnCredentialsApiV1TurnCredentialsGetErrors];
|
||||||
|
|
||||||
|
export type GetTurnCredentialsApiV1TurnCredentialsGetResponses = {
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
200: TurnCredentialsResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetTurnCredentialsApiV1TurnCredentialsGetResponse = GetTurnCredentialsApiV1TurnCredentialsGetResponses[keyof GetTurnCredentialsApiV1TurnCredentialsGetResponses];
|
||||||
|
|
||||||
export type OptionsInitApiV1PublicEmbedInitOptionsData = {
|
export type OptionsInitApiV1PublicEmbedInitOptionsData = {
|
||||||
body?: never;
|
body?: never;
|
||||||
path?: never;
|
path?: never;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue