"""本地配对服务:接收浏览器发来的 OMS Token。""" from __future__ import annotations import json import logging import threading from dataclasses import dataclass from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from typing import Callable logger = logging.getLogger(__name__) DEFAULT_PAIR_PORT = 19333 @dataclass(frozen=True) class PairPayload: token: str oms_url: str web_origin: str = "" class PairServer: """127.0.0.1 本地 HTTP 服务,供 OMS 网页推送 JWT。""" def __init__(self, port: int = DEFAULT_PAIR_PORT) -> None: self.port = port self._server: ThreadingHTTPServer | None = None self._thread: threading.Thread | None = None self._waiting = False self._on_pair: Callable[[PairPayload], None] | None = None @property def is_waiting(self) -> bool: return self._waiting def start(self, on_pair: Callable[[PairPayload], None]) -> None: self.stop() self._on_pair = on_pair self._waiting = True server_ref = self class Handler(BaseHTTPRequestHandler): def log_message(self, fmt: str, *args) -> None: logger.debug(fmt, *args) def _write_cors(self) -> None: origin = self.headers.get("Origin", "") if origin.startswith(("http://localhost", "http://127.0.0.1", "https://")): self.send_header("Access-Control-Allow-Origin", origin) self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") self.send_header("Access-Control-Allow-Headers", "Content-Type") def do_OPTIONS(self) -> None: self.send_response(204) self._write_cors() self.end_headers() def do_GET(self) -> None: if self.path.split("?", 1)[0].rstrip("/") != "/status": self.send_error(404) return body = json.dumps({"waiting": server_ref._waiting}) self.send_response(200) self._write_cors() self.send_header("Content-Type", "application/json; charset=utf-8") self.end_headers() self.wfile.write(body.encode("utf-8")) def do_POST(self) -> None: if self.path.split("?", 1)[0].rstrip("/") != "/pair": self.send_error(404) return length = int(self.headers.get("Content-Length", 0)) raw = self.rfile.read(length) if length else b"{}" try: data = json.loads(raw.decode("utf-8")) except json.JSONDecodeError: self.send_error(400) return if not isinstance(data, dict): self.send_error(400) return token = str(data.get("token") or "").strip() oms_url = str(data.get("omsUrl") or data.get("oms_url") or "").strip().rstrip("/") web_origin = str(data.get("webOrigin") or data.get("web_origin") or "").strip().rstrip("/") if not token or not oms_url: payload = json.dumps({"ok": False, "message": "token 与 omsUrl 必填"}) self.send_response(400) self._write_cors() self.send_header("Content-Type", "application/json; charset=utf-8") self.end_headers() self.wfile.write(payload.encode("utf-8")) return server_ref._waiting = False if server_ref._on_pair: server_ref._on_pair( PairPayload(token=token, oms_url=oms_url, web_origin=web_origin) ) payload = json.dumps({"ok": True}) self.send_response(200) self._write_cors() self.send_header("Content-Type", "application/json; charset=utf-8") self.end_headers() self.wfile.write(payload.encode("utf-8")) self._server = ThreadingHTTPServer(("127.0.0.1", self.port), Handler) self._thread = threading.Thread(target=self._server.serve_forever, daemon=True) self._thread.start() logger.info("配对服务已启动 127.0.0.1:%s", self.port) def stop(self) -> None: self._waiting = False if self._server: self._server.shutdown() self._server.server_close() self._server = None self._thread = None