From 69bd41b5a139c72abeb94850cee944cf65a82dc5 Mon Sep 17 00:00:00 2001 From: "Gan, Jimmy" Date: Thu, 19 Feb 2026 17:44:55 +0800 Subject: [PATCH] Fix terminal: pass JWT via query param, remove Docker-incompatible IP check --- dashboard/backend/routers/terminal.py | 32 +++++++++---------- dashboard/frontend/src/routes/Terminal.svelte | 3 +- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/dashboard/backend/routers/terminal.py b/dashboard/backend/routers/terminal.py index c858198..8d01e32 100644 --- a/dashboard/backend/routers/terminal.py +++ b/dashboard/backend/routers/terminal.py @@ -5,17 +5,23 @@ import signal import struct import fcntl import termios -from fastapi import WebSocket, WebSocketDisconnect, Depends -import auth +import select +from fastapi import WebSocket, WebSocketDisconnect, Query +import jwt +import config -async def ws_endpoint(websocket: WebSocket, user: str = Depends(auth.get_current_user_ws)): - # Block non-Tailscale IPs (Defense in depth) - client_ip = websocket.client.host if websocket.client else "" - if not client_ip.startswith("100."): - await websocket.close(code=1008, reason="Tailscale only") + +async def ws_endpoint(websocket: WebSocket, token: str = Query("")): + # Verify auth token + try: + payload = jwt.decode(token, config.SECRET_KEY, algorithms=[config.ALGORITHM]) + if payload.get("sub") != config.ADMIN_USER: + await websocket.close(code=1008, reason="Unauthorized") + return + except Exception: + await websocket.close(code=1008, reason="Unauthorized") return - await websocket.accept() master_fd, slave_fd = pty.openpty() pid = os.fork() @@ -30,13 +36,13 @@ async def ws_endpoint(websocket: WebSocket, user: str = Depends(auth.get_current os.execvp("/bin/bash", ["/bin/bash"]) os.close(slave_fd) - loop = asyncio.get_event_loop() async def read_pty(): try: while True: await asyncio.sleep(0.01) - if select_readable(master_fd): + r, _, _ = select.select([master_fd], [], [], 0) + if r: data = os.read(master_fd, 4096) if not data: break @@ -44,11 +50,6 @@ async def ws_endpoint(websocket: WebSocket, user: str = Depends(auth.get_current except (OSError, WebSocketDisconnect): pass - def select_readable(fd): - import select - r, _, _ = select.select([fd], [], [], 0) - return bool(r) - read_task = asyncio.create_task(read_pty()) try: @@ -56,7 +57,6 @@ async def ws_endpoint(websocket: WebSocket, user: str = Depends(auth.get_current msg = await websocket.receive() if "bytes" in msg and msg["bytes"]: data = msg["bytes"] - # Handle resize: first byte 0x01 means resize message if data[0:1] == b"\x01" and len(data) == 5: cols = struct.unpack("!H", data[1:3])[0] rows = struct.unpack("!H", data[3:5])[0] diff --git a/dashboard/frontend/src/routes/Terminal.svelte b/dashboard/frontend/src/routes/Terminal.svelte index 20b11d0..e3dbff1 100644 --- a/dashboard/frontend/src/routes/Terminal.svelte +++ b/dashboard/frontend/src/routes/Terminal.svelte @@ -38,7 +38,8 @@ fit.fit(); const proto = location.protocol === "https:" ? "wss:" : "ws:"; - const ws = new WebSocket(`${proto}//${location.host}/ws/terminal`); + const token = localStorage.getItem("token") || ""; + const ws = new WebSocket(`${proto}//${location.host}/ws/terminal?token=${encodeURIComponent(token)}`); ws.binaryType = "arraybuffer"; ws.onopen = () => {