Files
jbjf6666 85e9a53750 docs: 新增 Windows 移植交接文档
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 分离等)
- 验收基线(复用现有测试套件)与建议的工作顺序
2026-10-16 01:05:09 +08:00

327 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""极简 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()