Terminal: SSH to NAS host instead of container shell
- Rewrite terminal.py to use asyncssh instead of pty/fork - Add SSH key pair for dashboard container auth - Mount SSH key and add SSH config vars in docker-compose - Install openssh-client in Dockerfile - Add asyncssh to requirements.txt
This commit is contained in:
@@ -14,3 +14,8 @@ ADMIN_USER = os.environ.get("ADMIN_USER", "admin")
|
||||
# Default hash for "admin" using pbkdf2_sha256 scheme to avoid bcrypt compat issues
|
||||
ADMIN_PASSWORD_HASH = os.environ.get("ADMIN_PASSWORD_HASH", "$pbkdf2-sha256$29000$25tTKoUQIgQghLC2ds4ZYw$H/fCb3H5wmOkwj6l6KrVlmns0TWfzKPOrG3sgVDR.eg")
|
||||
TOTP_SECRET = os.environ.get("TOTP_SECRET", "JBSWY3DPEHPK3PXP") # Base32 encoded secretans 2FA disabled by default
|
||||
|
||||
# SSH terminal
|
||||
SSH_HOST = os.environ.get("SSH_HOST", "host.docker.internal")
|
||||
SSH_USER = os.environ.get("SSH_USER", "zjgump")
|
||||
SSH_KEY_PATH = os.environ.get("SSH_KEY_PATH", "/app/ssh/id_ed25519")
|
||||
|
||||
@@ -7,3 +7,4 @@ psutil==6.1.0
|
||||
pyjwt==2.8.0
|
||||
passlib==1.7.4
|
||||
pyotp==2.9.0
|
||||
asyncssh==2.17.0
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import asyncio
|
||||
import os
|
||||
import pty
|
||||
import signal
|
||||
import struct
|
||||
import fcntl
|
||||
import termios
|
||||
import select
|
||||
from fastapi import WebSocket, WebSocketDisconnect, Query
|
||||
import asyncssh
|
||||
import jwt
|
||||
import config
|
||||
|
||||
|
||||
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:
|
||||
@@ -23,52 +17,47 @@ async def ws_endpoint(websocket: WebSocket, token: str = Query("")):
|
||||
return
|
||||
|
||||
await websocket.accept()
|
||||
master_fd, slave_fd = pty.openpty()
|
||||
pid = os.fork()
|
||||
|
||||
if pid == 0:
|
||||
os.close(master_fd)
|
||||
os.setsid()
|
||||
os.dup2(slave_fd, 0)
|
||||
os.dup2(slave_fd, 1)
|
||||
os.dup2(slave_fd, 2)
|
||||
os.close(slave_fd)
|
||||
os.execvp("/bin/bash", ["/bin/bash"])
|
||||
|
||||
os.close(slave_fd)
|
||||
|
||||
async def read_pty():
|
||||
try:
|
||||
while True:
|
||||
await asyncio.sleep(0.01)
|
||||
r, _, _ = select.select([master_fd], [], [], 0)
|
||||
if r:
|
||||
data = os.read(master_fd, 4096)
|
||||
if not data:
|
||||
break
|
||||
await websocket.send_bytes(data)
|
||||
except (OSError, WebSocketDisconnect):
|
||||
pass
|
||||
|
||||
read_task = asyncio.create_task(read_pty())
|
||||
|
||||
try:
|
||||
while True:
|
||||
msg = await websocket.receive()
|
||||
if "bytes" in msg and msg["bytes"]:
|
||||
data = msg["bytes"]
|
||||
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]
|
||||
fcntl.ioctl(master_fd, termios.TIOCSWINSZ, struct.pack("HHHH", rows, cols, 0, 0))
|
||||
else:
|
||||
os.write(master_fd, data)
|
||||
elif "text" in msg and msg["text"]:
|
||||
os.write(master_fd, msg["text"].encode())
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
conn = await asyncssh.connect(
|
||||
config.SSH_HOST, username=config.SSH_USER,
|
||||
client_keys=[config.SSH_KEY_PATH], known_hosts=None,
|
||||
)
|
||||
except Exception:
|
||||
await websocket.close(code=1011, reason="SSH connection failed")
|
||||
return
|
||||
|
||||
try:
|
||||
chan, session = await conn.create_session(
|
||||
asyncssh.SSHClientProcess, term_type="xterm-256color",
|
||||
term_size=(80, 24),
|
||||
)
|
||||
|
||||
async def read_ssh():
|
||||
try:
|
||||
async for data in chan.stdout:
|
||||
await websocket.send_bytes(data.encode() if isinstance(data, str) else data)
|
||||
except (asyncssh.Error, WebSocketDisconnect):
|
||||
pass
|
||||
|
||||
read_task = asyncio.create_task(read_ssh())
|
||||
|
||||
try:
|
||||
while True:
|
||||
msg = await websocket.receive()
|
||||
if "bytes" in msg and msg["bytes"]:
|
||||
data = msg["bytes"]
|
||||
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]
|
||||
chan.change_terminal_size(cols, rows)
|
||||
else:
|
||||
chan.write(data.decode("utf-8", errors="replace"))
|
||||
elif "text" in msg and msg["text"]:
|
||||
chan.write(msg["text"])
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
finally:
|
||||
read_task.cancel()
|
||||
finally:
|
||||
read_task.cancel()
|
||||
os.close(master_fd)
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
os.waitpid(pid, 0)
|
||||
conn.close()
|
||||
|
||||
Reference in New Issue
Block a user