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()  |