Files

141 lines
4.1 KiB
Python

# -*- coding: utf-8 -*-
"""Hello handshake helpers for chrome backend connections."""
from __future__ import annotations
import asyncio
import json
from typing import Any
try:
from .protocol_mirror import PROTOCOL_VERSION, contract_snapshot
except ImportError:
from protocol_mirror import PROTOCOL_VERSION, contract_snapshot
try:
from .build_fingerprint import BUILD_FINGERPRINT
except ImportError:
try:
from build_fingerprint import BUILD_FINGERPRINT
except ImportError:
BUILD_FINGERPRINT = {"commit": "unknown", "builtAt": None}
class HandshakeError(RuntimeError):
"""Raised when a backend hello handshake fails."""
class HandshakePermanentError(HandshakeError):
"""Raised when the core rejects hello and retrying cannot help."""
def __init__(
self,
message: str,
*,
code: str = "",
expected_min: int | None = None,
expected: int | None = None,
actual: int | None = None,
advice: str = "",
raw_message: dict[str, Any] | None = None,
) -> None:
super().__init__(message)
self.code = code
self.expected_min = expected_min
self.expected = expected
self.actual = actual
self.advice = advice
self.raw_message = raw_message or {}
class HandshakeTransientError(HandshakeError):
"""Raised when retrying the hello handshake may succeed."""
def _protocol_value(message: dict[str, Any], key: str) -> int | None:
value = message.get(key)
return value if isinstance(value, int) else None
def _rejection_advice(
code: str,
expected_min: int | None,
expected: int | None,
actual: int | None,
) -> str:
if code == "BROWSER_PROTOCOL_VERSION_MISMATCH":
if (
actual is not None
and expected_min is not None
and actual < expected_min
):
return "Please upgrade the extension to a compatible version."
if actual is not None and expected is not None and actual > expected:
return "Please upgrade QwenPaw core to a compatible version."
return (
"Please upgrade the extension or QwenPaw core to compatible "
"versions."
)
return "QwenPaw core permanently rejected the Native Messaging hello."
async def send_hello(
ws: Any,
entry_id: str,
protocol_version: int = PROTOCOL_VERSION,
) -> None:
"""Send bridge hello metadata to a backend websocket."""
# Contract and build metadata let the core compare this installed host
# without changing the existing protocol-version handshake fields.
await ws.send(
json.dumps(
{
"type": "hello",
"entryId": entry_id,
"protocolVersion": protocol_version,
"contract": contract_snapshot(),
"build": dict(BUILD_FINGERPRINT),
},
separators=(",", ":"),
),
)
async def wait_hello_ack(ws: Any, timeout: float = 5.0) -> dict:
"""Wait for and validate hello_ack."""
try:
raw = await asyncio.wait_for(ws.recv(), timeout=timeout)
except asyncio.TimeoutError as exc:
raise HandshakeTransientError("Hello ack timeout") from exc
message = json.loads(
raw.decode("utf-8") if isinstance(raw, bytes) else raw,
)
if message.get("type") != "hello_ack" or message.get("status") != "ok":
code = str(message.get("code") or "")
expected_min = _protocol_value(
message,
"expected_min_protocol_version",
)
expected = _protocol_value(message, "expected_protocol_version")
actual = _protocol_value(message, "actual_protocol_version")
raise HandshakePermanentError(
f"Hello rejected: {message}",
code=code,
expected_min=expected_min,
expected=expected,
actual=actual,
advice=_rejection_advice(code, expected_min, expected, actual),
raw_message=message,
)
return message
__all__ = [
"HandshakeError",
"HandshakePermanentError",
"HandshakeTransientError",
"send_hello",
"wait_hello_ack",
]