Files
browser-cli/browser_cli/serve/proxy.py
T
daniel156161 6fa931aa36
Testing / remote-protocol-compat (0.9.5) (push) Successful in 56s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 59s
Testing / test (push) Successful in 1m1s
Build & Publish Package / publish (push) Successful in 33s
Package Extension / package-extension (push) Successful in 36s
feat: harden remote serve and reuse connections
- Gate TCP serve commands with safe-by-default policies, per-key allow tokens, per-key rate limiting, and audit labels.
- Reuse authenticated encrypted remote sessions and parallelize/caches multi-browser fanout to reduce repeated handshake roundtrips.
- Increase paged native-host batch size with extension-side byte budgeting to speed large tab listings safely.
- Point install output at public Chrome Web Store / Firefox AMO listings by default, with --dev preserving unpacked workflows.
- Share search-engine metadata between CLI and SDK and bump the package/extension version to 0.16.0.
- Cover the new security, pooling, paging, install, and fanout behavior with expanded Python and extension tests.
2026-06-18 14:24:15 +02:00

81 lines
3.2 KiB
Python

"""Proxying from TCP clients to the local browser native-host socket."""
from __future__ import annotations
import asyncio
import json
from browser_cli import transport
from browser_cli.compat import adapt_request, adapt_response
from browser_cli.framing import async_recv_frame, async_send_frame
from browser_cli.serve.logging import log_request
_STRIP_PROTOCOL_FIELDS = {"token", "_route", "pubkey", "sig", "user_agent", "pq_kex", "encrypted", "accept_encoding"}
class ServeProxyMixin:
addr: tuple
profile: str | None
client_ver: str
command: str
compress: bool
accept_encoding: dict | None
auth_label: str | None
async def send_error(self, msg: str, msg_id=None) -> None: ...
async def send_payload(self, data: bytes) -> None: ...
async def forward_to_browser(self, msg: dict) -> None:
from browser_cli.client import BrowserNotConnected
from browser_cli.client.targets import resolve_socket
from browser_cli.platform import is_windows
resolved_profile = msg.get("_route") or self.profile
clean_msg = {k: v for k, v in msg.items() if k not in _STRIP_PROTOCOL_FIELDS}
clean_payload = json.dumps(adapt_request(clean_msg, self.client_ver)).encode()
try:
sock_path = resolve_socket(resolved_profile)
except BrowserNotConnected as e:
await self.send_error(str(e))
log_request(self.addr, self.command, resolved_profile, "ERROR", "browser not connected", identity=self.auth_label)
return
try:
if is_windows():
resp_payload = await self._windows_roundtrip(sock_path, clean_payload)
else:
resp_payload = await self._unix_roundtrip(sock_path, clean_payload)
await self.send_browser_response(adapt_response(resp_payload, self.command, self.client_ver), resolved_profile)
except (OSError, json.JSONDecodeError, ConnectionError) as e:
await self.send_error(str(e))
log_request(self.addr, self.command, resolved_profile, "ERROR", str(e), identity=self.auth_label)
async def _windows_roundtrip(self, sock_path: str, payload: bytes) -> bytes:
from multiprocessing.connection import Client as PipeClient
def _pipe_roundtrip():
with PipeClient(sock_path, family="AF_PIPE") as pipe:
pipe.send_bytes(payload)
return pipe.recv_bytes()
return await asyncio.to_thread(_pipe_roundtrip)
async def _unix_roundtrip(self, sock_path: str, payload: bytes) -> bytes:
local_reader, local_writer = await asyncio.open_unix_connection(sock_path)
try:
await async_send_frame(local_writer, payload)
return await async_recv_frame(local_reader) or b""
finally:
local_writer.close()
await local_writer.wait_closed()
async def send_browser_response(self, resp_payload: bytes, resolved_profile: str | None) -> None:
resp_data = json.loads(resp_payload)
if self.compress:
await self.send_payload(transport.encode_response(resp_data, self.accept_encoding, self.command))
else:
await self.send_payload(resp_payload)
if resp_data.get("success", True):
log_request(self.addr, self.command, resolved_profile, "OK", identity=self.auth_label)
else:
log_request(self.addr, self.command, resolved_profile, "ERROR", resp_data.get("error", ""), identity=self.auth_label)