docs/WINDOWS_PORT.md 覆盖: - 逐文件可移植性审计(核心逻辑层约 90% 可直接复用) - 核心难点:iOS 17+ 的 RSD 隧道无法在 Windows 复用(macOS 实现依赖 XPC + ObjC runtime 私有框架),改用 pymobiledevice3 的 userspace 隧道 - 已实测确认的命令与 API(--userspace 挂在 lockdown start-tunnel / mounter auto-mount 上,remote start-tunnel 不支持;类名是 UserspaceRsdTunnel) - 窗口启动、数据目录、打包与分发(DMG → Inno Setup)、图标(.icns → .ico) - macOS 侧踩过的坑:镜像挂载用 Python API、必须打包包元数据、不要把 distutils 放进 excludes、AlreadyMountedError 当成功处理 - 设计约束(DVT 长连接、只能改经纬度、last_point 与 last_reported 分离等) - 验收基线(复用现有测试套件)与建议的工作顺序
327 lines
11 KiB
Python
327 lines
11 KiB
Python
"""极简 WebSocket 服务端实现(RFC 6455):零第三方依赖。
|
||
|
||
设计:
|
||
- ThreadingHTTPServer 的 handler 检测到 Upgrade: websocket 后调用 serve(),
|
||
该函数劫持 socket 并阻塞在当前线程里跑消息循环。
|
||
- 发送可能来自任意线程(HTTP 线程、播放引擎线程),因此内部用锁 + deque 保证串行写。
|
||
- 服务端只推送文本帧,不接收分片消息(浏览器不会对短 JSON 做分片)。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import base64
|
||
import hashlib
|
||
import os
|
||
import socket
|
||
import struct
|
||
import threading
|
||
import time
|
||
from collections import deque
|
||
from typing import Any, Callable
|
||
|
||
from . import protocol
|
||
|
||
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||
|
||
OP_CONT = 0x0
|
||
OP_TEXT = 0x1
|
||
OP_BIN = 0x2
|
||
OP_CLOSE = 0x8
|
||
OP_PING = 0x9
|
||
OP_PONG = 0xA
|
||
|
||
MAX_FRAME = 8 * 1024 * 1024
|
||
|
||
|
||
def is_websocket_upgrade(headers: Any) -> bool:
|
||
upgrade = (headers.get("Upgrade") or "").strip().lower()
|
||
connection = (headers.get("Connection") or "").lower()
|
||
return upgrade == "websocket" and "upgrade" in connection
|
||
|
||
|
||
def accept_key(client_key: str) -> str:
|
||
digest = hashlib.sha1((client_key + GUID).encode("ascii")).digest()
|
||
return base64.b64encode(digest).decode("ascii")
|
||
|
||
|
||
class WSConnection:
|
||
"""一条已升级的连接。线程安全的 send_json。"""
|
||
|
||
def __init__(self, sock: socket.socket, addr: tuple[str, int], require_masked: bool = True) -> None:
|
||
self.sock = sock
|
||
self.addr = addr
|
||
self.require_masked = require_masked
|
||
self.id = f"ws-{int(time.time() * 1000) % 100000}-{os.urandom(2).hex()}"
|
||
self._send_lock = threading.Lock()
|
||
self._closed = threading.Event()
|
||
self._buf = b""
|
||
self._frag_op: int | None = None
|
||
self._frag_data = bytearray()
|
||
self.bytes_sent = 0
|
||
self.bytes_recv = 0
|
||
|
||
# ---------------- 发送 ----------------
|
||
def send_frame(self, opcode: int, payload: bytes = b"") -> None:
|
||
if self._closed.is_set():
|
||
return
|
||
header = bytearray()
|
||
header.append(0x80 | opcode)
|
||
length = len(payload)
|
||
if length < 126:
|
||
header.append(length)
|
||
elif length < 65536:
|
||
header.append(126)
|
||
header.extend(struct.pack("!H", length))
|
||
else:
|
||
header.append(127)
|
||
header.extend(struct.pack("!Q", length))
|
||
frame = bytes(header) + payload
|
||
with self._send_lock:
|
||
if self._closed.is_set():
|
||
return
|
||
try:
|
||
self.sock.sendall(frame)
|
||
self.bytes_sent += len(frame)
|
||
except OSError:
|
||
self._closed.set()
|
||
|
||
def send_text(self, text: str) -> None:
|
||
self.send_frame(OP_TEXT, text.encode("utf-8"))
|
||
|
||
def send_json(self, obj: dict[str, Any]) -> None:
|
||
self.send_text(protocol.dumps(obj))
|
||
|
||
def ping(self) -> None:
|
||
self.send_frame(OP_PING)
|
||
|
||
def close(self, code: int = 1000, reason: str = "") -> None:
|
||
if self._closed.is_set():
|
||
return
|
||
payload = struct.pack("!H", code) + reason.encode("utf-8")
|
||
try:
|
||
self.send_frame(OP_CLOSE, payload)
|
||
finally:
|
||
self._closed.set()
|
||
try:
|
||
self.sock.shutdown(socket.SHUT_RDWR)
|
||
except OSError:
|
||
pass
|
||
try:
|
||
self.sock.close()
|
||
except OSError:
|
||
pass
|
||
|
||
@property
|
||
def closed(self) -> bool:
|
||
return self._closed.is_set()
|
||
|
||
# ---------------- 接收 ----------------
|
||
def _recv_exact(self, n: int) -> bytes | None:
|
||
while len(self._buf) < n:
|
||
try:
|
||
chunk = self.sock.recv(65536)
|
||
except (OSError, socket.timeout):
|
||
return None
|
||
if not chunk:
|
||
return None
|
||
self._buf += chunk
|
||
self.bytes_recv += len(chunk)
|
||
data, self._buf = self._buf[:n], self._buf[n:]
|
||
return data
|
||
|
||
def _read_frame(self) -> tuple[int, bool, bytes] | None:
|
||
head = self._recv_exact(2)
|
||
if head is None:
|
||
return None
|
||
b1, b2 = head[0], head[1]
|
||
fin = bool(b1 & 0x80)
|
||
opcode = b1 & 0x0F
|
||
masked = bool(b2 & 0x80)
|
||
length = b2 & 0x7F
|
||
if length == 126:
|
||
ext = self._recv_exact(2)
|
||
if ext is None:
|
||
return None
|
||
length = struct.unpack("!H", ext)[0]
|
||
elif length == 127:
|
||
ext = self._recv_exact(8)
|
||
if ext is None:
|
||
return None
|
||
length = struct.unpack("!Q", ext)[0]
|
||
if length > MAX_FRAME:
|
||
self.close(1009, "frame too large")
|
||
return None
|
||
mask = b""
|
||
if masked:
|
||
mask = self._recv_exact(4) or b""
|
||
if len(mask) != 4:
|
||
return None
|
||
elif self.require_masked:
|
||
# RFC 6455:客户端发来的帧必须带掩码,否则属协议错误
|
||
self.close(1002, "unmasked frame from client")
|
||
return None
|
||
payload = self._recv_exact(length) if length else b""
|
||
if payload is None:
|
||
return None
|
||
if masked and payload:
|
||
payload = bytes(byte ^ mask[i % 4] for i, byte in enumerate(payload))
|
||
return opcode, fin, payload
|
||
|
||
def run(self, on_message: Callable[["WSConnection", dict[str, Any]], None],
|
||
on_close: Callable[["WSConnection"], None] | None = None) -> None:
|
||
"""消息循环,阻塞直到对端关闭或出错。"""
|
||
try:
|
||
self.sock.settimeout(None)
|
||
except OSError:
|
||
pass
|
||
try:
|
||
while not self._closed.is_set():
|
||
frame = self._read_frame()
|
||
if frame is None:
|
||
break
|
||
opcode, fin, payload = frame
|
||
if opcode == OP_CLOSE:
|
||
self.send_frame(OP_CLOSE, payload[:2] if len(payload) >= 2 else b"")
|
||
break
|
||
if opcode == OP_PING:
|
||
self.send_frame(OP_PONG, payload)
|
||
continue
|
||
if opcode == OP_PONG:
|
||
continue
|
||
if opcode in (OP_TEXT, OP_BIN):
|
||
if fin:
|
||
data = payload
|
||
else:
|
||
self._frag_op = opcode
|
||
self._frag_data = bytearray(payload)
|
||
continue
|
||
elif opcode == OP_CONT:
|
||
if self._frag_op is None:
|
||
continue
|
||
self._frag_data.extend(payload)
|
||
if len(self._frag_data) > MAX_FRAME:
|
||
self.close(1009, "fragmented message too large")
|
||
return None
|
||
if not fin:
|
||
continue
|
||
data = bytes(self._frag_data)
|
||
self._frag_op = None
|
||
self._frag_data = bytearray()
|
||
else:
|
||
continue
|
||
if not data:
|
||
continue
|
||
try:
|
||
msg = protocol.loads(data)
|
||
except Exception:
|
||
self.send_json({"type": "error", "error": "消息不是合法 JSON", "code": "bad_json"})
|
||
continue
|
||
if isinstance(msg, dict):
|
||
on_message(self, msg)
|
||
finally:
|
||
self._closed.set()
|
||
try:
|
||
self.sock.close()
|
||
except OSError:
|
||
pass
|
||
if on_close is not None:
|
||
try:
|
||
on_close(self)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def handshake(sock: socket.socket, headers: Any) -> WSConnection | None:
|
||
"""完成 HTTP → WebSocket 升级。成功返回 WSConnection。
|
||
|
||
注意:只有当客户端请求了 Sec-WebSocket-Protocol 时才能回该头(RFC 6455)。
|
||
否则浏览器会因为"服务端选择了未请求的子协议"而直接判定握手失败。
|
||
"""
|
||
key = headers.get("Sec-WebSocket-Key")
|
||
if not key:
|
||
try:
|
||
sock.sendall(
|
||
b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
||
)
|
||
except OSError:
|
||
pass
|
||
return None
|
||
version = (headers.get("Sec-WebSocket-Version") or "").strip()
|
||
if version and version != "13":
|
||
# RFC 6455:只支持版本 13,其它版本必须明确拒绝,不能装作握手成功
|
||
try:
|
||
sock.sendall(
|
||
b"HTTP/1.1 426 Upgrade Required\r\nSec-WebSocket-Version: 13\r\n"
|
||
b"Content-Length: 0\r\nConnection: close\r\n\r\n"
|
||
)
|
||
except OSError:
|
||
pass
|
||
return None
|
||
lines = [
|
||
"HTTP/1.1 101 Switching Protocols",
|
||
"Upgrade: websocket",
|
||
"Connection: Upgrade",
|
||
f"Sec-WebSocket-Accept: {accept_key(key.strip())}",
|
||
]
|
||
requested = headers.get("Sec-WebSocket-Protocol")
|
||
if requested:
|
||
offered = [item.strip() for item in requested.split(",") if item.strip()]
|
||
# 只回客户端提供的第一个协议,绝不回它没要求过的值
|
||
if offered:
|
||
lines.append(f"Sec-WebSocket-Protocol: {offered[0]}")
|
||
response = "\r\n".join(lines) + "\r\n\r\n"
|
||
try:
|
||
sock.sendall(response.encode("ascii"))
|
||
except OSError:
|
||
return None
|
||
addr = sock.getpeername() if hasattr(sock, "getpeername") else ("?", 0)
|
||
if not isinstance(addr, tuple):
|
||
addr = ("?", 0)
|
||
return WSConnection(sock, (str(addr[0]), int(addr[1])))
|
||
|
||
|
||
class Hub:
|
||
"""连接注册表 + 广播。"""
|
||
|
||
def __init__(self) -> None:
|
||
self._lock = threading.Lock()
|
||
self._conns: deque[WSConnection] = deque()
|
||
|
||
def add(self, conn: WSConnection) -> None:
|
||
with self._lock:
|
||
self._conns.append(conn)
|
||
|
||
def remove(self, conn: WSConnection) -> None:
|
||
with self._lock:
|
||
try:
|
||
self._conns.remove(conn)
|
||
except ValueError:
|
||
pass
|
||
|
||
def snapshot(self) -> list[WSConnection]:
|
||
with self._lock:
|
||
return [c for c in self._conns if not c.closed]
|
||
|
||
@property
|
||
def count(self) -> int:
|
||
return len(self.snapshot())
|
||
|
||
def broadcast(self, obj: dict[str, Any]) -> int:
|
||
payload = protocol.dumps(obj)
|
||
sent = 0
|
||
for conn in self.snapshot():
|
||
try:
|
||
conn.send_text(payload)
|
||
sent += 1
|
||
except Exception:
|
||
self.remove(conn)
|
||
return sent
|
||
|
||
def close_all(self) -> None:
|
||
for conn in self.snapshot():
|
||
try:
|
||
conn.close(1001, "server shutdown")
|
||
except Exception:
|
||
pass
|
||
with self._lock:
|
||
self._conns.clear()
|