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
138 lines
4.8 KiB
Python
138 lines
4.8 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.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.
|
|
"""
|
|
import socket
|
|
|
|
# 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
|
|
|
|
# We need a DB session to look up the terminal session
|
|
# Since we're in a WebSocket endpoint, we need to create one manually
|
|
from app.core.database import SessionLocal
|
|
async with SessionLocal() 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
|