Replace shared bearer token with registered Ed25519 client keys.
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
"""Native messaging host: 127.0.0.1 HTTP <-> Firefox extension stdio."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
@@ -14,6 +13,10 @@ 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, ReplayCache, verify_request # noqa: E402
|
||||
|
||||
if sys.platform == "win32":
|
||||
import msvcrt
|
||||
|
||||
@@ -25,10 +28,10 @@ PORT = int(os.environ.get("FAB_PORT", "17634"))
|
||||
MAX_BODY = 256 * 1024
|
||||
ALLOWED_HOSTS = {f"127.0.0.1:{PORT}", f"localhost:{PORT}"}
|
||||
STATE_DIR = Path(os.environ.get("LOCALAPPDATA", str(Path.home()))) / "firefox-agent-bridge"
|
||||
TOKEN_PATH = STATE_DIR / "token"
|
||||
STDIN_LOCK = threading.Lock()
|
||||
PENDING: dict[str, tuple[threading.Event, dict[str, Any]]] = {}
|
||||
PENDING_LOCK = threading.Lock()
|
||||
REPLAY = ReplayCache()
|
||||
|
||||
|
||||
def log(msg: str) -> None:
|
||||
@@ -36,15 +39,6 @@ def log(msg: str) -> None:
|
||||
sys.stderr.flush()
|
||||
|
||||
|
||||
def read_token() -> str:
|
||||
env = os.environ.get("FAB_TOKEN")
|
||||
if env:
|
||||
return env.strip()
|
||||
if TOKEN_PATH.exists():
|
||||
return TOKEN_PATH.read_text(encoding="utf-8").strip()
|
||||
return ""
|
||||
|
||||
|
||||
def send_to_extension(payload: dict[str, Any]) -> None:
|
||||
raw = json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
||||
with STDIN_LOCK:
|
||||
@@ -106,27 +100,25 @@ class Handler(BaseHTTPRequestHandler):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _auth_ok(self) -> bool:
|
||||
if not self._gate_ok():
|
||||
return False
|
||||
token = read_token()
|
||||
if not token:
|
||||
self._send(500, {"ok": False, "error": "bridge token missing; run install-native-host.ps1"})
|
||||
return False
|
||||
header = self.headers.get("Authorization", "")
|
||||
got = header[7:].strip() if header.lower().startswith("bearer ") else ""
|
||||
if not hmac.compare_digest(got, token):
|
||||
self._send(401, {"ok": False, "error": "missing or invalid bearer token"})
|
||||
return False
|
||||
return True
|
||||
|
||||
def _json_body(self) -> dict[str, Any]:
|
||||
def _read_raw(self) -> bytes:
|
||||
length = int(self.headers.get("Content-Length") or "0")
|
||||
if length > MAX_BODY:
|
||||
raise ValueError("request body too large")
|
||||
if length <= 0:
|
||||
return {}
|
||||
raw = self.rfile.read(length)
|
||||
return b""
|
||||
return self.rfile.read(length)
|
||||
|
||||
def _authorize(self, http_method: str, path: str, raw_body: bytes) -> bool:
|
||||
if not self._gate_ok():
|
||||
return False
|
||||
try:
|
||||
verify_request({k: v for k, v in self.headers.items()}, http_method, path, raw_body, REPLAY)
|
||||
except PermissionError as exc:
|
||||
self._send(401, {"ok": False, "error": str(exc)})
|
||||
return False
|
||||
return True
|
||||
|
||||
def _parse_json(self, raw: bytes) -> dict[str, Any]:
|
||||
if not raw:
|
||||
return {}
|
||||
data = json.loads(raw.decode("utf-8"))
|
||||
@@ -141,7 +133,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
return
|
||||
self._send(200, {"ok": True, "service": "firefox-agent-bridge", "port": PORT})
|
||||
return
|
||||
if not self._auth_ok():
|
||||
if not self._authorize("GET", parsed.path, b""):
|
||||
return
|
||||
qs = parse_qs(parsed.query)
|
||||
try:
|
||||
@@ -167,11 +159,16 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self._send(502, {"ok": False, "error": str(exc)})
|
||||
|
||||
def do_POST(self) -> None:
|
||||
if not self._auth_ok():
|
||||
return
|
||||
parsed = urlparse(self.path)
|
||||
try:
|
||||
body = self._json_body()
|
||||
raw = self._read_raw()
|
||||
except ValueError as exc:
|
||||
self._send(400, {"ok": False, "error": str(exc)})
|
||||
return
|
||||
if not self._authorize("POST", parsed.path, raw):
|
||||
return
|
||||
try:
|
||||
body = self._parse_json(raw)
|
||||
except ValueError as exc:
|
||||
self._send(400, {"ok": False, "error": str(exc)})
|
||||
return
|
||||
@@ -225,9 +222,8 @@ def stdin_loop() -> None:
|
||||
|
||||
def main() -> int:
|
||||
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
token = read_token()
|
||||
if not token:
|
||||
log(f"no token at {TOKEN_PATH}; run tools/install-native-host.ps1")
|
||||
if not CLIENTS_PATH.exists():
|
||||
log(f"no registered clients at {CLIENTS_PATH}; run tools/register_client.py add")
|
||||
threading.Thread(target=stdin_loop, name="fab-stdin", daemon=True).start()
|
||||
server = ThreadingHTTPServer((HOST, PORT), Handler)
|
||||
log(f"listening on http://{HOST}:{PORT}")
|
||||
|
||||
Reference in New Issue
Block a user