2026-02-25 21:59:29 +08:00
|
|
|
"""
|
|
|
|
|
WebSocket \u96a7\u9053\u4e8c\u8fdb\u5236\u5e27\u534f\u8bae
|
|
|
|
|
|
|
|
|
|
\u5e27\u683c\u5f0f:
|
|
|
|
|
| stream_id (4B) | msg_type (1B) | flags (1B) | payload_len (4B) | payload (NB) |
|
|
|
|
|
|
|
|
|
|
\u7528\u4e8e Aether \u4e0e aether-proxy \u4e4b\u95f4\u7684 WebSocket \u96a7\u9053\u591a\u8def\u590d\u7528\u901a\u4fe1\u3002
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import struct
|
|
|
|
|
from enum import IntEnum
|
|
|
|
|
from typing import Self
|
|
|
|
|
|
2026-03-02 12:27:51 +08:00
|
|
|
_U64_MAX = (1 << 64) - 1
|
|
|
|
|
|
2026-02-25 21:59:29 +08:00
|
|
|
HEADER_SIZE = 10 # 4 + 1 + 1 + 4 bytes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MsgType(IntEnum):
|
|
|
|
|
"""\u6d88\u606f\u7c7b\u578b"""
|
|
|
|
|
|
|
|
|
|
REQUEST_HEADERS = 0x01 # Aether -> Proxy: \u8bf7\u6c42\u5143\u6570\u636e (JSON)
|
|
|
|
|
REQUEST_BODY = 0x02 # Aether -> Proxy: \u8bf7\u6c42\u4f53
|
|
|
|
|
RESPONSE_HEADERS = 0x03 # Proxy -> Aether: \u54cd\u5e94\u72b6\u6001\u7801 + headers (JSON)
|
|
|
|
|
RESPONSE_BODY = 0x04 # Proxy -> Aether: \u54cd\u5e94\u4f53\uff08\u6d41\u5f0f\u5206\u5757\uff09
|
|
|
|
|
STREAM_END = 0x05 # \u53cc\u5411: \u6d41\u7ed3\u675f
|
|
|
|
|
STREAM_ERROR = 0x06 # \u53cc\u5411: \u6d41\u9519\u8bef
|
|
|
|
|
|
|
|
|
|
PING = 0x10 # \u53cc\u5411: \u5fc3\u8df3 (stream_id=0)
|
|
|
|
|
PONG = 0x11 # \u53cc\u5411: \u5fc3\u8df3\u54cd\u5e94 (stream_id=0)
|
|
|
|
|
GOAWAY = 0x12 # \u53cc\u5411: \u4f18\u96c5\u5173\u95ed (stream_id=0)
|
|
|
|
|
HEARTBEAT_DATA = 0x13 # Proxy -> Aether: \u6307\u6807\u4e0a\u62a5
|
|
|
|
|
HEARTBEAT_ACK = 0x14 # Aether -> Proxy: \u5fc3\u8df3\u786e\u8ba4 + \u8fdc\u7a0b\u914d\u7f6e
|
2026-04-03 14:59:58 +08:00
|
|
|
NODE_STATUS = 0x15 # Tunnel relay -> Worker: \u8282\u70b9\u8fde\u63a5\u72b6\u6001\u5e7f\u64ad
|
2026-02-25 21:59:29 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class FrameFlags:
|
|
|
|
|
"""\u5e27\u6807\u5fd7\u4f4d"""
|
|
|
|
|
|
|
|
|
|
END_STREAM = 0x01
|
|
|
|
|
GZIP_COMPRESSED = 0x02
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Frame:
|
|
|
|
|
"""WebSocket \u96a7\u9053\u5e27"""
|
|
|
|
|
|
|
|
|
|
__slots__ = ("stream_id", "msg_type", "flags", "payload")
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
stream_id: int,
|
|
|
|
|
msg_type: MsgType,
|
|
|
|
|
flags: int = 0,
|
|
|
|
|
payload: bytes = b"",
|
|
|
|
|
) -> None:
|
|
|
|
|
self.stream_id = stream_id
|
|
|
|
|
self.msg_type = msg_type
|
|
|
|
|
self.flags = flags
|
|
|
|
|
self.payload = payload
|
|
|
|
|
|
|
|
|
|
def encode(self) -> bytes:
|
|
|
|
|
header = struct.pack(
|
|
|
|
|
"!IBBI",
|
|
|
|
|
self.stream_id,
|
|
|
|
|
self.msg_type,
|
|
|
|
|
self.flags,
|
|
|
|
|
len(self.payload),
|
|
|
|
|
)
|
|
|
|
|
return header + self.payload
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def decode(cls, data: bytes) -> Self:
|
|
|
|
|
if len(data) < HEADER_SIZE:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"\u5e27\u6570\u636e\u592a\u77ed: \u9700\u8981 {HEADER_SIZE} \u5b57\u8282, \u5b9e\u9645 {len(data)}"
|
|
|
|
|
)
|
|
|
|
|
stream_id, msg_type_raw, flags, payload_len = struct.unpack("!IBBI", data[:HEADER_SIZE])
|
|
|
|
|
expected_total = HEADER_SIZE + payload_len
|
|
|
|
|
if len(data) < expected_total:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"\u5e27\u6570\u636e\u4e0d\u5b8c\u6574: \u9700\u8981 {expected_total} \u5b57\u8282, \u5b9e\u9645 {len(data)}"
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
msg_type = MsgType(msg_type_raw)
|
|
|
|
|
except ValueError:
|
|
|
|
|
raise ValueError(f"\u672a\u77e5\u6d88\u606f\u7c7b\u578b: 0x{msg_type_raw:02x}")
|
|
|
|
|
payload = data[HEADER_SIZE:expected_total]
|
|
|
|
|
return cls(stream_id, msg_type, flags, payload)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def is_end_stream(self) -> bool:
|
|
|
|
|
return bool(self.flags & FrameFlags.END_STREAM)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def is_gzip(self) -> bool:
|
|
|
|
|
return bool(self.flags & FrameFlags.GZIP_COMPRESSED)
|
|
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
|
return (
|
|
|
|
|
f"Frame(stream={self.stream_id}, type={self.msg_type.name}, "
|
|
|
|
|
f"flags=0x{self.flags:02x}, payload_len={len(self.payload)})"
|
|
|
|
|
)
|
2026-03-02 12:27:51 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def normalize_heartbeat_id(value: object) -> int | None:
|
|
|
|
|
"""Normalize heartbeat_id to u64-compatible int for Rust ACK parsing."""
|
|
|
|
|
if isinstance(value, bool) or value is None:
|
|
|
|
|
return None
|
|
|
|
|
parsed: int | None = None
|
|
|
|
|
if isinstance(value, int):
|
|
|
|
|
parsed = value
|
|
|
|
|
elif isinstance(value, float):
|
|
|
|
|
if value.is_integer():
|
|
|
|
|
parsed = int(value)
|
|
|
|
|
elif isinstance(value, str):
|
|
|
|
|
stripped = value.strip()
|
|
|
|
|
if stripped.isdigit():
|
|
|
|
|
try:
|
|
|
|
|
parsed = int(stripped)
|
|
|
|
|
except ValueError:
|
|
|
|
|
parsed = None
|
|
|
|
|
if parsed is None or parsed < 0 or parsed > _U64_MAX:
|
|
|
|
|
return None
|
|
|
|
|
return parsed
|