feat: auth keys shows trusted keys with names; remote auth trust/keys
- authorized_keys format extended to '<hex> [optional-name]' - auth keys repurposed: shows server's trusted keys (Name/Public Key table) instead of local client keys; --remote queries the remote serve instance - auth trust gains --name flag for labelling keys; --remote pushes the key to the remote server's authorized_keys - serve.py handles browser-cli.auth.keys and browser-cli.auth.trust as server-side commands (authenticated, never forwarded to native host) - serve.py reloads authorized_keys from disk on every connection so auth trust --remote takes effect immediately without restarting serve - auth show unchanged: still prints your own client public key Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,7 @@ def _log(addr:tuple, command:str, profile:str|None, status:str, error:str|None=N
|
||||
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, auth_keys:list[str]|None, nonce:str) -> None:
|
||||
def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, server_token:str|None, auth_keys:list[str]|None, auth_keys_path:"Path|None", nonce:str) -> None:
|
||||
from browser_cli.client import _resolve_socket, BrowserNotConnected
|
||||
from browser_cli.platform import is_windows
|
||||
|
||||
@@ -87,6 +87,37 @@ def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, serv
|
||||
_log(addr, command, None, "OK")
|
||||
return
|
||||
|
||||
if command == "browser-cli.auth.keys":
|
||||
if auth_keys_path is None:
|
||||
_send_error(msg_id, "no authorized keys file configured on this server")
|
||||
_log(addr, command, None, "ERROR", "no authorized keys file")
|
||||
return
|
||||
from browser_cli.auth import load_authorized_keys_with_names
|
||||
entries = [{"pubkey": pk, "name": name} for pk, name in load_authorized_keys_with_names(auth_keys_path)]
|
||||
data = json.dumps({"id": msg_id, "success": True, "data": entries}).encode()
|
||||
client_sock.sendall(struct.pack("<I", len(data)) + data)
|
||||
_log(addr, command, None, "OK")
|
||||
return
|
||||
|
||||
if command == "browser-cli.auth.trust":
|
||||
if auth_keys_path is None:
|
||||
_send_error(msg_id, "no authorized keys file configured on this server")
|
||||
_log(addr, command, None, "ERROR", "no authorized keys file")
|
||||
return
|
||||
from browser_cli.auth import add_authorized_key
|
||||
args = msg.get("args") or {}
|
||||
pubkey = str(args.get("pubkey") or "")
|
||||
name = str(args.get("name") or "")
|
||||
if len(pubkey) != 64:
|
||||
_send_error(msg_id, "invalid pubkey: expected 64 hex characters")
|
||||
_log(addr, command, None, "ERROR", "invalid pubkey")
|
||||
return
|
||||
added = add_authorized_key(auth_keys_path, pubkey, name)
|
||||
data = json.dumps({"id": msg_id, "success": True, "data": {"added": added}}).encode()
|
||||
client_sock.sendall(struct.pack("<I", len(data)) + data)
|
||||
_log(addr, command, None, "OK" if added else "ALREADY_TRUSTED")
|
||||
return
|
||||
|
||||
resolved_profile = msg.get("_route") or profile
|
||||
|
||||
strip = {"token", "_route", "pubkey", "sig"}
|
||||
@@ -129,20 +160,26 @@ def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, serv
|
||||
_send_error(msg_id, str(e))
|
||||
_log(addr, command, resolved_profile, "ERROR", str(e))
|
||||
|
||||
def _handle_client(client_sock:socket.socket, addr:tuple, profile:str|None, server_token:str|None, auth_keys:list[str]|None) -> None:
|
||||
def _handle_client(client_sock:socket.socket, addr:tuple, profile:str|None, server_token:str|None, auth_keys_path:"Path|None") -> None:
|
||||
if not _CONN_LIMIT.acquire(blocking=False):
|
||||
client_sock.close()
|
||||
return
|
||||
client_sock.settimeout(30)
|
||||
try:
|
||||
with client_sock:
|
||||
# reload on every connection so auth trust --remote takes effect immediately
|
||||
if auth_keys_path is not None:
|
||||
from browser_cli.auth import load_authorized_keys
|
||||
auth_keys: list[str] | None = load_authorized_keys(auth_keys_path)
|
||||
else:
|
||||
auth_keys = None
|
||||
nonce = secrets.token_hex(32)
|
||||
challenge = json.dumps({"type": "challenge", "nonce": nonce}).encode()
|
||||
try:
|
||||
client_sock.sendall(struct.pack("<I", len(challenge)) + challenge)
|
||||
except OSError:
|
||||
return
|
||||
_proxy_request(client_sock, addr, profile, server_token, auth_keys, nonce)
|
||||
_proxy_request(client_sock, addr, profile, server_token, auth_keys, auth_keys_path, nonce)
|
||||
finally:
|
||||
_CONN_LIMIT.release()
|
||||
|
||||
@@ -163,16 +200,15 @@ def cmd_serve(ctx, host, port, token, no_auth, auth_keys_file):
|
||||
|
||||
if auth_keys_file:
|
||||
from browser_cli.auth import load_authorized_keys
|
||||
path = Path(auth_keys_file)
|
||||
auth_keys = load_authorized_keys(path)
|
||||
if not auth_keys:
|
||||
console.print(f"[yellow]Warning:[/yellow] No authorized keys found in {path}")
|
||||
auth_keys_path = Path(auth_keys_file)
|
||||
if not load_authorized_keys(auth_keys_path):
|
||||
console.print(f"[yellow]Warning:[/yellow] No authorized keys found in {auth_keys_path}")
|
||||
server_token = None
|
||||
elif no_auth:
|
||||
auth_keys = None
|
||||
auth_keys_path = None
|
||||
server_token = None
|
||||
else:
|
||||
auth_keys = None
|
||||
auth_keys_path = None
|
||||
server_token = token or secrets.token_urlsafe(32)
|
||||
|
||||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
@@ -188,8 +224,10 @@ def cmd_serve(ctx, host, port, token, no_auth, auth_keys_file):
|
||||
browser_hint = f" (browser: {profile})" if profile else ""
|
||||
console.print(f"[green]Serving browser{browser_hint} →[/green] [cyan]{host}:{port}[/cyan]")
|
||||
|
||||
if auth_keys is not None:
|
||||
console.print(f" Auth: [bold green]Ed25519 pubkey[/bold green] ({len(auth_keys)} trusted key{'s' if len(auth_keys) != 1 else ''})")
|
||||
if auth_keys_path is not None:
|
||||
from browser_cli.auth import load_authorized_keys
|
||||
n = len(load_authorized_keys(auth_keys_path))
|
||||
console.print(f" Auth: [bold green]Ed25519 pubkey[/bold green] ({n} trusted key{'s' if n != 1 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]")
|
||||
elif server_token:
|
||||
@@ -206,7 +244,7 @@ def cmd_serve(ctx, host, port, token, no_auth, auth_keys_file):
|
||||
try:
|
||||
while True:
|
||||
conn, addr = server.accept()
|
||||
threading.Thread(target=_handle_client, args=(conn, addr, profile, server_token, auth_keys), daemon=True).start()
|
||||
threading.Thread(target=_handle_client, args=(conn, addr, profile, server_token, auth_keys_path), daemon=True).start()
|
||||
except KeyboardInterrupt:
|
||||
console.print("[yellow]Stopped.[/yellow]")
|
||||
finally:
|
||||
|
||||
Reference in New Issue
Block a user