Bruno Charest 70c15c9b6f
Some checks failed
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
Add debug mode feature flag with environment variable parsing, UI badge indicator, secret redaction utility, and enhanced terminal session management with status checks and session limit error handling
2025-12-21 17:22:36 -05:00

135 lines
4.7 KiB
Python

"""
Routes WebSocket pour les mises à jour en temps réel.
"""
import asyncio
import logging
from typing import Optional
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends, HTTPException, status
from app.core.config import settings
from app.models.database import async_session_maker
from app.services import ws_manager
from app.crud.terminal_session import TerminalSessionRepository
from app.services.terminal_service import terminal_service
logger = logging.getLogger(__name__)
router = APIRouter()
@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
"""Endpoint WebSocket pour les mises à jour en temps réel."""
await ws_manager.connect(websocket)
try:
while True:
# Garder la connexion ouverte
data = await websocket.receive_text()
# Traiter les messages entrants si nécessaire
except WebSocketDisconnect:
ws_manager.disconnect(websocket)
@router.websocket("/terminal/ws/{session_id}")
async def terminal_websocket_proxy(
websocket: WebSocket,
session_id: str,
):
"""
WebSocket proxy for ttyd terminal sessions.
This endpoint proxies WebSocket connections to the local ttyd instance,
allowing the browser to connect through the exposed port 8008 instead of
needing direct access to the ttyd port (7680+).
The session_id is used to look up the ttyd port from the database.
Authentication is handled via session token in the query string.
"""
# Get token from query params
token = websocket.query_params.get("token")
if not token:
await websocket.close(code=status.WS_1008_POLICY_VIOLATION, reason="Missing token")
return
async with async_session_maker() as db_session:
session_repo = TerminalSessionRepository(db_session)
session = await session_repo.get(session_id)
if not session:
await websocket.close(code=status.WS_1008_POLICY_VIOLATION, reason="Session not found")
return
# Verify token
if not terminal_service.verify_token(token, session.token_hash):
await websocket.close(code=status.WS_1008_POLICY_VIOLATION, reason="Invalid token")
return
# Accept the WebSocket connection
await websocket.accept()
# Connect to local ttyd via TCP socket
ttyd_port = session.ttyd_port
ttyd_host = "127.0.0.1" # ttyd is always local in the container
try:
# Create a TCP socket to ttyd
reader, writer = await asyncio.open_connection(ttyd_host, ttyd_port)
logger.info(f"Proxying WebSocket for session {session_id[:8]}... to ttyd on port {ttyd_port}")
# Bidirectional proxy: forward messages between WebSocket and ttyd
async def forward_ws_to_ttyd():
"""Forward WebSocket messages to ttyd."""
try:
while True:
data = await websocket.receive_bytes()
writer.write(data)
await writer.drain()
except WebSocketDisconnect:
logger.debug(f"WebSocket disconnected for session {session_id[:8]}...")
except Exception as e:
logger.error(f"Error forwarding WS to ttyd: {e}")
finally:
try:
writer.close()
await writer.wait_closed()
except Exception:
pass
async def forward_ttyd_to_ws():
"""Forward ttyd messages to WebSocket."""
try:
while True:
data = await reader.readexactly(1024)
if not data:
break
await websocket.send_bytes(data)
except asyncio.IncompleteReadError:
# Normal EOF
pass
except Exception as e:
logger.error(f"Error forwarding ttyd to WS: {e}")
finally:
try:
await websocket.close()
except Exception:
pass
# Run both directions concurrently
await asyncio.gather(
forward_ws_to_ttyd(),
forward_ttyd_to_ws(),
return_exceptions=True
)
except ConnectionRefusedError:
logger.error(f"Failed to connect to ttyd on {ttyd_host}:{ttyd_port} for session {session_id[:8]}...")
await websocket.close(code=1011, reason="ttyd connection failed")
except Exception as e:
logger.exception(f"Error in terminal WebSocket proxy: {e}")
try:
await websocket.close(code=1011, reason="Proxy error")
except Exception:
pass