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 /cmd/mitmuxd | |
| 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 'cmd/mitmuxd')
| -rw-r--r-- | cmd/mitmuxd/main.go | 52 |
1 files changed, 48 insertions, 4 deletions
diff --git a/cmd/mitmuxd/main.go b/cmd/mitmuxd/main.go index f79f2cf..0d7f214 100644 --- a/cmd/mitmuxd/main.go +++ b/cmd/mitmuxd/main.go @@ -1,24 +1,31 @@ // Command mitmuxd is the mitmux headless proxy daemon. It owns the -// listening socket and (in later build steps) the traffic database; a -// TUI or other client attaches separately without interrupting capture. +// listening socket and the history database; a TUI or other client +// attaches separately, over a Unix control socket, without interrupting +// capture. package main import ( "context" "flag" "log" + "net" "os" "os/signal" + "path/filepath" "syscall" "time" "mitmux/internal/ca" + "mitmux/internal/ipc" "mitmux/internal/proxy" + "mitmux/internal/store" ) func main() { listen := flag.String("listen", "127.0.0.1:8080", "proxy listen address") - caDir := flag.String("ca-dir", "", "directory for CA cert/key (default: XDG config dir)") + caDir := flag.String("ca-dir", "", "directory for CA cert/key and history db (default: XDG config dir)") + dbPath := flag.String("db", "", "path to history database (default: <ca-dir>/history.db)") + socketPath := flag.String("socket", "", "control socket path (default: $XDG_RUNTIME_DIR/mitmux.sock, else <ca-dir>/mitmux.sock)") flag.Parse() dir := *caDir @@ -36,7 +43,43 @@ func main() { } log.Printf("CA ready: %s (install %s/ca.pem in your client's trust store to avoid TLS warnings)", root.Cert.Subject.CommonName, dir) - srv := proxy.New(*listen, root) + dbFile := *dbPath + if dbFile == "" { + dbFile = filepath.Join(dir, "history.db") + } + db, err := store.Open(dbFile) + if err != nil { + log.Fatalf("open history db: %v", err) + } + defer db.Close() + log.Printf("history db: %s", dbFile) + + sockFile := *socketPath + if sockFile == "" { + if rt := os.Getenv("XDG_RUNTIME_DIR"); rt != "" { + sockFile = filepath.Join(rt, "mitmux.sock") + } else { + sockFile = filepath.Join(dir, "mitmux.sock") + } + } + os.Remove(sockFile) // stale socket from an unclean shutdown + sockLn, err := net.Listen("unix", sockFile) + if err != nil { + log.Fatalf("listen on control socket %s: %v", sockFile, err) + } + defer os.Remove(sockFile) + log.Printf("control socket: %s", sockFile) + + hub := ipc.NewHub() + ipcSrv := ipc.NewServer(db, hub) + go func() { + if err := ipcSrv.Serve(sockLn); err != nil { + log.Printf("control socket: %v", err) + } + }() + + srv := proxy.New(*listen, root, db) + srv.OnEntry = ipc.LogAndBroadcast(hub) errCh := make(chan error, 1) go func() { @@ -58,5 +101,6 @@ func main() { if err := srv.Shutdown(ctx); err != nil { log.Fatalf("shutdown: %v", err) } + sockLn.Close() } } |