import asyncio import websockets import json # Guacamole protocol message formatting functions def encode_guac_message(opcode, *params): parts = [f"{len(p)}.{p}" for p in params] return f"{opcode},{','.join(parts)};" def decode_guac_message(message): parts = message.strip(';').split(',') opcode = parts[0] params = [] current_pos = 1 while current_pos < len(parts): length_str = parts[current_pos].split('.')[0] length = int(length_str) # Note: A real implementation would handle string content more carefully param = parts[current_pos][len(length_str) + 1:] params.append(param) current_pos += 1 return opcode, params # Simplified VM and User classes for state management class VM: def __init__(self, vm_id, display_name): self.vm_id = vm_id self.display_name = display_name self.users = {} # {username: WebSocket connection} self.turn_queue = [] self.chat_history = [] self.screen_size = (800, 600) class User: def __init__(self, username, rank=0): self.username = username self.rank = rank # Server state vms = { "vm-001": VM("vm-001", "Linux Desktop"), "vm-002": VM("vm-002", "Windows 98") } # Main server logic async def handle_client(websocket, path): # Enforce guacamole subprotocol if "guacamole" not in websocket.subprotocol: await websocket.close() print("Connection rejected: Incorrect subprotocol.") return # Handshake phase print("New connection established. Starting handshake...") user = User(username=None) vm_id = None # Send NOPs periodically (this is a simplified example) async def nop_keepalive(): while True: await asyncio.sleep(10) # 10-second interval if websocket.open: await websocket.send(encode_guac_message("nop")) else: break asyncio.ensure_future(nop_keepalive()) try: async for message in websocket: opcode, params = decode_guac_message(message) print(f"Received opcode: {opcode} with params: {params}") # Handshake opcodes if opcode == "list": list_message = "list," for v in vms.values(): # For a real server, thumbnail would be base64-encoded image data list_message += f"{len(v.vm_id)}.{v.vm_id}," list_message += f"{len(v.display_name)}.{v.display_name}," list_message += f"{4}.THMB;" # Mock thumbnail await websocket.send(list_message) elif opcode == "rename": requested_name = params[0] if params else "guest" user.username = requested_name # Simplified, server would check for availability # Send back a Client Renamed event response = encode_guac_message("rename", "0", "0", user.username) await websocket.send(response) elif opcode == "connect": requested_vm_id = params[0] if requested_vm_id in vms: vm = vms[requested_vm_id] vm_id = requested_vm_id vm.users[user.username] = websocket print(f"User {user.username} connected to VM {vm_id}") # Connection success message await websocket.send(encode_guac_message("connect", "1")) # Announce new user to others in the VM for other_user_ws in vm.users.values(): if other_user_ws != websocket: await other_user_ws.send(encode_guac_message("adduser", user.username, "0")) # Announce VM screen size width, height = vm.screen_size await websocket.send(encode_guac_message("size", "0", str(width), str(height))) # Send mock framebuffer update (png) # A real implementation would send a base64 encoded image await websocket.send(encode_guac_message("png", "0", "0", "0", "0", "FAKE_BASE64_IMAGE_DATA")) else: await websocket.send(encode_guac_message("connect", "0")) # VM interaction opcodes (only after connection) elif vm_id: vm = vms[vm_id] if opcode == "chat": message = params[0] chat_msg = f"{user.username}: {message}" vm.chat_history.append(chat_msg) for other_user_ws in vm.users.values(): await other_user_ws.send(encode_guac_message("chat", user.username, message)) # Add more opcode handlers here (e.g., mouse, key, turn) # Client has sent NOP in response to server elif opcode == "nop": # Do nothing, NOP is a keepalive pass except websockets.exceptions.ConnectionClosed as e: print(f"Connection closed with code {e.code}: {e.reason}") finally: # Cleanup on disconnect if user.username and vm_id: vm = vms[vm_id] if user.username in vm.users: del vm.users[user.username] print(f"User {user.username} disconnected from VM {vm_id}") # Announce removed user for other_user_ws in vm.users.values(): await other_user_ws.send(encode_guac_message("remuser", "1", user.username)) # Main function to start the server async def main(): server = await websockets.serve( handle_client, "0.0.0.0", 8765, subprotocols=["guacamole"] ) print("CollabVM server started on ws://localhost:8765") await server.wait_closed() if __name__ == "__main__": asyncio.run(main())