diff options
| author | srdusr <[email protected]> | 2024-02-14 00:37:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-02-14 00:37:00 +0200 |
| commit | 8d15c2e0b326933f8fc912e3b13f37e78a9bc0b6 (patch) | |
| tree | 567274fe13d0a8aea8356e9edeba33ef778cf20f /internal/proxy/tee.go | |
| parent | f2f0a2135a202e3e15d2a8cbfbd791aad9b04f3a (diff) | |
| download | mitmux-8d15c2e0b326933f8fc912e3b13f37e78a9bc0b6.tar.gz mitmux-8d15c2e0b326933f8fc912e3b13f37e78a9bc0b6.zip | |
History view: SQLite storage, daemon/TUI split over Unix socket
Implements build-order step 3. Adds:
- internal/store: SQLite (WAL, single-writer) history table, raw
request/response blobs plus metadata for the list view.
- internal/proxy: request/response capture wired into forward(). HTTP/1.1
legs are captured byte-exact via a teeConn that records wire bytes as
they're read, taken right after the message is fully drained (so no
manual re-reading/replaying is needed - RoundTrip's own streaming does
the draining). HTTP/2 legs (no meaningful "raw bytes" of their own -
multiplexed, HPACK-compressed framing) are reconstructed instead, and
marked as such in storage.
- internal/ipc: JSON-over-Unix-socket protocol between mitmuxd (owns the
proxy and the DB) and any client - list/get for queries, subscribe for
a live push stream of newly captured entries. Keeps the proxy engine
independent of the UI, per the architecture sketch.
- cmd/mitmux: Bubble Tea TUI - a live-updating history table and a
request/response detail view with raw bytes.
Two real bugs surfaced during testing and got fixed before commit:
1. http.Transport's HTTP/2 auto-dispatch does a literal *tls.Conn type
assertion on the dialed connection; wrapping it in a capturing teeConn
broke that silently, and HTTP/2 framing got parsed as HTTP/1.1 text.
Fixed by dropping http.Transport for the upstream leg entirely in
favor of an explicit per-protocol round trip (see PLAN.md stack note).
2. singleConnListener wrapped the client teeConn *inside* a
closeSignalConn, so ConnContext's type assertion for it silently
failed and HTTP/1.1 client-side capture never activated. Fixed the
wrap order; verified via direct SQLite inspection that request_exact
flips back to 1 and the stored bytes are genuinely wire-exact
(preserved chunked-encoding framing, original header casing/order).
Verified live: plain HTTP, HTTPS H1.1, HTTPS H2, and a POST with a body,
checked against the raw stored bytes directly in SQLite; IPC list/get/
subscribe against a throwaway client; and the TUI driven end-to-end in a
tmux session (list, detail view, tab between request/response, live
update on a new request while sitting on the list).
Diffstat (limited to 'internal/proxy/tee.go')
| -rw-r--r-- | internal/proxy/tee.go | 92 |
1 files changed, 92 insertions, 0 deletions
diff --git a/internal/proxy/tee.go b/internal/proxy/tee.go new file mode 100644 index 0000000..f6a385a --- /dev/null +++ b/internal/proxy/tee.go @@ -0,0 +1,92 @@ +package proxy + +import ( + "context" + "net" + "sync" +) + +// maxCaptureBytes bounds how much of any single request or response +// mitmux buffers for history storage, independent of how much data +// actually flows through the proxy. Proxying itself always streams the +// full body regardless of this limit - only what gets stored is capped, +// so a multi-gigabyte download can't be turned into a memory exhaustion +// vector just because the history view wants to remember it. +const maxCaptureBytes = 10 << 20 // 10 MiB + +// teeConn wraps a net.Conn, recording every byte read off the wire (up +// to maxCaptureBytes) so it can be attributed to a specific request or +// response later. Take returns everything recorded since the last call +// and resets the buffer, so callers must take exactly once per message +// they want attributed correctly - see forward() for why that's safe +// here (call sites synchronize on the request/response boundary itself). +type teeConn struct { + net.Conn + mu sync.Mutex + buf []byte +} + +func newTeeConn(c net.Conn) *teeConn { + return &teeConn{Conn: c} +} + +func (c *teeConn) Read(p []byte) (int, error) { + n, err := c.Conn.Read(p) + if n > 0 { + c.mu.Lock() + if room := maxCaptureBytes - len(c.buf); room > 0 { + end := n + if end > room { + end = room + } + c.buf = append(c.buf, p[:end]...) + } + c.mu.Unlock() + } + return n, err +} + +// Take returns the bytes read since the last Take call (or since the +// connection was created) and resets the buffer. +func (c *teeConn) Take() []byte { + c.mu.Lock() + defer c.mu.Unlock() + out := c.buf + c.buf = nil + return out +} + +// teeListener wraps a net.Listener so every accepted connection is +// tee-captured. +type teeListener struct { + net.Listener +} + +func (l *teeListener) Accept() (net.Conn, error) { + c, err := l.Listener.Accept() + if err != nil { + return nil, err + } + return newTeeConn(c), nil +} + +type contextKey int + +const clientTeeKey contextKey = iota + +// teeConnFromContext returns the teeConn wrapping the client connection +// the current request was read from, as attached via http.Server's +// ConnContext hook. Returns nil for HTTP/2 client connections, which +// aren't tee-captured (see capture.go). +func teeConnFromContext(ctx context.Context) *teeConn { + tc, _ := ctx.Value(clientTeeKey).(*teeConn) + return tc +} + +func withClientTee(ctx context.Context, c net.Conn) context.Context { + tc, ok := c.(*teeConn) + if !ok { + return ctx + } + return context.WithValue(ctx, clientTeeKey, tc) +} |