feat(serve): add remote browser control over TCP with token auth
Exposes a local browser over a TCP socket so remote machines can control it using the same CLI and Python API. Token auth (auto-generated via secrets.token_urlsafe) is on by default; --no-auth disables it. Profile routing via _route message field lets clients target specific browser instances on the remote host. BROWSER_CLI_PROFILE is forwarded automatically so --browser flag works transparently over remote. - browser-cli serve [--host] [--port] [--token] [--no-auth] - browser-cli --remote HOST:PORT --token TOKEN <command> - BrowserCLI(remote="host:port", token="...").tabs_list()
This commit is contained in:
+25
-3
@@ -98,28 +98,50 @@ def _resolve_socket(profile: str | None = None) -> str:
|
||||
)
|
||||
|
||||
|
||||
def send_command(command: str, args: dict | None = None, profile: str | None = None) -> Any:
|
||||
def send_command(command: str, args: dict | None = None, profile: str | None = None, remote: str | None = None, token: str | None = None) -> Any:
|
||||
"""Send a command to the browser and return the response data."""
|
||||
sock_path = _resolve_socket(profile)
|
||||
remote_endpoint = remote or os.environ.get("BROWSER_CLI_REMOTE")
|
||||
resolved_token = token or os.environ.get("BROWSER_CLI_TOKEN")
|
||||
msg = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"command": command,
|
||||
"args": args or {},
|
||||
}
|
||||
if remote_endpoint:
|
||||
if resolved_token:
|
||||
msg["token"] = resolved_token
|
||||
route_profile = profile or os.environ.get("BROWSER_CLI_PROFILE")
|
||||
if route_profile:
|
||||
msg["_route"] = route_profile
|
||||
payload = json.dumps(msg).encode("utf-8")
|
||||
framed = struct.pack("<I", len(payload)) + payload
|
||||
|
||||
try:
|
||||
if is_windows():
|
||||
if remote_endpoint:
|
||||
host, _, port_str = remote_endpoint.rpartition(":")
|
||||
if not host or not port_str:
|
||||
raise BrowserNotConnected(f"Invalid remote endpoint '{remote_endpoint}': expected host:port")
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.connect((host, int(port_str)))
|
||||
sock.sendall(framed)
|
||||
response = _recv_all(sock)
|
||||
elif is_windows():
|
||||
sock_path = _resolve_socket(profile)
|
||||
with PipeClient(sock_path, family="AF_PIPE") as conn:
|
||||
conn.send_bytes(payload)
|
||||
response = conn.recv_bytes()
|
||||
else:
|
||||
sock_path = _resolve_socket(profile)
|
||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
|
||||
sock.connect(sock_path)
|
||||
sock.sendall(framed)
|
||||
response = _recv_all(sock)
|
||||
except (FileNotFoundError, ConnectionRefusedError, OSError):
|
||||
if remote_endpoint:
|
||||
raise BrowserNotConnected(
|
||||
f"Cannot connect to remote browser at {remote_endpoint}.\n"
|
||||
"Make sure browser-cli serve is running on the remote host."
|
||||
)
|
||||
profile_hint = f" (profile: {profile})" if profile else ""
|
||||
raise BrowserNotConnected(
|
||||
f"Cannot connect to browser{profile_hint}.\n"
|
||||
|
||||
Reference in New Issue
Block a user