feat: harden remote serve and reuse connections
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

- 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.
This commit is contained in:
2026-06-18 14:24:15 +02:00
parent 8dece7800f
commit 6fa931aa36
49 changed files with 3407 additions and 1878 deletions
+103
View File
@@ -0,0 +1,103 @@
"""Per-process pool of authenticated remote connections for reuse.
A ``browser-cli serve`` connection stays open after its first (encrypted)
command, so the client can send further commands over it without re-running the
TCP/TLS/challenge/auth handshake (~hundreds of ms each). Only encrypted (PQ)
sessions are pooled — plaintext/legacy sessions stay one-shot, matching the
server, which only loops for encrypted sessions.
Connections are checked out exclusively (never shared between threads at once),
returned on success, and dropped on any I/O error or once older than an idle
bound (kept below the server's idle timeout so we don't reuse a connection the
server has already closed).
"""
from __future__ import annotations
import atexit
import json
import socket
import threading
import time
from browser_cli.constants import REMOTE_SESSION_IDLE_TIMEOUT
from browser_cli.framing import frame
# Retire a pooled connection a few seconds before the server would, so we never
# hand back one the server has just timed out and closed.
_MAX_IDLE_SECONDS = max(5, REMOTE_SESSION_IDLE_TIMEOUT - 5)
_MAX_PER_ENDPOINT = 8
class PooledConnection:
__slots__ = ("sock", "secret", "last_used")
def __init__(self, sock: socket.socket, secret: bytes) -> None:
self.sock = sock
self.secret = secret
self.last_used = time.monotonic()
_POOL: dict[str, list[PooledConnection]] = {}
_LOCK = threading.Lock()
def _close(sock: socket.socket) -> None:
try:
sock.close()
except OSError:
pass
def checkout(endpoint: str) -> PooledConnection | None:
"""Take an idle authenticated connection for *endpoint*, or None."""
now = time.monotonic()
with _LOCK:
conns = _POOL.get(endpoint)
while conns:
conn = conns.pop()
if now - conn.last_used <= _MAX_IDLE_SECONDS:
return conn
_close(conn.sock) # too old — assume the server has dropped it
return None
def checkin(endpoint: str, conn: PooledConnection) -> None:
"""Return a still-healthy connection to the pool for reuse."""
conn.last_used = time.monotonic()
with _LOCK:
bucket = _POOL.setdefault(endpoint, [])
if len(bucket) >= _MAX_PER_ENDPOINT:
_close(conn.sock)
return
bucket.append(conn)
def discard(conn: PooledConnection) -> None:
"""Drop a connection that errored or is no longer usable."""
_close(conn.sock)
def close_all() -> None:
"""Close every pooled connection (process exit / test isolation)."""
with _LOCK:
for bucket in _POOL.values():
for conn in bucket:
_close(conn.sock)
_POOL.clear()
def session_inner_message(msg: dict) -> dict:
"""Strip auth/transport fields, leaving the command for an established session."""
keep = {"id", "command", "args", "user_agent", "accept_encoding", "_route", "_suppress_pq_warning"}
return {k: v for k, v in msg.items() if k in keep}
def send_over(conn: PooledConnection, msg: dict) -> bytes | None:
"""Send one command over an existing encrypted session. Raises on I/O error."""
from browser_cli.auth import pq_encrypt
from browser_cli.remote.socket import recv_all
from browser_cli.remote.transport import _decode_pq_response
inner = json.dumps(session_inner_message(msg)).encode("utf-8")
envelope = json.dumps({"encrypted": pq_encrypt(conn.secret, "request", inner)}).encode("utf-8")
conn.sock.sendall(frame(envelope))
response = recv_all(conn.sock)
if not response:
# EOF — an older server (no session loop) closed after one command. Treat as
# a transport failure so the caller re-handshakes; never as an app error,
# which could double-execute a non-idempotent command on retry.
raise EOFError("remote closed the pooled connection")
return _decode_pq_response(response, conn.secret)
atexit.register(close_all)