mirror of
				https://github.com/usmannasir/cyberpanel.git
				synced 2025-10-31 02:15:55 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			154 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			154 lines
		
	
	
		
			5.4 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"
 | |
| 
 | |
| # 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()  |