Keep client secrets in add-on storage; host only verifies and serves the local API.

This commit is contained in:
alexveley
2026-09-22 07:42:10 -04:00
parent de0814f3ed
commit 41c83dabac
13 changed files with 185 additions and 290 deletions
+33 -24
View File
@@ -2,6 +2,7 @@
"""Native messaging host: 127.0.0.1 HTTP <-> Firefox extension stdio."""
from __future__ import annotations
import hashlib
import json
import os
import struct
@@ -13,10 +14,6 @@ from pathlib import Path
from typing import Any
from urllib.parse import parse_qs, urlparse
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from fab_auth import CLIENTS_PATH, handle_admin, verify_bearer # noqa: E402
if sys.platform == "win32":
import msvcrt
@@ -57,6 +54,33 @@ def read_from_extension() -> dict[str, Any] | None:
return json.loads(body.decode("utf-8"))
def _header(headers: dict[str, str], name: str) -> str:
want = name.lower()
for key, value in headers.items():
if key.lower() == want:
return (value or "").strip()
return ""
def verify_token_with_extension(token: str, timeout: float = 8.0) -> dict[str, Any]:
if not token.startswith("fab_"):
raise PermissionError("unrecognized token")
digest = hashlib.sha256(token.encode("ascii")).hexdigest()
req_id = str(uuid.uuid4())
event = threading.Event()
slot: dict[str, Any] = {}
with PENDING_LOCK:
PENDING[req_id] = (event, slot)
send_to_extension({"type": "auth", "id": req_id, "token_hash": digest})
if not event.wait(timeout):
with PENDING_LOCK:
PENDING.pop(req_id, None)
raise PermissionError("extension did not verify token")
if not slot.get("ok"):
raise PermissionError(slot.get("error") or "unknown token")
return slot.get("client") or {}
def call_extension(method: str, args: list[Any] | None = None, timeout: float = 15.0) -> dict[str, Any]:
req_id = str(uuid.uuid4())
event = threading.Event()
@@ -111,7 +135,10 @@ class Handler(BaseHTTPRequestHandler):
if not self._gate_ok():
return False
try:
verify_bearer({k: v for k, v in self.headers.items()})
auth = _header({k: v for k, v in self.headers.items()}, "Authorization")
if not auth.lower().startswith("bearer "):
raise PermissionError("Bearer token required")
verify_token_with_extension(auth[7:].strip())
except PermissionError as exc:
self._send(401, {"ok": False, "error": str(exc)})
return False
@@ -130,7 +157,7 @@ class Handler(BaseHTTPRequestHandler):
if parsed.path == "/health":
if not self._gate_ok():
return
self._send(200, {"ok": True, "service": "firefox-agent-bridge", "port": PORT})
self._send(200, {"ok": True, "service": "bookmarks-api", "port": PORT})
return
if not self._authorize("GET", parsed.path, b""):
return
@@ -208,22 +235,6 @@ def stdin_loop() -> None:
break
if message is None:
break
if message.get("type") == "admin":
try:
result = handle_admin(str(message.get("method") or ""), message.get("args") or [])
send_to_extension(
{"type": "admin-result", "id": message.get("id"), "ok": True, "result": result}
)
except Exception as exc:
send_to_extension(
{
"type": "admin-result",
"id": message.get("id"),
"ok": False,
"error": str(exc),
}
)
continue
req_id = str(message.get("id") or "")
with PENDING_LOCK:
pending = PENDING.pop(req_id, None)
@@ -237,8 +248,6 @@ def stdin_loop() -> None:
def main() -> int:
STATE_DIR.mkdir(parents=True, exist_ok=True)
if not CLIENTS_PATH.exists():
log(f"no registered clients at {CLIENTS_PATH}; use the add-on Manage clients page")
threading.Thread(target=stdin_loop, name="fab-stdin", daemon=True).start()
server = ThreadingHTTPServer((HOST, PORT), Handler)
log(f"listening on http://{HOST}:{PORT}")