mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-02-27 08:50:46 +01:00
- Add get_ssh_port() to read Port from /etc/ssh/sshd_config - Use 127.0.0.1 and port=SSH_PORT in asyncssh.connect() - Web Terminal now works when SSH runs on non-22 port
176 lines
6.2 KiB
Python
176 lines
6.2 KiB
Python
import asyncio
|
|
import asyncssh
|
|
import tempfile
|
|
import os
|
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Query
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
import paramiko # For key generation and manipulation
|
|
import io
|
|
import pwd
|
|
from jose import jwt, JWTError
|
|
import logging
|
|
|
|
app = FastAPI()
|
|
# JWT_SECRET = "YOUR_SECRET_KEY"
|
|
JWT_SECRET = "DAsjK2gl50PE09d1N3uZPTQ6JdwwfiuhlyWKMVbUEpc"
|
|
JWT_ALGORITHM = "HS256"
|
|
|
|
# Allow CORS for local dev/testing
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
SSH_USER = "your_website_user" # Replace with a real user for testing
|
|
AUTHORIZED_KEYS_PATH = f"/home/{SSH_USER}/.ssh/authorized_keys"
|
|
|
|
# Read the actual SSH port from sshd_config (fixes WebTerminal when SSH uses custom port)
|
|
def get_ssh_port() -> int:
|
|
try:
|
|
with open("/etc/ssh/sshd_config", "r") as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line or line.startswith('#'):
|
|
continue
|
|
line = line.split('#')[0].strip()
|
|
parts = line.split()
|
|
if len(parts) >= 2 and parts[0].lower() == 'port':
|
|
port = int(parts[1])
|
|
logging.info(f"[get_ssh_port] SSH port detected: {port}")
|
|
return port
|
|
except Exception as e:
|
|
logging.warning(f"[get_ssh_port] Could not read sshd_config: {e}")
|
|
logging.warning("[get_ssh_port] Falling back to default port 22")
|
|
return 22
|
|
|
|
SSH_PORT = get_ssh_port()
|
|
|
|
# Helper to generate a keypair
|
|
def generate_ssh_keypair():
|
|
key = paramiko.RSAKey.generate(2048)
|
|
private_io = io.StringIO()
|
|
key.write_private_key(private_io)
|
|
private_key = private_io.getvalue()
|
|
public_key = f"{key.get_name()} {key.get_base64()}"
|
|
return private_key, public_key
|
|
|
|
# Add public key to authorized_keys with a unique comment
|
|
def add_key_to_authorized_keys(public_key, comment):
|
|
entry = f'from="127.0.0.1,::1" {public_key} {comment}\n'
|
|
with open(AUTHORIZED_KEYS_PATH, "a") as f:
|
|
f.write(entry)
|
|
|
|
# Remove public key from authorized_keys by comment
|
|
def remove_key_from_authorized_keys(comment):
|
|
with open(AUTHORIZED_KEYS_PATH, "r") as f:
|
|
lines = f.readlines()
|
|
with open(AUTHORIZED_KEYS_PATH, "w") as f:
|
|
for line in lines:
|
|
if comment not in line:
|
|
f.write(line)
|
|
|
|
@app.websocket("/ws")
|
|
async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), ssh_user: str = Query(None)):
|
|
# Re-enable JWT validation
|
|
try:
|
|
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
|
|
user = payload.get("ssh_user")
|
|
if not user:
|
|
await websocket.close()
|
|
return
|
|
except JWTError:
|
|
await websocket.close()
|
|
return
|
|
home_dir = pwd.getpwnam(user).pw_dir
|
|
ssh_dir = os.path.join(home_dir, ".ssh")
|
|
authorized_keys_path = os.path.join(ssh_dir, "authorized_keys")
|
|
|
|
os.makedirs(ssh_dir, exist_ok=True)
|
|
if not os.path.exists(authorized_keys_path):
|
|
with open(authorized_keys_path, "w"): pass
|
|
os.chown(ssh_dir, pwd.getpwnam(user).pw_uid, pwd.getpwnam(user).pw_gid)
|
|
os.chmod(ssh_dir, 0o700)
|
|
os.chown(authorized_keys_path, pwd.getpwnam(user).pw_uid, pwd.getpwnam(user).pw_gid)
|
|
os.chmod(authorized_keys_path, 0o600)
|
|
|
|
private_key, public_key = generate_ssh_keypair()
|
|
comment = f"webterm-{os.urandom(8).hex()}"
|
|
entry = f'from="127.0.0.1,::1" {public_key} {comment}\n'
|
|
with open(authorized_keys_path, "a") as f:
|
|
f.write(entry)
|
|
|
|
with tempfile.NamedTemporaryFile(delete=False) as keyfile:
|
|
keyfile.write(private_key.encode())
|
|
keyfile_path = keyfile.name
|
|
|
|
await websocket.accept()
|
|
conn = None
|
|
process = None
|
|
try:
|
|
conn = await asyncssh.connect(
|
|
"127.0.0.1",
|
|
port=SSH_PORT,
|
|
username=user,
|
|
client_keys=[keyfile_path],
|
|
known_hosts=None
|
|
)
|
|
process = await conn.create_process(term_type="xterm")
|
|
|
|
async def ws_to_ssh():
|
|
try:
|
|
while True:
|
|
data = await websocket.receive_bytes()
|
|
# Decode bytes to str before writing to SSH stdin
|
|
process.stdin.write(data.decode('utf-8', errors='replace'))
|
|
except WebSocketDisconnect:
|
|
process.stdin.close()
|
|
|
|
async def ssh_to_ws():
|
|
try:
|
|
while not process.stdout.at_eof():
|
|
data = await process.stdout.read(1024)
|
|
if data:
|
|
# Defensive type check and logging
|
|
logging.debug(f"[ssh_to_ws] Sending to WS: type={type(data)}, sample={data[:40] if isinstance(data, bytes) else data}")
|
|
if isinstance(data, bytes):
|
|
await websocket.send_bytes(data)
|
|
elif isinstance(data, str):
|
|
await websocket.send_text(data)
|
|
else:
|
|
await websocket.send_text(str(data))
|
|
except Exception as ex:
|
|
logging.exception(f"[ssh_to_ws] Exception: {ex}")
|
|
pass
|
|
|
|
await asyncio.gather(ws_to_ssh(), ssh_to_ws())
|
|
except Exception as e:
|
|
try:
|
|
# Always send error as text (string)
|
|
msg = f"Connection error: {e}"
|
|
logging.exception(f"[websocket_endpoint] Exception: {e}")
|
|
if isinstance(msg, bytes):
|
|
msg = msg.decode('utf-8', errors='replace')
|
|
await websocket.send_text(str(msg))
|
|
except Exception as ex:
|
|
logging.exception(f"[websocket_endpoint] Error sending error message: {ex}")
|
|
pass
|
|
try:
|
|
await websocket.close()
|
|
except Exception:
|
|
pass
|
|
finally:
|
|
# Remove key from authorized_keys and delete temp private key
|
|
with open(authorized_keys_path, "r") as f:
|
|
lines = f.readlines()
|
|
with open(authorized_keys_path, "w") as f:
|
|
for line in lines:
|
|
if comment not in line:
|
|
f.write(line)
|
|
os.remove(keyfile_path)
|
|
if process:
|
|
process.close()
|
|
if conn:
|
|
conn.close() |