6270d8c956
Testing / remote-protocol-compat (0.16.0) (push) Successful in 1m1s
Testing / remote-protocol-compat (0.15.0) (push) Successful in 1m3s
Testing / test (push) Failing after 1m15s
Build & Publish Package / publish (push) Successful in 51s
Package Extension / package-extension (push) Successful in 1m6s
- Add SSH-style server identity keys and known-host verification for remote serve endpoints. - Add remote add/list/remove commands for explicit endpoint persistence. - Fix remote clients listing to fan out through target discovery instead of ambiguous auto-routing. - Add URL glob matching for tabs filter and count with extension tests. - Add n8n credential pinning for server public keys or SHA256 fingerprints. - Remove obsolete compat shim behavior while keeping empty compat seams for future protocol changes. - Bump browser-cli to 0.16.4 and n8n node to 0.3.1. - Cover known-hosts, remote registry, compat seams, n8n protocol verification, and URL matching with tests.
154 lines
5.7 KiB
Python
154 lines
5.7 KiB
Python
from __future__ import annotations
|
|
|
|
import click
|
|
from rich.console import Console
|
|
from rich.table import Table
|
|
|
|
from browser_cli.errors import BrowserNotConnected
|
|
|
|
from browser_cli import BrowserCLI
|
|
from browser_cli.commands import handle_errors
|
|
from browser_cli.commands.rendering import print_browser_grouped_table_rows
|
|
from browser_cli.remote.known_hosts import fingerprint, load_known_hosts, remove_known_host, save_known_host
|
|
from browser_cli.remote.registry import load_remotes, remove_remote, save_remote, save_remote_key
|
|
|
|
console = Console()
|
|
|
|
def _print_remotes() -> None:
|
|
remotes = load_remotes()
|
|
if not remotes:
|
|
console.print("[yellow]No remembered remotes[/yellow]")
|
|
return
|
|
table = Table(show_header=True, header_style="bold cyan")
|
|
table.add_column("Endpoint")
|
|
table.add_column("Key")
|
|
for endpoint, cfg in sorted(remotes.items()):
|
|
table.add_row(endpoint, str(cfg.get("key", "")))
|
|
console.print(table)
|
|
|
|
def _remove_remote(endpoint: str, *, verb: str) -> None:
|
|
if not remove_remote(endpoint):
|
|
console.print(f"[yellow]Remote {endpoint} not remembered[/yellow]")
|
|
return
|
|
console.print(f"[green]{verb} {endpoint}[/green]")
|
|
|
|
def _fetch_server_pubkey(endpoint: str) -> str:
|
|
from browser_cli.auth.server_identity import verify_challenge_signature
|
|
from browser_cli.remote.auth import parse_challenge
|
|
from browser_cli.remote.socket import connect_socket, recv_all
|
|
|
|
sock = connect_socket(endpoint)
|
|
try:
|
|
challenge, _nonce = parse_challenge(recv_all(sock) or b"")
|
|
finally:
|
|
sock.close()
|
|
if not isinstance(challenge, dict) or not isinstance(challenge.get("server_pubkey"), str):
|
|
raise BrowserNotConnected("remote server did not advertise a server identity key")
|
|
if not verify_challenge_signature(challenge):
|
|
raise BrowserNotConnected("remote server identity signature is invalid")
|
|
return str(challenge["server_pubkey"])
|
|
|
|
@click.group("remote")
|
|
def remote_group():
|
|
"""Manage remembered browser-cli remote endpoints."""
|
|
|
|
@remote_group.command("status")
|
|
@click.argument("endpoint")
|
|
@click.option("--key", default=None, help="Key spec/path to use for this probe")
|
|
@handle_errors
|
|
def remote_status(endpoint, key):
|
|
"""Probe a remote endpoint and show server/client status."""
|
|
client = BrowserCLI(remote=endpoint, key=key)
|
|
clients = [
|
|
{**item, "profileLabel": item.get("profile", ""), "profileGroup": endpoint}
|
|
for item in client.clients()
|
|
]
|
|
columns = [
|
|
("Browser", lambda item: item.get("name", "")),
|
|
("Extension", lambda item: item.get("extensionVersion", "")),
|
|
]
|
|
print_browser_grouped_table_rows(
|
|
clients,
|
|
columns,
|
|
console=console,
|
|
empty_message="[yellow]No browser clients found[/yellow]",
|
|
browser_getter=lambda item: item.get("profileLabel", ""),
|
|
group_getter=lambda item: item.get("profileGroup", ""),
|
|
browser_header="Profile",
|
|
)
|
|
|
|
@remote_group.command("add")
|
|
@click.argument("endpoint")
|
|
@click.option("--key", "key_spec", default=None, help="Key spec/path to remember for this endpoint")
|
|
def remote_add(endpoint, key_spec):
|
|
"""Remember a remote endpoint for global multi-browser commands."""
|
|
save_remote(endpoint, key_spec)
|
|
if key_spec:
|
|
console.print(f"[green]Added remote {endpoint} with key {key_spec}[/green]")
|
|
else:
|
|
console.print(f"[green]Added remote {endpoint}[/green]")
|
|
|
|
@remote_group.command("trust")
|
|
@click.argument("endpoint")
|
|
@click.argument("key_spec")
|
|
def remote_trust(endpoint, key_spec):
|
|
"""Remember which key spec to use for ENDPOINT."""
|
|
save_remote_key(endpoint, key_spec)
|
|
console.print(f"[green]Trusted remote {endpoint} with key {key_spec}[/green]")
|
|
|
|
@remote_group.command("list")
|
|
def remote_list():
|
|
"""List remembered remote endpoints."""
|
|
_print_remotes()
|
|
|
|
@remote_group.command("trust-host")
|
|
@click.argument("endpoint")
|
|
@click.option("--pubkey", default=None, help="Pin this server public key instead of probing the endpoint")
|
|
@handle_errors
|
|
def remote_trust_host(endpoint, pubkey):
|
|
"""Pin a remote server identity key, SSH known_hosts style."""
|
|
server_pubkey = pubkey or _fetch_server_pubkey(endpoint)
|
|
save_known_host(endpoint, server_pubkey)
|
|
console.print(f"[green]Trusted server {endpoint}[/green] [dim]{fingerprint(server_pubkey)}[/dim]")
|
|
|
|
@remote_group.command("known-hosts")
|
|
def remote_known_hosts():
|
|
"""List pinned remote server identity keys."""
|
|
known = load_known_hosts()
|
|
if not known:
|
|
console.print("[yellow]No known remote server identities[/yellow]")
|
|
return
|
|
table = Table(show_header=True, header_style="bold cyan")
|
|
table.add_column("Endpoint")
|
|
table.add_column("Fingerprint")
|
|
table.add_column("Public Key")
|
|
for endpoint, pubkey in sorted(known.items()):
|
|
table.add_row(endpoint, fingerprint(pubkey), pubkey)
|
|
console.print(table)
|
|
|
|
@remote_group.command("untrust-host")
|
|
@click.argument("endpoint")
|
|
def remote_untrust_host(endpoint):
|
|
"""Remove a pinned remote server identity key."""
|
|
if not remove_known_host(endpoint):
|
|
console.print(f"[yellow]Remote server {endpoint} is not in known hosts[/yellow]")
|
|
return
|
|
console.print(f"[green]Removed server identity for {endpoint}[/green]")
|
|
|
|
@remote_group.command("keys")
|
|
def remote_keys():
|
|
"""List remembered remote key specs."""
|
|
_print_remotes()
|
|
|
|
@remote_group.command("remove")
|
|
@click.argument("endpoint")
|
|
def remote_remove(endpoint):
|
|
"""Remove a remembered remote endpoint."""
|
|
_remove_remote(endpoint, verb="Removed")
|
|
|
|
@remote_group.command("revoke")
|
|
@click.argument("endpoint")
|
|
def remote_revoke(endpoint):
|
|
"""Remove remembered key/config for ENDPOINT."""
|
|
_remove_remote(endpoint, verb="Revoked")
|