TCP / app.py
euler314's picture
Upload 4 files
66a180a verified
Raw
History Blame Contribute Delete
3.42 kB
#!/usr/bin/env python3
import asyncio
import json
import re
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import PlainTextResponse
app = FastAPI()
CODE_RE = re.compile(r"^\d{9}$")
# pending[code] = {"sender": ws|None, "receiver": ws|None, "event": asyncio.Event()}
PENDING = {}
PENDING_LOCK = asyncio.Lock()
@app.get("/")
async def root():
return PlainTextResponse("ok")
async def register(code, role, ws):
async with PENDING_LOCK:
entry = PENDING.get(code)
if entry is None:
entry = {"sender": None, "receiver": None, "event": asyncio.Event()}
PENDING[code] = entry
if entry.get(role) is not None:
return None, "role already connected"
entry[role] = ws
if entry.get("sender") is not None and entry.get("receiver") is not None:
entry["event"].set()
return entry, None
async def unregister(code, role, ws):
async with PENDING_LOCK:
entry = PENDING.get(code)
if entry is None:
return
if entry.get(role) is ws:
entry[role] = None
if entry.get("sender") is None and entry.get("receiver") is None:
PENDING.pop(code, None)
async def forward(src, dst):
try:
while True:
msg = await src.receive()
if msg.get("type") == "websocket.disconnect":
break
if msg.get("bytes") is not None:
await dst.send_bytes(msg["bytes"])
elif msg.get("text") is not None:
await dst.send_text(msg["text"])
except WebSocketDisconnect:
pass
except Exception:
pass
@app.websocket("/ws")
async def ws_relay(ws: WebSocket):
await ws.accept()
code = None
role = None
entry = None
try:
raw = await ws.receive_text()
try:
payload = json.loads(raw)
except json.JSONDecodeError:
await ws.send_text(json.dumps({"error": "invalid json"}))
return
role = payload.get("role")
code = payload.get("code")
if role not in ("sender", "receiver"):
await ws.send_text(json.dumps({"error": "invalid role"}))
return
if not isinstance(code, str) or not CODE_RE.match(code):
await ws.send_text(json.dumps({"error": "invalid code"}))
return
entry, err = await register(code, role, ws)
if err:
await ws.send_text(json.dumps({"error": err}))
return
await ws.send_text(json.dumps({"status": "waiting"}))
await entry["event"].wait()
other = entry["receiver"] if role == "sender" else entry["sender"]
if other is None:
await ws.send_text(json.dumps({"error": "peer missing"}))
return
await ws.send_text(json.dumps({"status": "paired"}))
await other.send_text(json.dumps({"status": "paired"}))
task_a = asyncio.create_task(forward(ws, other))
task_b = asyncio.create_task(forward(other, ws))
done, pending = await asyncio.wait(
{task_a, task_b}, return_when=asyncio.FIRST_COMPLETED
)
for task in pending:
task.cancel()
finally:
if code and role:
await unregister(code, role, ws)
try:
await ws.close()
except Exception:
pass