srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/cmd/mitmuxd
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-02-14 00:37:00 +0200
committersrdusr <[email protected]>2024-02-14 00:37:00 +0200
commit8d15c2e0b326933f8fc912e3b13f37e78a9bc0b6 (patch)
tree567274fe13d0a8aea8356e9edeba33ef778cf20f /cmd/mitmuxd
parentf2f0a2135a202e3e15d2a8cbfbd791aad9b04f3a (diff)
downloadmitmux-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.go52
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()
}
}