From 384573a2dc5e3b8e2a7bdfe2ce949f2c52ba2c52 Mon Sep 17 00:00:00 2001 From: srdusr <99972264+srdusr@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:52:00 +0200 Subject: WebSocket interception The last "known limitation": a ws://wss:// connection stops being one-shot request/response the instant its 101 Switching Protocols lands, and forward()'s normal write-response-then-record flow has no way to represent that. Scoped to HTTP/1.1 client legs (HTTP/2 can't be hijacked for raw post-response access the way HTTP/1.1 can, and browsers open a dedicated HTTP/1.1 connection for WebSocket regardless of the surrounding page's protocol, so this isn't a real-world gap). internal/proxy/websocket.go decodes each RFC 6455 frame's opcode and payload for capture while relaying the exact same raw bytes it read unmodified - this is capture, not tampering, matching the rest of the codebase's raw-bytes-as-source-of-truth stance. One row per frame, not per reassembled message (fragmentation is rare in real-world WebSocket traffic; not worth buffering an unbounded number of pending fragments to handle it). forward() branches on a matching 101 into handleWebSocketUpgrade, which hijacks the client connection, relays the handshake response raw, records the upgrade request/response to history normally, then relays frames bidirectionally into a new ws_messages table - reachable from the TUI's detail view via `w`. Found and fixed two real bugs by actually driving a WebSocket connection through a running daemon, not by reading the code: stripHopByHop was deleting Connection/Upgrade from every outgoing request (correct for an ordinary request per RFC 7230, catastrophic for one asking to upgrade - every WebSocket attempt silently became a 426); and the relay tore the whole connection down the instant either side saw a close frame, before the peer's own close-frame reply could be relayed back, producing an abrupt EOF instead of a clean close. Verified live end to end on both paths a real client uses: ws:// (plain HTTP forward-proxying) against a Python websockets echo server, and wss:// (CONNECT-tunneled, TLS-intercepted) against the same server behind TLS - text, binary, and extended-length frames, plus a full close handshake with both directions' close frames present, confirmed via the actual bytes captured in ws_messages. --- internal/ipc/server.go | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'internal/ipc/server.go') diff --git a/internal/ipc/server.go b/internal/ipc/server.go index 4db27b9..378c1a9 100644 --- a/internal/ipc/server.go +++ b/internal/ipc/server.go @@ -143,6 +143,14 @@ func (s *Server) handleConn(conn net.Conn) { } enc.Encode(Response{Type: "get", Detail: detailFromEntry(e)}) + case "ws_messages": + msgs, err := s.db.ListWSMessages(req.ID) + if err != nil { + enc.Encode(Response{Type: "error", Error: err.Error()}) + continue + } + enc.Encode(Response{Type: "ws_messages", WSMessages: msgs}) + case "repeat": if s.repeater == nil { enc.Encode(Response{Type: "error", Error: "repeater not available"}) -- cgit v1.2.3