HEX
Server: LiteSpeed
System: Linux php-prod-3.spaceapp.ru 5.15.0-151-generic #161-Ubuntu SMP Tue Jul 22 14:25:40 UTC 2025 x86_64
User: xnsbl7462 (1008)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: //usr/local/CyberCP/fastapi_ssh_server.py
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"

# 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(
            "localhost",
            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()