diff options
| author | srdusr <[email protected]> | 2026-06-30 14:52:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2026-06-30 14:52:00 +0200 |
| commit | 384573a2dc5e3b8e2a7bdfe2ce949f2c52ba2c52 (patch) | |
| tree | 13bfe97b1f5bcf90e3fad33d90b522b29f3e439d /internal/ipc/ipc.go | |
| parent | 2ade8c807584bff0b60d6b6f278dbde29b13a5ff (diff) | |
| download | mitmux-384573a2dc5e3b8e2a7bdfe2ce949f2c52ba2c52.tar.gz mitmux-384573a2dc5e3b8e2a7bdfe2ce949f2c52ba2c52.zip | |
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.
Diffstat (limited to 'internal/ipc/ipc.go')
| -rw-r--r-- | internal/ipc/ipc.go | 20 |
1 files changed, 20 insertions, 0 deletions
diff --git a/internal/ipc/ipc.go b/internal/ipc/ipc.go index 68217c2..dc7a1df 100644 --- a/internal/ipc/ipc.go +++ b/internal/ipc/ipc.go @@ -114,6 +114,7 @@ type Response struct { Rules []rules.Rule `json:"rules,omitempty"` // for "rules" ScopeRules []scope.Rule `json:"scope_rules,omitempty"` // for "scope_rules" ClientCerts []clientcert.Cert `json:"client_certs,omitempty"` // for "client_certs" + WSMessages []store.WSMessage `json:"ws_messages,omitempty"` // for "ws_messages" Status *StatusMsg `json:"status,omitempty"` // for "status" // For "import_done": how many entries were actually inserted (a @@ -331,6 +332,25 @@ func (c *Client) Get(id int64) (*EntryDetail, error) { return resp.Detail, nil } +// ListWSMessages returns every WebSocket frame captured for entryID's +// connection, in the order they were sent - empty (not an error) if the +// entry wasn't a WebSocket upgrade or nothing was captured. +func (c *Client) ListWSMessages(entryID int64) ([]store.WSMessage, error) { + c.mu.Lock() + defer c.mu.Unlock() + if err := c.enc.Encode(Request{Type: "ws_messages", ID: entryID}); err != nil { + return nil, err + } + var resp Response + if err := c.dec.Decode(&resp); err != nil { + return nil, err + } + if resp.Type == "error" { + return nil, errors.New(resp.Error) + } + return resp.WSMessages, nil +} + // Repeat sends raw to scheme://host exactly as given (no re-serialization, // no header injection) and returns the resulting entry, including the raw // response bytes. The exchange is also recorded to history. |