import re, threading, secrets, socket, struct, click, json, sys, os from pathlib import Path from browser_cli.version_manager import PROTOCOL_MIN_CLIENT, parse_version, get_installed_version from browser_cli.compat import adapt_request, adapt_response _UA_PATTERN = re.compile(r"^browser-cli/\d") _CONN_LIMIT = threading.BoundedSemaphore(64) _MAX_MSG_BYTES = 32 * 1024 * 1024 from rich.console import Console from datetime import datetime console = Console() def _recv_exact(sock:socket.socket, n:int) -> bytes: buf = b"" while len(buf) < n: chunk = sock.recv(n - len(buf)) if not chunk: raise ConnectionError("Connection closed") buf += chunk return buf def _log(addr:tuple, command:str, profile:str|None, status:str, error:str|None=None) -> None: ts = datetime.now().strftime("%H:%M:%S") addr_str = f"{addr[0]}:{addr[1]}" profile_str = f"[dim]{profile}[/dim] " if profile else "" if error: console.print(f"[dim]{ts}[/dim] {addr_str} {profile_str}[cyan]{command}[/cyan] [red]{status}[/red] {error}") else: console.print(f"[dim]{ts}[/dim] {addr_str} {profile_str}[cyan]{command}[/cyan] [green]{status}[/green]") def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, auth_keys:list[str]|None, auth_keys_path:"Path|None", nonce:str) -> None: from browser_cli.client import _resolve_socket, BrowserNotConnected from browser_cli.platform import is_windows def _send_error(msg_id, msg:str) -> None: err = json.dumps({"id": msg_id, "success": False, "error": msg}).encode() try: client_sock.sendall(struct.pack(" _MAX_MSG_BYTES: _send_error(None, f"message too large ({msg_len} bytes)") return payload = _recv_exact(client_sock, msg_len) except (ConnectionError, OSError): return try: msg = json.loads(payload) except (json.JSONDecodeError, ValueError): _send_error(None, "invalid JSON") _log(addr, "?", None, "ERROR", "invalid JSON") return msg_id = msg.get("id") command = msg.get("command", "?") # ── user-agent + version check ──────────────────────────────────────────── ua = msg.get("user_agent") or "" if not _UA_PATTERN.match(ua): _send_error(msg_id, "forbidden: client required") _log(addr, command, None, "DENIED", f"bad user-agent: {ua!r}") return client_ver = "0" try: client_ver = ua.split("/", 1)[1] if parse_version(client_ver) < parse_version(PROTOCOL_MIN_CLIENT): _send_error(msg_id, f"client version {client_ver} is too old; please upgrade to >= {PROTOCOL_MIN_CLIENT}") _log(addr, command, None, "DENIED", f"client {client_ver} < min {PROTOCOL_MIN_CLIENT}") return except (IndexError, ValueError): pass # ── auth ────────────────────────────────────────────────────────────────── if auth_keys is not None: pub = msg.get("pubkey") or "" sig = msg.get("sig") or "" if not pub or not sig: _send_error(msg_id, "unauthorized: pubkey auth required — run 'browser-cli auth keygen' on the client") _log(addr, command, None, "DENIED", "missing pubkey/sig") return if pub not in auth_keys: _send_error(msg_id, "unauthorized: untrusted public key") _log(addr, command, None, "DENIED", "untrusted key") return from browser_cli.auth import verify if not verify(pub, bytes.fromhex(nonce), msg, sig): _send_error(msg_id, "unauthorized: invalid signature") _log(addr, command, None, "DENIED", "bad signature") return if command == "browser-cli.targets": from browser_cli.client import active_browser_targets targets = [ {"profile": target.profile, "displayName": target.display_name} for target in active_browser_targets(include_remotes=False) ] data = json.dumps({"id": msg_id, "success": True, "data": targets}).encode() client_sock.sendall(struct.pack(" None: if not _CONN_LIMIT.acquire(blocking=False): client_sock.close() return client_sock.settimeout(30) try: with client_sock: # reload on every connection so auth trust --remote takes effect immediately if auth_keys_path is not None: from browser_cli.auth import load_authorized_keys auth_keys: list[str] | None = load_authorized_keys(auth_keys_path) else: auth_keys = None nonce = secrets.token_hex(32) challenge = json.dumps({ "type": "challenge", "nonce": nonce, "server_version": get_installed_version(), "min_client_version": PROTOCOL_MIN_CLIENT, }).encode() try: client_sock.sendall(struct.pack("