import threading, secrets, socket, struct, click, json, sys 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, server_token:str|None) -> None: from browser_cli.client import _resolve_socket, BrowserNotConnected from browser_cli.platform import is_windows try: header = _recv_exact(client_sock, 4) msg_len = struct.unpack(" None: err = json.dumps({"id": msg_id, "success": False, "error": msg}).encode() try: client_sock.sendall(struct.pack(" None: with client_sock: _proxy_request(client_sock, addr, profile, server_token) @click.command("serve") @click.option("--host", default="127.0.0.1", show_default=True, help="Address to bind.") @click.option("--port", default=8765, show_default=True, type=int, help="TCP port to listen on.") @click.option("--token", default=None, metavar="TOKEN", help="Auth token (auto-generated if omitted).") @click.option("--no-auth", is_flag=True, default=False, help="Disable token authentication.") @click.pass_context def cmd_serve(ctx, host, port, token, no_auth): """Expose this browser over TCP so remote hosts can control it.""" profile = ctx.obj.get("browser") if ctx.obj else None if host in ("0.0.0.0", "::"): console.print("[yellow]Warning:[/yellow] Binding to all interfaces — anyone who can reach this port controls your browser.") if no_auth: server_token = None else: server_token = token or secrets.token_urlsafe(32) server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: server.bind((host, port)) except OSError as e: console.print(f"[red]Cannot bind to {host}:{port}:[/red] {e}") sys.exit(1) server.listen(16) browser_hint = f" (browser: {profile})" if profile else "" console.print(f"[green]Serving browser{browser_hint} →[/green] [cyan]{host}:{port}[/cyan]") if server_token: console.print(f" Token: [bold yellow]{server_token}[/bold yellow]") console.print(f" CLI: [dim]browser-cli --remote {host}:{port} --token {server_token} tabs list[/dim]") console.print(f" Python: [dim]BrowserCLI(remote=\"{host}:{port}\", token=\"{server_token}\").tabs_list()[/dim]") else: console.print(f" CLI: [dim]browser-cli --remote {host}:{port} tabs list[/dim]") console.print(f" Python: [dim]BrowserCLI(remote=\"{host}:{port}\").tabs_list()[/dim]") console.print("[yellow] Auth disabled (--no-auth)[/yellow]") console.print("Ctrl-C to stop.\n") try: while True: conn, addr = server.accept() threading.Thread(target=_handle_client, args=(conn, addr, profile, server_token), daemon=True).start() except KeyboardInterrupt: console.print("[yellow]Stopped.[/yellow]") finally: server.close()