66 lines
2.1 KiB
Python
66 lines
2.1 KiB
Python
"""Transparent WebSocket frame relay — no parse, no audio processing."""
|
|
|
|
import asyncio
|
|
import logging
|
|
|
|
import websockets
|
|
from fastapi import WebSocket, WebSocketDisconnect
|
|
from websockets.exceptions import ConnectionClosed
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def relay_text_websockets(left: WebSocket, right: WebSocket) -> None:
|
|
stop = asyncio.Event()
|
|
|
|
async def forward(source: WebSocket, destination: WebSocket) -> None:
|
|
try:
|
|
while not stop.is_set():
|
|
message = await source.receive_text()
|
|
await destination.send_text(message)
|
|
except (WebSocketDisconnect, RuntimeError):
|
|
stop.set()
|
|
|
|
await asyncio.gather(forward(left, right), forward(right, left))
|
|
|
|
|
|
async def relay_provider_to_upstream(
|
|
provider: WebSocket,
|
|
upstream_url: str,
|
|
*,
|
|
initial_frames: list[str] | None = None,
|
|
additional_headers: dict[str, str] | None = None,
|
|
) -> None:
|
|
"""Raw carrier ↔ customer upstream tunnel."""
|
|
logger.info("Opening upstream tunnel to %s", upstream_url)
|
|
async with websockets.connect(
|
|
upstream_url,
|
|
open_timeout=10,
|
|
close_timeout=5,
|
|
additional_headers=additional_headers,
|
|
ping_interval=20,
|
|
ping_timeout=20,
|
|
max_queue=16,
|
|
) as upstream:
|
|
for frame in initial_frames or []:
|
|
await upstream.send(frame)
|
|
|
|
async def provider_to_upstream() -> None:
|
|
try:
|
|
while True:
|
|
raw = await provider.receive_text()
|
|
await upstream.send(raw)
|
|
except (WebSocketDisconnect, RuntimeError):
|
|
return
|
|
|
|
async def upstream_to_provider() -> None:
|
|
try:
|
|
while True:
|
|
raw = await upstream.recv()
|
|
if isinstance(raw, bytes):
|
|
raw = raw.decode("utf-8")
|
|
await provider.send_text(raw)
|
|
except (ConnectionClosed, WebSocketDisconnect, RuntimeError):
|
|
return
|
|
|
|
await asyncio.gather(provider_to_upstream(), upstream_to_provider())
|