7b4d96845d
- Resolve bare remote hosts through the remote registry when one explicit port is stored. - Preserve bare host behavior when no unique explicit-port registry match exists. - Route requested remote targets through the new registry resolver. - Add registry tests for unique and ambiguous explicit-port matches. - Bump package and extension versions to 0.16.6.
101 lines
3.7 KiB
Python
101 lines
3.7 KiB
Python
"""Persistence for remembered remote browser endpoints and key specs."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from browser_cli.constants import CONFIG_DIR
|
|
from browser_cli.endpoints import _normalize_endpoint
|
|
|
|
REMOTE_REGISTRY_PATH = CONFIG_DIR / "remotes.json"
|
|
|
|
def load_remotes() -> dict[str, dict[str, str]]:
|
|
if not REMOTE_REGISTRY_PATH.exists():
|
|
return {}
|
|
try:
|
|
data = json.loads(REMOTE_REGISTRY_PATH.read_text(encoding="utf-8"))
|
|
except Exception:
|
|
return {}
|
|
if not isinstance(data, dict):
|
|
return {}
|
|
# Normalize keys so old entries stored as "domain:443" match current lookups.
|
|
return {_normalize_endpoint(str(endpoint)): cfg for endpoint, cfg in data.items() if isinstance(cfg, dict)}
|
|
|
|
def resolve_remote_endpoint(endpoint: str | None) -> str | None:
|
|
"""Resolve a user-supplied remote alias to a remembered endpoint.
|
|
|
|
Domain-like remotes without an explicit port still default to :443 when no
|
|
matching remembered remote exists. If the user remembered exactly one
|
|
explicit-port remote for the same host (for example
|
|
``browser-host.example:8765``), use that endpoint so ``--remote
|
|
browser-host.example`` targets the stored service instead of assuming HTTPS.
|
|
"""
|
|
if not endpoint:
|
|
return None
|
|
normalized = _normalize_endpoint(endpoint)
|
|
host, sep, _port = normalized.rpartition(":")
|
|
if sep:
|
|
return normalized
|
|
|
|
remotes = load_remotes()
|
|
explicit_matches = []
|
|
for remote_endpoint in remotes:
|
|
remote_host, remote_sep, remote_port = remote_endpoint.rpartition(":")
|
|
if remote_sep and remote_host == normalized and remote_port != "443":
|
|
explicit_matches.append(remote_endpoint)
|
|
if len(explicit_matches) == 1:
|
|
return explicit_matches[0]
|
|
return normalized
|
|
|
|
def is_valid_key_spec(value: str) -> bool:
|
|
"""Return True for 'agent', 'agent:<selector>', or a plausible key file path."""
|
|
return value == "agent" or value.startswith("agent:") or (
|
|
not value.startswith("<") and ("/" in value or Path(value).suffix in {".pem", ".key"})
|
|
)
|
|
|
|
def save_remote(endpoint: str, key_spec: str | None = None) -> None:
|
|
"""Persist a remote endpoint, optionally with a key spec."""
|
|
if not endpoint:
|
|
return
|
|
normalized = _normalize_endpoint(endpoint)
|
|
remotes = load_remotes()
|
|
current = remotes.get(normalized, {})
|
|
if key_spec:
|
|
if not is_valid_key_spec(key_spec):
|
|
return
|
|
current["key"] = key_spec
|
|
remotes[normalized] = current
|
|
REMOTE_REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
fd = os.open(str(REMOTE_REGISTRY_PATH), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
f.write(json.dumps(remotes, indent=2, sort_keys=True) + "\n")
|
|
|
|
def remove_remote(endpoint: str) -> bool:
|
|
"""Remove a remembered remote endpoint. Returns True when it existed."""
|
|
normalized = _normalize_endpoint(endpoint)
|
|
remotes = load_remotes()
|
|
if normalized not in remotes:
|
|
return False
|
|
del remotes[normalized]
|
|
REMOTE_REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
fd = os.open(str(REMOTE_REGISTRY_PATH), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
f.write(json.dumps(remotes, indent=2, sort_keys=True) + "\n")
|
|
return True
|
|
|
|
def save_remote_key(endpoint: str, key_spec: str) -> None:
|
|
"""Persist the key spec (e.g. 'agent' or a file path) for a remote endpoint."""
|
|
save_remote(endpoint, key_spec)
|
|
|
|
def key_for_remote(endpoint: str | None) -> str | None:
|
|
if not endpoint:
|
|
return None
|
|
cfg = load_remotes().get(endpoint) or {}
|
|
key = cfg.get("key")
|
|
if not key:
|
|
return None
|
|
key_str = str(key)
|
|
# Reject corrupted values (e.g. str(AgentKey(...)) saved by an older bug).
|
|
return key_str if is_valid_key_spec(key_str) else None
|