108 lines
4.3 KiB
Python
108 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
||
"""CyberGril R5 · WSS TLS bridge (PROPOSAL — 拟议材料,未在任何服务器执行).
|
||
|
||
背景(docs/rework/hel879/unified-review/r5-facts-20261027.md E2/E4):.12 的
|
||
LiveKit 7880 是纯 HTTP/WS 且通用 worker/8090 演示都依赖它保持明文,不能改
|
||
TLS;HTTPS 页面直连 ws:// 又是混合内容。本脚本在**新端口**终结 TLS 并把字节
|
||
流透明转发给本机 7880,使浏览器可用 ``wss://192.168.200.12:<port>`` 接入
|
||
LiveKit 信令,零新依赖(asyncio + ssl 标准库),不触碰既有服务。
|
||
|
||
- 监听(拟议):0.0.0.0:8363,证书 = 与 8361 同一张自签证书
|
||
(SAN=IP:192.168.200.12,存 /opt/cybergril-voice-certs/,600)。
|
||
- 上游:127.0.0.1:7880(仅回环,不暴露第二明文入口)。
|
||
- voice.yaml:livekit_url 改 wss://192.168.200.12:8363;
|
||
livekit_internal 保持 http://127.0.0.1:7880。
|
||
- 实现为透明 TCP TLS 终结(不解析 WebSocket/HTTP),LiveKit 信令是 7880 上的
|
||
HTTP/WebSocket,字节级转发即协议无关;WebRTC 媒体面(UDP/7881)不经本桥,
|
||
由 WebRTC 自带 DTLS/SRTP 加密。
|
||
- 每设备分别打开 https://192.168.200.12:8361 与 :8363,验证证书例外和实际
|
||
连接;不推断例外缓存范围。禁止用 --ignore-certificate-errors 冒充验收。
|
||
|
||
部署(获批后)示例:
|
||
python3 wss_bridge.example.py --listen 0.0.0.0:8363 \
|
||
--upstream 127.0.0.1:7880 \
|
||
--cert /opt/cybergril-voice-certs/cybergril-voice.pem \
|
||
--key /opt/cybergril-voice-certs/cybergril-voice.key
|
||
|
||
回滚:停本进程即可(独立进程,不影响 LiveKit/通用 worker/8090)。
|
||
|
||
本地冒烟(零网络出站):见
|
||
docs/rework/hel879/unified-review/r5-facts-20261027.md 与
|
||
docs/rework/hel879/unified-review/wss-bridge-smoke.log(一次性本地证书 +
|
||
dummy 上游回环验证字节穿透与 TLS 握手)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import asyncio
|
||
import contextlib
|
||
import ssl
|
||
import sys
|
||
|
||
|
||
async def _pipe(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
||
try:
|
||
while True:
|
||
data = await reader.read(65536)
|
||
if not data:
|
||
break
|
||
writer.write(data)
|
||
await writer.drain()
|
||
except (ConnectionError, asyncio.CancelledError, ssl.SSLError):
|
||
pass
|
||
finally:
|
||
with contextlib.suppress(Exception):
|
||
writer.close()
|
||
|
||
|
||
async def _handle(
|
||
downstream: tuple[asyncio.StreamReader, asyncio.StreamWriter],
|
||
upstream_host: str,
|
||
upstream_port: int,
|
||
) -> None:
|
||
down_reader, down_writer = downstream
|
||
try:
|
||
up_reader, up_writer = await asyncio.open_connection(upstream_host, upstream_port)
|
||
except (ConnectionError, OSError):
|
||
down_writer.close()
|
||
return
|
||
await asyncio.gather(
|
||
_pipe(down_reader, up_writer),
|
||
_pipe(up_reader, down_writer),
|
||
return_exceptions=True,
|
||
)
|
||
|
||
|
||
async def main(argv: list[str] | None = None) -> int:
|
||
parser = argparse.ArgumentParser(description="TLS->plain TCP bridge (CyberGril R5 WSS proposal)")
|
||
parser.add_argument("--listen", default="0.0.0.0:8363", help="host:port to terminate TLS on")
|
||
parser.add_argument("--upstream", default="127.0.0.1:7880", help="plain upstream host:port")
|
||
parser.add_argument("--cert", required=True, help="TLS certificate PEM path")
|
||
parser.add_argument("--key", required=True, help="TLS private key path")
|
||
args = parser.parse_args(argv)
|
||
|
||
listen_host, _, listen_port = args.listen.rpartition(":")
|
||
up_host, _, up_port = args.upstream.rpartition(":")
|
||
if not listen_host or not up_host:
|
||
parser.error("listen/upstream must be host:port")
|
||
|
||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||
ctx.load_cert_chain(args.cert, args.key)
|
||
|
||
async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
||
await _handle((reader, writer), up_host, int(up_port))
|
||
|
||
server = await asyncio.start_server(
|
||
handler, listen_host or "0.0.0.0", int(listen_port), ssl=ctx
|
||
)
|
||
addrs = ", ".join(str(s.getsockname()) for s in server.sockets or [])
|
||
print(f"wss-bridge(PROPOSAL) tls {addrs} -> {up_host}:{up_port}", file=sys.stderr)
|
||
async with server:
|
||
await server.serve_forever()
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(asyncio.run(main()))
|