"""backnforth server — static site + WebSocket relay, Python stdlib only. python3 server.py [--no-browser] Serves the therapist page on http://:8000/ and the session relay on ws://:8787. No pip installs, no virtualenv — any Python 3.7+ works, including old machines that will never see a package index. """ import base64 import hashlib import json import os import socket import socketserver import struct import sys import threading import webbrowser from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer HTTP_PORT = 8000 WS_PORT = 8787 _WS_GUID = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11' SESSIONS = {} # session id -> set of live WSHandler LOCK = threading.Lock() _SERVERS = [] def local_ip(): try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(('8.8.8.8', 80)) ip = s.getsockname()[0] s.close() return ip except OSError: try: return socket.gethostbyname(socket.gethostname()) except OSError: return '127.0.0.1' def shutdown_all(): for srv in _SERVERS: try: srv.shutdown() except Exception: pass class WSHandler(socketserver.BaseRequestHandler): """Minimal RFC6455 server: unfragmented text frames, ping/pong, close. ponytail: no fragmentation/binary support — the state payloads are a few hundred bytes of JSON and browsers never fragment at that size. """ def setup(self): self.session_id = None self.send_lock = threading.Lock() def handle(self): if not self._handshake(): return try: while True: msg = self._read_message() if msg is None: break self._dispatch(msg) except (ConnectionError, OSError): pass def finish(self): if self.session_id: with LOCK: SESSIONS.get(self.session_id, set()).discard(self) # ---- relay protocol (same JSON messages as the old websockets server) ---- def _dispatch(self, text): try: data = json.loads(text) except ValueError: return t = data.get('type') if t == 'join': self.session_id = data.get('sessionId') or 'default' with LOCK: SESSIONS.setdefault(self.session_id, set()).add(self) self._send_json({'type': 'joined', 'sessionId': self.session_id}) elif t in ('state', 'patientViewport'): sid = data.get('sessionId') or self.session_id or 'default' out = json.dumps({'type': t, 'payload': data.get('payload')}) with LOCK: peers = list(SESSIONS.get(sid, ())) for peer in peers: if peer is not self: try: peer._send_text(out) except Exception: pass elif t in ('whoami', 'detect_ip'): self._send_json({'type': 'detect_ip', 'ip': local_ip()}) elif t == 'ping': self._send_json({'type': 'pong'}) elif t == 'shutdown': if self.client_address[0] in ('127.0.0.1', '::1', local_ip()): self._send_json({'type': 'shutdown_ack'}) threading.Thread(target=shutdown_all, daemon=True).start() # ---- websocket plumbing ---- def _handshake(self): self.request.settimeout(10) data = b'' while b'\r\n\r\n' not in data: chunk = self.request.recv(4096) if not chunk or len(data) > 65536: return False data += chunk headers = {} for line in data.split(b'\r\n')[1:]: if b':' in line: k, v = line.split(b':', 1) headers[k.strip().lower()] = v.strip() key = headers.get(b'sec-websocket-key') if not key: return False accept = base64.b64encode(hashlib.sha1(key + _WS_GUID).digest()).decode() self.request.sendall(( 'HTTP/1.1 101 Switching Protocols\r\n' 'Upgrade: websocket\r\n' 'Connection: Upgrade\r\n' 'Sec-WebSocket-Accept: ' + accept + '\r\n\r\n' ).encode()) self.request.settimeout(None) return True def _recv_exact(self, n): buf = b'' while len(buf) < n: chunk = self.request.recv(n - len(buf)) if not chunk: return None buf += chunk return buf def _read_message(self): while True: head = self._recv_exact(2) if head is None: return None b1, b2 = head opcode = b1 & 0x0F 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 > 1_000_000: return None mask = self._recv_exact(4) if (b2 & 0x80) else b'\x00\x00\x00\x00' if mask is None: return None payload = self._recv_exact(length) if length else b'' if payload is None: return None payload = bytes(c ^ mask[i % 4] for i, c in enumerate(payload)) if opcode == 0x1: # text try: return payload.decode('utf-8') except UnicodeDecodeError: return None elif opcode == 0x9: # ping -> pong self._send_frame(0xA, payload) elif opcode == 0x8: # close try: self._send_frame(0x8, b'') except Exception: pass return None # anything else (binary/continuation): skip def _send_frame(self, opcode, payload): n = len(payload) head = bytes([0x80 | opcode]) if n < 126: head += bytes([n]) elif n < 65536: head += bytes([126]) + struct.pack('>H', n) else: head += bytes([127]) + struct.pack('>Q', n) with self.send_lock: self.request.sendall(head + payload) def _send_text(self, text): self._send_frame(0x1, text.encode('utf-8')) def _send_json(self, obj): self._send_text(json.dumps(obj)) class _QuietHTTP(SimpleHTTPRequestHandler): def log_message(self, *args): pass def main(): os.chdir(os.path.dirname(os.path.abspath(__file__))) socketserver.ThreadingTCPServer.allow_reuse_address = True ThreadingHTTPServer.allow_reuse_address = True httpd = ThreadingHTTPServer(('0.0.0.0', HTTP_PORT), _QuietHTTP) wsd = socketserver.ThreadingTCPServer(('0.0.0.0', WS_PORT), WSHandler) httpd.daemon_threads = True wsd.daemon_threads = True _SERVERS.extend([httpd, wsd]) ip = local_ip() print('backnforth running:') print(' Therapist page: http://%s:%d/' % (ip, HTTP_PORT)) print(' Session relay: ws://%s:%d' % (ip, WS_PORT)) print('Press Ctrl+C to stop.') threading.Thread(target=wsd.serve_forever, daemon=True).start() if '--no-browser' not in sys.argv: try: webbrowser.open('http://%s:%d/' % (ip, HTTP_PORT)) except Exception: pass try: httpd.serve_forever() except KeyboardInterrupt: print('\nStopped.') if __name__ == '__main__': main()