diff --git a/docs/features/vnc-spice-websocket-console.md b/docs/features/vnc-spice-websocket-console.md deleted file mode 100644 index 410bcbfae..000000000 --- a/docs/features/vnc-spice-websocket-console.md +++ /dev/null @@ -1,871 +0,0 @@ -# VNC/SPICE WebSocket Console Support - -## Feature Overview - -**Date**: 2026-03-16 -**Status**: Implemented -**Branch**: `feature/vnc-interface` -**Component**: Compute API, Controller API, BaseNode - -### Summary - -Add WebSocket-based console support for VNC and SPICE console types, enabling browser-based graphical console access to QEMU VMs and Docker containers without requiring standalone websockify processes. - ---- - -## Architecture - -### Design Principles - -1. **Unified WebSocket Architecture**: All console types (Telnet, VNC, SPICE) use consistent WebSocket endpoints -2. **Authentication Integration**: Leverage existing GNS3 authentication mechanisms -3. **No External Dependencies**: Eliminate standalone websockify processes and additional port allocations -4. **Layered Design**: Controller → Compute → Node WebSocket forwarding - -### Architecture Diagram - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Browser Client │ -│ - noVNC for VNC │ -│ - spice-html5 for SPICE │ -└────────────────────────┬────────────────────────────────────────┘ - │ WebSocket Connection - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ GNS3 Controller API │ -│ - /v3/controller/projects/{id}/qemu/nodes/{id}/console/vnc │ -│ - /v3/controller/projects/{id}/qemu/nodes/{id}/console/spice │ -│ - HTTP Basic Auth + RBAC (Node.Console privilege) │ -└────────────────────────┬────────────────────────────────────────┘ - │ WebSocket Forwarding - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ GNS3 Compute API │ -│ - /v3/compute/projects/{id}/qemu/nodes/{id}/console/vnc │ -│ - /v3/compute/projects/{id}/qemu/nodes/{id}/console/spice │ -│ - HTTP Basic Auth (compute credentials) │ -└────────────────────────┬────────────────────────────────────────┘ - │ WebSocket → TCP Bridge - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ BaseNode.start_vnc_websocket_console() │ -│ BaseNode.start_spice_websocket_console() │ -│ - Bidirectional binary data forwarding │ -│ - WebSocket → TCP (VNC:5900+, SPICE:5900+) │ -└────────────────────────┬────────────────────────────────────────┘ - │ TCP Connection - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ QEMU VM / Docker Container │ -│ - VNC Server (QXL display) │ -│ - SPICE Server (with optional spice-vdagent) │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## API Endpoints - -### Compute Layer - -#### VNC Console WebSocket - -``` -WS /v3/compute/projects/{project_id}/qemu/nodes/{node_id}/console/vnc -WS /v3/compute/projects/{project_id}/docker/nodes/{node_id}/console/vnc -``` - -**Authentication**: HTTP Basic Auth (compute credentials) - -**Request Headers**: -```http -Upgrade: websocket -Connection: Upgrade -Authorization: Basic -``` - -**Response**: -- Binary WebSocket connection for VNC protocol (RFB) - -**Implementation**: -```python -# gns3server/api/routes/compute/qemu_nodes.py -@router.websocket("/{node_id}/console/vnc") -async def vnc_console_ws( - websocket: WebSocket = Depends(ws_compute_authentication), - node: QemuVM = Depends(dep_node) -): - await node.start_vnc_websocket_console(websocket) -``` - ---- - -#### SPICE Console WebSocket - -``` -WS /v3/compute/projects/{project_id}/qemu/nodes/{node_id}/console/spice -WS /v3/compute/projects/{project_id}/docker/nodes/{node_id}/console/spice -``` - -**Authentication**: HTTP Basic Auth (compute credentials) - -**Request Headers**: -```http -Upgrade: websocket -Connection: Upgrade -Authorization: Basic -``` - -**Response**: -- Binary WebSocket connection for SPICE protocol - -**Implementation**: -```python -# gns3server/api/routes/compute/qemu_nodes.py -@router.websocket("/{node_id}/console/spice") -async def spice_console_ws( - websocket: WebSocket = Depends(ws_compute_authentication), - node: QemuVM = Depends(dep_node) -): - await node.start_spice_websocket_console(websocket) -``` - ---- - -### Controller Layer - -#### VNC Console WebSocket - -``` -WS /v3/controller/projects/{project_id}/qemu/nodes/{node_id}/console/vnc -WS /v3/controller/projects/{project_id}/docker/nodes/{node_id}/console/vnc -``` - -**Authentication**: User JWT token + RBAC privilege check - -**Required Privilege**: `Node.Console` - -**Response**: -- Binary WebSocket connection (proxied to compute layer) - ---- - -#### SPICE Console WebSocket - -``` -WS /v3/controller/projects/{project_id}/qemu/nodes/{node_id}/console/spice -WS /v3/controller/projects/{project_id}/docker/nodes/{node_id}/console/spice -``` - -**Authentication**: User JWT token + RBAC privilege check - -**Required Privilege**: `Node.Console` - -**Response**: -- Binary WebSocket connection (proxied to compute layer) - ---- - -## Console Types - -### Supported Console Types - -| Console Type | Description | Node Support | -|-------------|-------------|-------------| -| `telnet` | Serial console via Telnet protocol | All nodes | -| `vnc` | Graphical console via VNC (RFB) protocol | QEMU, Docker | -| `spice` | Enhanced graphical console via SPICE protocol | QEMU only* | -| `spice+agent` | SPICE with spice-vdagent for features like clipboard sharing, folder sharing | QEMU only* | -| `none` | No console | All nodes | - -*Docker supports the console type in schema but typically doesn't use SPICE (SPICE is designed for KVM/QEMU VMs). - -### Console Type Configuration - -**QEMU VM**: -```json -{ - "console_type": "spice+agent", - "console": 5900 -} -``` - -**Docker Container**: -```json -{ - "console_type": "vnc", - "console": 5900 -} -``` - ---- - -## Implementation Details - -### BaseNode WebSocket Forwarding - -**File**: `gns3server/compute/base_node.py` - -#### VNC Console Implementation - -```python -async def start_vnc_websocket_console(self, websocket): - """ - Connect to VNC console using WebSocket. - - :param websocket: FastAPI WebSocket object - """ - # 1. Validation - if self.status != "started": - await websocket.close(code=1000) - raise NodeError(f"Node {self.name} is not started") - if self._console_type != "vnc": - await websocket.close(code=1000) - raise NodeError(f"Node {self.name} console type is not vnc") - - # 2. Connect to VNC server - vnc_reader, vnc_writer = await asyncio.open_connection( - self._manager.port_manager.console_host, - self.console # VNC TCP port (e.g., 5900) - ) - - # 3. Bidirectional forwarding - async def ws_forward(vnc_writer): - # Browser → VNC - while True: - data = await websocket.receive_bytes() - vnc_writer.write(data) - await vnc_writer.drain() - - async def vnc_forward(vnc_reader): - # VNC → Browser - while not vnc_reader.at_eof(): - data = await vnc_reader.read(65536) # Larger buffer for VNC frames - await websocket.send_bytes(data) - - # 4. Run both directions concurrently - aws = [ - asyncio.create_task(ws_forward(vnc_writer)), - asyncio.create_task(vnc_forward(vnc_reader)) - ] - done, pending = await asyncio.wait(aws, return_when=asyncio.FIRST_COMPLETED) - - # 5. Cleanup - for task in pending: - task.cancel() - vnc_writer.close() - await vnc_writer.wait_closed() -``` - -#### SPICE Console Implementation - -SPICE implementation is identical to VNC, except for: -- Console type validation: `self._console_type in ("spice", "spice+agent")` -- Connection target: SPICE TCP port -- Log messages reference SPICE instead of VNC - ---- - -### Controller Layer Proxy - -**File**: `gns3server/api/routes/controller/nodes.py` - -```python -@router.websocket("/{node_id}/console/spice") -async def spice_console( - websocket: WebSocket, - current_user: schemas.User = Depends(has_privilege_on_websocket("Node.Console")), - node: Node = Depends(dep_node) -): - """ - SPICE WebSocket console. - - Required privilege: Node.Console - """ - # 1. Authentication & Authorization - if current_user is None: - return - - # 2. Build compute layer URL - compute = node.compute - spice_console_compute_url = ( - f"{websocket.url.scheme}://{compute.host}:{compute.port}" - f"/v3/compute/projects/{node.project.id}/{node.node_type}/nodes/{node.id}/console/spice" - ) - - # 3. Forward to compute layer - async def spice_receive(spice_console_compute): - """Client → Compute""" - while True: - data = await websocket.receive_bytes() - await spice_console_compute.send_bytes(data) - - async with HTTPClient.get_client().ws_connect( - spice_console_compute_url, - auth=aiohttp.BasicAuth(user, password), - ssl_context=ssl_context - ) as ws: - asyncio.ensure_future(spice_receive(ws)) - async for msg in ws: - if msg.type == aiohttp.WSMsgType.BINARY: - await websocket.send_bytes(msg.data) # Compute → Client -``` - ---- - -## Authentication Flow - -### Compute Layer Authentication - -**File**: `gns3server/api/routes/compute/dependencies/authentication.py` - -```python -async def ws_compute_authentication(websocket: WebSocket) -> Union[None, WebSocket]: - server_settings = Config.instance().settings.Server - - # Accept connection first - await websocket.accept() - - # Skip auth if disabled - if not server_settings.enable_http_auth: - return websocket - - # Extract Authorization header - authorization = websocket.headers.get("Authorization") - scheme, param = get_authorization_scheme_param(authorization) - - # Validate Basic Auth - if scheme.lower() != "basic": - await websocket.close(code=1008) - return websocket - - # Decode and verify credentials - data = base64.b64decode(param).decode("ascii") - username, separator, password = data.partition(":") - - username_valid = secrets.compare_digest(username, server_settings.compute_username) - password_valid = secrets.compare_digest(password, server_settings.compute_password.get_secret_value()) - - if not (username_valid and password_valid): - await websocket.close(code=1008) - return websocket - - return websocket -``` - -### Controller Layer Authentication - -```python -# WebSocket dependency for privilege checking -async def has_privilege_on_websocket(privilege_id: str): - async def _has_privilege(websocket: WebSocket): - # 1. Extract and verify JWT token - token = websocket.query_params.get("ticket") - current_user = decode_jwt_token(token) - - # 2. Check privilege - if not check_privilege(current_user, privilege_id): - await websocket.close(code=1008) - return None - - return current_user - - return _has_privilege -``` - ---- - -## Usage Examples - -### JavaScript Client (noVNC) - -```javascript -// Connect to VNC console -const host = "gns3-server.example.com"; -const port = 3080; // GNS3 controller port -const projectId = "uuid"; -const nodeId = "uuid"; -const token = "jwt_token"; // User authentication token - -const vncUrl = `wss://${host}:${port}/v3/controller/projects/${projectId}/qemu/nodes/${nodeId}/console/vnc?ticket=${token}`; - -const rfb = new RFB({ - target: document.getElementById('vnc-canvas'), - url: vncUrl, - credentials: { password: '' } // VNC password (if configured) -}); - -rfb.addEventListener("connect", () => { - console.log("Connected to VNC console"); -}); - -rfb.addEventListener("disconnect", () => { - console.log("Disconnected from VNC console"); -}); -``` - -### JavaScript Client (spice-html5) - -```javascript -// Connect to SPICE console -const host = "gns3-server.example.com"; -const port = 3080; -const projectId = "uuid"; -const nodeId = "uuid"; -const token = "jwt_token"; - -const spiceUrl = `wss://${host}:${port}/v3/controller/projects/${projectId}/qemu/nodes/${nodeId}/console/spice?ticket=${token}`; - -const client = new SpiceHtml5.Client({ - uri: spiceUrl, - screenId: 'spice-screen', - password: '', // SPICE password (if configured) - tls: true // Use TLS -}); - -client.addEventListener('connect', () => { - console.log("Connected to SPICE console"); -}); - -client.connect(); -``` - -### Python Client (websockets) - -```python -import asyncio -import websockets -import base64 - -async def connect_vnc_console(host, port, project_id, node_id, username, password): - # Compute endpoint URL - url = f"ws://{host}:{port}/v3/compute/projects/{project_id}/qemu/nodes/{node_id}/console/vnc" - - # HTTP Basic Auth - credentials = base64.b64encode(f"{username}:{password}".encode()).decode() - headers = {"Authorization": f"Basic {credentials}"} - - async with websockets.connect(url, extra_headers=headers) as ws: - # Send/receive VNC binary data - while True: - data = await ws.recv() - # Process VNC protocol data... - -asyncio.run(connect_vnc_console("localhost", 3080, "...", "...", "admin", "password")) -``` - -### cURL (WebSocket Test) - -```bash -# Test WebSocket connection (using websocat) -websocat \ - -H "Authorization: Basic $(echo -n 'admin:password' | base64)" \ - ws://localhost:3080/v3/compute/projects/{project_id}/qemu/nodes/{node_id}/console/vnc -``` - ---- - -## Comparison: Old vs New Architecture - -### Old Architecture (websockify standalone) - -``` -Browser → websockify process 1 → VNC TCP (5900) - → websockify process 2 → VNC TCP (5901) - → websockify process N → VNC TCP (590N) - -Issues: -- Each node requires separate websockify process -- Each node consumes additional WebSocket port -- No authentication on websockify port -- Complex process lifecycle management -- Resource intensive (many processes) -``` - -### New Architecture (GNS3 API WebSocket) - -``` -Browser → GNS3 Controller WebSocket (single port) - → GNS3 Compute WebSocket - → Internal TCP bridge to VNC/SPICE - -Advantages: -- Single WebSocket endpoint -- Integrated authentication -- No external processes -- Consistent with telnet console -- Lower resource footprint -``` - ---- - -## Configuration - -### Server Settings - -**File**: `gns3server/config.py` - -```yaml -Server: - # Enable/disable compute authentication - enable_http_auth: true - - # Compute credentials (for controller → compute auth) - compute_username: "admin" - compute_password: "password" - - # Console host configuration - console_host: "0.0.0.0" # or "::" for IPv6 -``` - -### QEMU VM Console Configuration - -```python -# Start QEMU VM with SPICE console -node = await qemu_manager.create_node( - project_id=project_id, - name="ubuntu-vm", - console_type="spice+agent", # SPICE with guest agent - console=5900, # SPICE port - # ... other QEMU parameters -) -``` - -**Resulting QEMU Command**: -```bash -qemu-system-x86_64 \ - -spice addr=0.0.0.0,port=5900,disable-ticketing \ - -vga qxl \ - -device virtio-serial \ - -chardev spicevmc,id=vdagent,debug=0,name=vdagent \ - -device virtserialport,chardev=vdagent,name=com.redhat.spice.0 \ - # ... other QEMU options -``` - -### Docker Container Console Configuration - -```python -# Start Docker container with VNC console -node = await docker_manager.create_node( - project_id=project_id, - name="ubuntu-container", - image="ubuntu:latest", - console_type="vnc", - console=5900, - console_resolution="1280x720", - # ... other Docker parameters -) -``` - -**Result**: -- TigerVNC server starts in container -- VNC server listens on port 5900 -- WebSocket endpoint available at `/console/vnc` - ---- - -## Performance Considerations - -### Buffer Sizes - -- **VNC frames**: Up to 65536 bytes per read (larger for graphics) -- **SPICE data**: Up to 65536 bytes per read -- **WebSocket → TCP**: Unbuffered (immediate write) -- **TCP → WebSocket**: Buffered reads for efficiency - -### Concurrent Connections - -- **Multiple clients**: Supported (like telnet console) -- **Broadcast behavior**: Each WebSocket connection is independent -- **Resource usage**: Linear with number of concurrent connections - -### Network Bandwidth - -| Console Type | Typical Bandwidth | Notes | -|-------------|-------------------|-------| -| Telnet | < 1 KB/s | Text only | -| VNC | 1-10 MB/s | Depends on screen changes | -| SPICE | 1-20 MB/s | Variable compression, can be higher with video | - ---- - -## Troubleshooting - -### Common Issues - -#### 1. Connection Refused - -**Symptom**: `WebSocket connection to 'ws://...' failed: Error in connection establishment` - -**Possible Causes**: -- Node not started -- Wrong console type configured -- Firewall blocking port - -**Solution**: -```bash -# Check node status -curl -X GET http://localhost:3080/v3/controller/projects/{project_id}/qemu/nodes/{node_id} - -# Verify console type -curl -X GET http://localhost:3080/v3/controller/projects/{project_id}/qemu/nodes/{node_id} | jq '.console_type' -``` - ---- - -#### 2. Authentication Failed - -**Symptom**: WebSocket closes with code 1008 - -**Possible Causes**: -- Invalid credentials -- Missing Authorization header -- `enable_http_auth` enabled but no credentials provided - -**Solution**: -```python -# Verify compute credentials -# In gns3_server configuration -Server.compute_username = "admin" -Server.compute_password = "password" - -# Test with curl -curl -v \ - -H "Authorization: Basic $(echo -n 'admin:password' | base64)" \ - http://localhost:3080/v3/compute/projects/{project_id}/qemu/nodes/{node_id} -``` - ---- - -#### 3. Console Type Mismatch - -**Symptom**: `Node console type is not vnc` or `Node console type is not SPICE` - -**Possible Causes**: -- Node configured with different console type -- Wrong WebSocket endpoint used - -**Solution**: -```bash -# Check actual console type -curl http://localhost:3080/v3/controller/projects/{project_id}/qemu/nodes/{node_id} | jq '.console_type' - -# Use correct endpoint -# - If console_type == "vnc": /console/vnc -# - If console_type == "spice" or "spice+agent": /console/spice -``` - ---- - -#### 4. SPICE Not Working - -**Symptom**: Cannot connect to SPICE console - -**Possible Causes**: -- QEMU not built with SPICE support -- SPICE port already in use -- Node not started - -**Solution**: -```bash -# Check QEMU SPICE support -qemu-system-x86_64 --help | grep spice - -# Check if SPICE port is listening -netstat -tuln | grep 5900 - -# Check QEMU process -ps aux | grep qemu-system | grep spice -``` - ---- - -## Testing - -### Unit Tests - -**Test VNC WebSocket forwarding**: -```python -async def test_vnc_websocket_console(): - # Create QEMU node with VNC console - node = QemuVM( - name="test-vm", - console_type="vnc", - console=5900, - # ... - ) - - # Start node - await node.start() - - # Create mock WebSocket - websocket = MockWebSocket() - - # Connect - await node.start_vnc_websocket_console(websocket) - - # Verify data forwarding - assert websocket.sent_data # VNC data received -``` - -### Integration Tests - -**Test end-to-end VNC console**: -```bash -# 1. Start GNS3 server -gns3server --local - -# 2. Create project with QEMU VM -curl -X POST http://localhost:3080/v3/controller/projects \ - -H "Content-Type: application/json" \ - -d '{"name": "test-project"}' - -# 3. Start QEMU VM with VNC -# (via API or GUI) - -# 4. Connect WebSocket -websocat -H "Authorization: Basic ..." \ - ws://localhost:3080/v3/compute/projects/{id}/qemu/nodes/{id}/console/vnc - -# 5. Send VNC handshake -# (requires RFB protocol client) -``` - ---- - -## Future Enhancements - -### Potential Improvements - -1. **WebSocket Compression** - - Enable permessage-deflate compression - - Reduce bandwidth for text-heavy consoles - -2. **Session Recording** - - Record VNC/SPICE sessions for playback - - Useful for training and debugging - -3. **Clipboard Integration** - - SPICE vdagent already supports clipboard - - Could expose via WebSocket extensions - -4. **File Transfer** - - SPICE supports folder sharing via webdav - - Could integrate with GNS3 file management - -5. **Audio Support** - - SPICE supports audio streaming - - Could add WebSocket audio forwarding - ---- - -## Security Considerations - -### Authentication - -- ✅ **Controller Layer**: JWT token + RBAC privilege check -- ✅ **Compute Layer**: HTTP Basic Auth -- ✅ **No standalone websockify**: Single authentication point -- ❌ **Old websockify approach**: No authentication on websockify port - -### Authorization - -- **Privilege Required**: `Node.Console` -- **Project Isolation**: Users can only access consoles in projects they have access to -- **Node Access Control**: Checked via `dep_node` dependency - -### Transport Security - -- **WebSocket (ws://)**: Unencrypted, for development -- **WebSocket Secure (wss://)**: Encrypted, recommended for production -- **SSL/TLS**: Configured via GNS3 server SSL settings - -### Console Protocol Security - -- **VNC**: Uses RFB protocol, supports password authentication -- **SPICE**: Supports ticket-based authentication (`disable-ticketing` in current implementation) -- **Recommendation**: Enable VNC/SPICE passwords for production - ---- - -## References - -### Files Modified - -| File | Description | -|------|-------------| -| `gns3server/compute/base_node.py` | Add `start_vnc_websocket_console()` and `start_spice_websocket_console()` methods | -| `gns3server/api/routes/compute/qemu_nodes.py` | Add `/{node_id}/console/vnc` and `/{node_id}/console/spice` endpoints | -| `gns3server/api/routes/compute/docker_nodes.py` | Add `/{node_id}/console/vnc` and `/{node_id}/console/spice` endpoints | -| `gns3server/api/routes/controller/nodes.py` | Add controller layer WebSocket proxy endpoints | - -### Commits - -- `ba92b405` - feat: add VNC WebSocket console support for Docker and QEMU nodes -- Current implementation extends this with SPICE support - -### Related Documentation - -- [Telnet Console Documentation](../bugs/telnet-server-connection-race-condition.md) -- [GNS3 API Documentation](./openapi.json) -- [noVNC Documentation](https://github.com/novnc/noVNC) -- [SPICE Protocol](https://www.spice-space.org/documentation.html) - -### External Libraries - -- **noVNC**: HTML5 VNC Client (https://github.com/novnc/noVNC) -- **spice-html5**: HTML5 SPICE Client (https://github.com/spice-html5/spice-html5) - ---- - -## Migration Guide - -### From Standalone websockify - -**Old Approach** (not recommended): -```python -# Start websockify for each node -websockify 10001 localhost:5900 # Process 1 -websockify 10002 localhost:5901 # Process 2 -websockify 10003 localhost:5902 # Process 3 -``` - -**New Approach** (recommended): -```python -# No websockify processes needed -# Just use GNS3 WebSocket API -WS /v3/controller/projects/{id}/qemu/nodes/{id}/console/vnc -WS /v3/controller/projects/{id}/qemu/nodes/{id}/console/spice -``` - -**Client Migration**: -```javascript -// Old: Connect directly to websockify -const url = `ws://server:${websockify_port}/`; - -// New: Connect through GNS3 API -const url = `wss://server:3080/v3/controller/projects/${projectId}/qemu/nodes/${nodeId}/console/vnc?ticket=${token}`; -``` - ---- - -## Changelog - -### Version 1.0 (2026-03-16) - -**Added**: -- VNC WebSocket console support for QEMU and Docker nodes -- SPICE WebSocket console support for QEMU nodes -- Controller layer WebSocket proxy endpoints -- Compute layer WebSocket endpoints with authentication -- Bidirectional binary data forwarding in BaseNode - -**Changed**: -- N/A (new feature) - -**Deprecated**: -- Standalone websockify approach (still functional but not recommended) - -**Fixed**: -- N/A (new feature) diff --git a/gns3server/api/routes/compute/dependencies/authentication.py b/gns3server/api/routes/compute/dependencies/authentication.py index 5e725b3d8..a5ad44b10 100644 --- a/gns3server/api/routes/compute/dependencies/authentication.py +++ b/gns3server/api/routes/compute/dependencies/authentication.py @@ -61,7 +61,7 @@ def compute_authentication(credentials: Optional[HTTPBasicCredentials] = Depends async def ws_compute_authentication(websocket: WebSocket) -> Union[None, WebSocket]: """ """ - + server_settings = Config.instance().settings.Server if not server_settings.enable_http_auth: diff --git a/gns3server/api/routes/compute/docker_nodes.py b/gns3server/api/routes/compute/docker_nodes.py index 4b5d7cb9c..1d9e98530 100644 --- a/gns3server/api/routes/compute/docker_nodes.py +++ b/gns3server/api/routes/compute/docker_nodes.py @@ -399,21 +399,6 @@ async def vnc_console_ws( await node.start_vnc_websocket_console(websocket) -@router.websocket( - "/{node_id}/console/spice" -) -async def spice_console_ws( - websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), - node: DockerVM = Depends(dep_node) -) -> None: - """ - SPICE Console WebSocket. - """ - - if websocket: - await node.start_spice_websocket_console(websocket) - - @router.post( "/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT, diff --git a/gns3server/api/routes/compute/qemu_nodes.py b/gns3server/api/routes/compute/qemu_nodes.py index 367b2fbe5..623ae8dae 100644 --- a/gns3server/api/routes/compute/qemu_nodes.py +++ b/gns3server/api/routes/compute/qemu_nodes.py @@ -430,21 +430,6 @@ async def vnc_console_ws( await node.start_vnc_websocket_console(websocket) -@router.websocket( - "/{node_id}/console/spice" -) -async def spice_console_ws( - websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), - node: QemuVM = Depends(dep_node) -) -> None: - """ - SPICE Console WebSocket. - """ - - if websocket: - await node.start_spice_websocket_console(websocket) - - @router.post( "/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT, diff --git a/gns3server/api/routes/controller/nodes.py b/gns3server/api/routes/controller/nodes.py index 72bf38b3d..aa4eddc0a 100644 --- a/gns3server/api/routes/controller/nodes.py +++ b/gns3server/api/routes/controller/nodes.py @@ -721,81 +721,6 @@ async def vnc_console( except aiohttp.ClientError as e: log.error(f"Client error received when forwarding to compute VNC console WebSocket: {e}") -@router.websocket("/{node_id}/console/spice") -async def spice_console( - websocket: WebSocket, - current_user: schemas.User = Depends(has_privilege_on_websocket("Node.Console")), - node: Node = Depends(dep_node) -) -> None: - """ - SPICE WebSocket console. - - Required privilege: Node.Console - """ - - if current_user is None: - return - - compute = node.compute - log.info( - f"New client {websocket.client.host}:{websocket.client.port} has connected to controller SPICE console WebSocket" - ) - - compute_host = compute.host - try: - # handle IPv6 address - ip = ipaddress.ip_address(compute_host) - if isinstance(ip, ipaddress.IPv6Address): - compute_host = '[' + compute_host + ']' - except ValueError: - pass - - spice_console_compute_url = ( - f"{websocket.url.scheme}://{compute_host}:{compute.port}/v3/compute/projects/" - f"{node.project.id}/{node.node_type}/nodes/{node.id}/console/spice" - ) - - async def spice_receive(spice_console_compute): - """ - Receive binary WebSocket data from client and forward to compute SPICE console WebSocket. - """ - - try: - while True: - data = await websocket.receive_bytes() - if data: - await spice_console_compute.send_bytes(data) - except WebSocketDisconnect: - await spice_console_compute.close() - log.info( - f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller" - f" SPICE console WebSocket" - ) - - try: - # receive binary data from compute SPICE console WebSocket and forward to client - log.info(f"Forwarding SPICE console WebSocket to '{spice_console_compute_url}'") - server_config = Config.instance().settings.Server - user = server_config.compute_username - password = server_config.compute_password - if not user: - raise ControllerForbiddenError("Compute username is not set") - user = user.strip() - if user and password: - auth = aiohttp.BasicAuth(user, password.get_secret_value(), "utf-8") - else: - auth = aiohttp.BasicAuth(user, "") - ssl_context = Controller.instance().ssl_context() - async with HTTPClient.get_client().ws_connect(spice_console_compute_url, auth=auth, ssl_context=ssl_context) as ws: - asyncio.ensure_future(spice_receive(ws)) - async for msg in ws: - if msg.type == aiohttp.WSMsgType.BINARY: - await websocket.send_bytes(msg.data) - elif msg.type == aiohttp.WSMsgType.ERROR: - break - except aiohttp.ClientError as e: - log.error(f"Client error received when forwarding to compute SPICE console WebSocket: {e}") - @router.post( "/console/reset", status_code=status.HTTP_204_NO_CONTENT, diff --git a/gns3server/compute/base_node.py b/gns3server/compute/base_node.py index 0d684e88a..e09111de4 100644 --- a/gns3server/compute/base_node.py +++ b/gns3server/compute/base_node.py @@ -611,76 +611,6 @@ class BaseNode: vnc_writer.close() await vnc_writer.wait_closed() - async def start_spice_websocket_console(self, websocket): - """ - Connect to SPICE console using WebSocket. - - :param websocket: FastAPI WebSocket object - """ - - log.info( - f"New client {websocket.client.host}:{websocket.client.port} has connected to compute " - f"SPICE console WebSocket" - ) - - if self.status != "started": - await websocket.close(code=1000) - raise NodeError(f"Node {self.name} is not started") - if self._console_type not in ("spice", "spice+agent"): - await websocket.close(code=1000) - raise NodeError(f"Node {self.name} console type is not SPICE") - - try: - spice_reader, spice_writer = await asyncio.open_connection( - self._manager.port_manager.console_host, - self.console # SPICE port - ) - log.info(f"Connected to SPICE server {self._manager.port_manager.console_host}:{self.console}") - except ConnectionError as e: - await websocket.close(code=1000) - raise NodeError(f"Cannot connect to node {self.name} SPICE server: {e}") - - async def ws_forward(spice_writer): - # Browser → SPICE: Forward binary WebSocket data to SPICE server - try: - while True: - data = await websocket.receive_bytes() - if data: - spice_writer.write(data) - await spice_writer.drain() - except WebSocketDisconnect: - log.info( - f"Client {websocket.client.host}:{websocket.client.port} has disconnected from compute " - f"SPICE console WebSocket" - ) - - async def spice_forward(spice_reader): - # SPICE → Browser: Forward SPICE data to WebSocket - try: - while not spice_reader.at_eof(): - data = await spice_reader.read(65536) # Larger buffer for SPICE data - if data: - await websocket.send_bytes(data) - except Exception as e: - log.warning(f"Exception while forwarding SPICE data to WebSocket: {e}") - - # Keep forwarding WebSocket data in both directions - if sys.version_info >= (3, 11, 0): - # Starting with Python 3.11, passing coroutine objects to wait() directly is forbidden. - aws = [asyncio.create_task(ws_forward(spice_writer)), asyncio.create_task(spice_forward(spice_reader))] - else: - aws = [ws_forward(spice_writer), spice_forward(spice_reader)] - - done, pending = await asyncio.wait(aws, return_when=asyncio.FIRST_COMPLETED) - for task in done: - if task.exception(): - log.warning(f"Exception while forwarding WebSocket data to SPICE server: {task.exception()}") - for task in pending: - task.cancel() - - spice_writer.close() - await spice_writer.wait_closed() - @property def aux(self): """