srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal
AgeCommit message (Collapse)AuthorFilesLines
2026-04-02Per-OS CA install instructions (mitmuxd -install-ca)srdusr2-0/+196
Trusting the CA was previously "import ca.pem into whatever's making the requests" with no further help. -install-ca generates the CA if needed and prints copy-pasteable, OS-specific steps, then exits without starting the proxy. Deliberately instructions-only, never auto-executing anything: Linux trust-store tooling varies enough across distros (trust vs update-ca-trust vs update-ca-certificates) that guessing wrong and running the wrong command unattended is worse than asking, and installing a root CA is a system-wide trust change affecting every TLS connection on the machine, not just mitmux's own traffic - running the printed command themselves keeps the user in control of that. internal/ca/install.go: InstallInstructions(goos, caPath) dispatches by OS. Linux detects trust (p11-kit - Arch, also on Fedora) / update-ca-trust (RHEL/Fedora/CentOS) / update-ca-certificates (Debian/Ubuntu/Gentoo) via PATH lookup and prints whichever is actually present, plus separate certutil/NSS instructions for Firefox/Chrome's own certificate store (which doesn't always follow the system trust store on Linux). macOS (security add-trusted-cert) and Windows (certutil -addstore / Import-Certificate) are implemented from each platform's standard documented tooling but not verified live - no macOS/Windows machine was available to test against, unlike Linux. commandExists is a package var (not a direct exec.LookPath call) so tests can fake which tools are "present" and exercise every detection branch deterministically, independent of what's actually installed on whatever machine runs `go test`. Verified live: built mitmuxd, ran -install-ca against a throwaway CA dir on this (Arch Linux) machine - correctly detected `trust` and `certutil` on PATH and printed accurate commands, confirmed the CA files were actually generated, confirmed no proxy/daemon process was left running (exits immediately after printing), and confirmed running it a second time reuses the existing CA (identical file hash) rather than regenerating. go build/vet/gofmt/test/mod tidy all clean.
2026-03-27Intruder payload processing and grep-match/grep-extractsrdusr2-15/+61
Payload processing: an optional case rule (upper/lower) and an optional encode rule (URL/Base64/Hex/HTML) applied to every payload line before it's substituted into the request, cycled with 'c'/'e'. Case always runs before encode - folding an already-encoded value would corrupt it (e.g. uppercasing Base64 padding). Applied entirely client-side in startIntrude() (payload_rules.go): a pure string transform with no proxy-side state, so it needs no protocol changes and reuses the Decoder's own urlEncodeAll. Grep-match/grep-extract: two optional Go regexps, edited with 'm'/'v' using the same modal edit-buffer pattern as the history list's '/' search (enter validates-and-commits, esc reverts to the last-confirmed pattern, an unparseable regexp is rejected with an error rather than silently accepted). Evaluated server-side, in internal/ipc/server.go's "intrude" handler, against each result's actual entry.ResponseRaw - that's where the real response bytes already are, and it's how Burp's own grep options work (matched against the real response, not a client-refetched copy). Grep-match flags a result (new Match column); grep-extract captures the first submatch, or the whole match if the pattern has no capturing group (new Extract column). Both patterns are compiled once before the attack starts and apply for that run only, not retroactively if changed mid-attack. All four new keys (c/e/m/v) are gated to normal mode, checked in the view's outer key switch before ever reaching the template/payloads vi-textareas - otherwise they'd be either untypeable letters or steal keystrokes mid-edit. Same discipline as the Repeater tab keys. internal/ipc: Request gained GrepMatch/GrepExtract string fields (for "intrude"), IntrudeResultMsg gained GrepMatch bool/GrepExtract string, and the client Intrude() helper takes the two pattern strings as new trailing parameters. Verified live in tmux against a running daemon and real httpbin.org traffic: built a template with a §marked§ query param, payloads 1/2/3, grep-match `"id": "2"` and grep-extract `"id": "([0-9]+)"`, ran the attack and confirmed the Match column flagged only the payload=2 row and Extract correctly pulled 1/2/3 from each response respectively; cycled case/encode through all states; confirmed an invalid regexp (`[abc`) is rejected with a visible error and esc correctly reverts to the last-confirmed pattern instead of committing the invalid one. (Also confirmed, incidentally: a batch of vi normal-mode two-key commands like "gg"/"dd" sent as one multi-character tmux send-keys argument doesn't reliably reach the app as separate keystrokes - a tmux scripting artifact, not a bug in the vi-mode implementation, which works correctly when each key is sent as its own event, as any real keypress would be.) go build/vet/gofmt/test/mod tidy all clean.
2026-02-17Flagged marker for history entriessrdusr4-11/+103
Last of the "should build soon" items from the Burp/ZAP/Caido gap research - Burp's row highlighting and Caido's Findings both serve the same real workflow: mark something interesting mid-engagement, revisit later. Scoped to a boolean flag (★) rather than full free-text notes/comments, which would need their own text-input overlay for comparatively modest extra value over a simple marker - tracked as a real follow-up in PLAN.md, not dropped silently. internal/store: history gains a flagged column (migrated in for existing databases the same way source was) plus Store.SetFlagged and Summary/Entry.Flagged. Search's structured-filter layer (added last commit for status:/source:) gains flagged:true/false alongside them - extractStructured already existed for exactly this kind of "pull it out before it reaches FTS5" filter. internal/ipc gains a "set_flagged" request. cmd/mitmux: 'f' toggles the flag on the selected history row (applied optimistically to local state, persisted async - a drift between local and server state on failure is an acceptable trade-off for a marker this low-stakes), shown as a ★ column in the list and in the detail view's title. store_test.go covers the flagged: parsing (true/false spellings, and a "looks like it but isn't" case - flagged:maybe - falling through as literal search text, matching the existing pattern for status:). Verified live: toggling 'f' shows the star immediately, flagged:true correctly filtered to just that entry, and a direct SQLite check confirmed the flag actually persisted to the database (flagged=1), not just reflected in local UI state.
2026-02-16Structured search filters: status:, source:srdusr2-15/+194
Closes another top item from the Burp/ZAP/Caido gap research: status- code and MIME/type filtering alongside free text is used constantly in practice (Caido's HTTPQL, Burp's proxy history filter). Scoped to status and source for now - method: already works today via FTS5's own method column (a plain text match on "POST" is effectively exact for a short alphanumeric token), so it didn't need special handling. status_code isn't a text column FTS5 can index, and doesn't benefit from full-text matching anyway (it's a numeric comparison, not a word search), so extractStructured pulls status:/source: tokens out of the query before it reaches FTS5 and turns them into real parameterized SQL predicates against history's typed columns: status:404 (exact), status:>=400 / status:!=200 (comparison operators), status:4xx (also 2xx/3xx/5xx - the shorthand people actually reach for: "show me the errors"), source:repeater/intruder/proxy. Whatever text remains after extraction still goes through the existing FTS5 path, so "admin status:200" correctly ANDs a real full-text match with a real status predicate in one query. When nothing remains (pure "status:4xx"), Search skips the FTS5 join entirely and queries history directly. store_test.go covers the parsing (exact/operator/range/source, combined with free text, and two "looks like it but isn't" cases - status:banana and the malformed 4-digit status:4004 - to confirm they fall through as literal search text instead of being misparsed). Verified live against real varied traffic (status 200/404/500 requests plus a POST with an "admin" body) - status:4xx matched only the 404; status:>=400 matched both 404 and 500; "admin status:200" correctly matched only the POST and excluded the other unrelated 200; source:proxy matched everything captured so far. All against the actual SQL execution path, not just the pure parsing function.
2026-02-05Status bar and help screensrdusr3-7/+50
Baseline TUI UX that should exist regardless of feature parity - flagged directly by the Burp/ZAP/Caido comparison research as missing. A persistent one-line status bar (proxy address, live request count, current view) is now appended to every screen. Fetched once at startup via a new "status" IPC request (internal/store gains Store.Count(); internal/ipc gains StatusMsg plus a daemon-side handler reading proxy.Server.Addr through ipc.NewServer's new proxyAddr parameter), then kept approximately live by incrementing locally on each "new" subscribe push rather than re-querying every time. '?' opens a full keybinding reference from every view, gated so it never shadows literal text entry - it's a no-op while typing in the search box, a rule form field, or (checked via viTextarea.Mode()) insert-mode text in Repeater/Intruder, where a URL query string literally starting with '?' is completely ordinary input. Any key dismisses it and returns to whichever view opened it. Every existing height calculation (table, viewport, textarea panes) had to shrink by one line to make room for the status bar without pushing content off-screen - done once via a shared `h := msg.Height-1` in the WindowSizeMsg handler rather than touching each call site individually. Verified live: status bar shows the real proxy address and updates its count after a live-captured request; '?' renders the full reference from the history list; dismissing returns to the correct prior view; and specifically confirmed '?' still types literally (tested typing "?foo=bar" into a Repeater request body in insert mode) rather than being swallowed by the help shortcut.
2026-02-03Intruder-equivalent: Sniper attacks with § markerssrdusr5-15/+383
Implements build-order step 7, the last (optional) item. Scoped to Sniper only - one payload set, one §-marked position fuzzed at a time, others held at their base value - since that covers most real Intruder usage; battering ram / pitchfork / cluster bomb aren't implemented. Sequential sending, capped at 1000 generated requests as a fixed safety limit. internal/proxy: repeat.go's Repeat() is refactored into a shared sendRaw(..., source) primitive so Intrude can reuse the exact same raw-byte send/record path with source="intruder" instead of duplicating it. intrude.go adds ParseMarkers/buildRequest (marker parsing and payload substitution, covered by intrude_test.go - this is fiddly byte-splicing logic, worth locking down with real tests rather than trusting it by inspection) and Intrude(), which walks positions × payloads calling sendRaw and streaming each result through a callback. internal/ipc gains a dedicated streaming "intrude" connection (same shape as Subscribe, but blocking sends rather than drop-on-slow- consumer - each result is the attack's actual data, not a notification). cmd/mitmux gains an Intruder view: editable request template (ctrl+p inserts a § marker at the cursor - typing § directly also works, ctrl+p just doesn't require a keyboard layout that can produce it), editable payload list, and a live results table wired to the existing detail view (selecting a row and hitting enter opens the full request/response for that specific attack request). Verified live against real external traffic: a Sniper attack against httpbin.org/status/§200§ with payloads 200/404/500 produced exactly the three corresponding real status codes back (not a canned/local result), confirmed the three requests landed in history tagged source="intruder" with the § markers correctly stripped from what was actually sent, and confirmed opening a result row's full detail from the results table. This closes out the full build order from PLAN.md (steps 1-7).
2024-09-23Match-and-replace: header rewrite rulessrdusr5-2/+344
Implements build-order step 6, scoped to headers only for this pass - see PLAN.md for why bodies are a separate problem (request-body capture currently depends on streaming straight through, which a body-rewriting rule would have to interrupt; deciding what "exact" means for a rule-modified request needs its own pass, not a rushed add-on to this one). internal/rules: Rule type and ApplyHeaders, which serializes a Header map to a raw "Name: value\r\n" block, runs enabled rules' match/replace over that text, and reparses it - operating on text rather than per-value substitution is what lets a rule add or remove a header, not just rewrite one, matching how Burp's header match/replace works. Invalid rule output (bad regex, unparseable result) leaves the header map untouched rather than corrupting the request. internal/store: rules table + CRUD. internal/proxy: forward() fetches enabled rules for each scope and applies them to outReq.Header / resp.Header, positioned so the existing capture/history pipeline is untouched - request_raw keeps showing what the client actually sent and response_raw what the origin actually sent, while the wire itself reflects the rules. Deliberate split: match-and-replace transforms traffic, it doesn't rewrite the audit trail. internal/ipc gains rules_list/rules_save/rules_delete/rules_toggle. cmd/mitmux gains a rules view ('m' from history) with add/edit/delete/toggle and a small form (name, match, replace, scope, regex). Verified live against real external traffic, not just local echoes: a request-scope rule rewriting User-Agent, confirmed via httpbin.org's own header echo that the origin received the rewritten value while curl sent the real one; a response-scope rule rewriting the Server header, confirmed the client actually received the rewritten value; disabling a rule confirmed via a follow-up request that it stops applying; and throughout, history continued showing the pre-rule original on both sides, confirming the capture/transform split holds.
2024-09-19Fix hang and data-race bugs found while re-verifying steps 1-5srdusr5-13/+62
Audited every file in the proxy/store/ipc/TUI stack before starting step 6, per request. Found and fixed three real bugs in already-shipped code, all confirmed with live tests (including a race-detector build) rather than just read: 1. No timeout covered the write-request/read-response phase of an upstream exchange, in either the main proxy path (roundTripH1/ roundTripH2) or Repeater - only the dial itself was bounded. A server that accepted the connection and then never finished responding hung the request forever. Fixed with conn.SetDeadline after a successful dial in both forward() and Repeat() (new upstreamTimeout constant, 60s). Verified against a real hung TCP listener: the daemon returned a clean "i/o timeout" error at exactly 60s instead of hanging. 2. ipc.Client shared one connection/encoder/decoder with no locking. Bubble Tea dispatches each request as its own goroutine, and viewList's 'r' key doesn't change mode while its loadDetail call is in flight - pressing it again (or 'enter' on another row) before the first response arrives calls Get/List/Repeat concurrently on the same connection, which can interleave JSON on the wire or hand one call another's response. Fixed with a mutex serializing round trips. Stress-tested with rapid overlapping key input against a -race build of both binaries: no warnings, no corruption. 3. The IPC "subscribe" handler only noticed a disconnected client when the next broadcast's Encode failed - a subscriber that quit while the daemon was otherwise idle leaked its goroutine and channel indefinitely. Fixed by reading the connection in the background too, so disconnection is detected immediately regardless of traffic. Also removed a dead, misleading parameter: captureResponse took a *teeConn it was never actually called with (the exact-capture path is handled directly in forward()), so the branch using it was unreachable. Re-verified all five prior steps end-to-end against a fresh build: plain HTTP, HTTPS H1.1/H2/untrusted-CA-rejection, exact vs reconstructed capture flags cross-checked directly in SQLite, Repeater over both HTTP and HTTPS, and search (plain text, dotted domains, hyphenated terms, column filters) - all correct.
2024-09-14Search/filter: FTS5 index over historysrdusr3-5/+150
Implements build-order step 5. internal/store gains an FTS5 virtual table (history_fts) kept in sync with every Insert in the same transaction, indexing method/host/path plus the full raw request and response text - so search covers headers and bodies, not just metadata. Store.Search ranks by bm25 relevance. internal/ipc's existing "list" request grows an optional query field rather than a new message type. cmd/mitmux gets an inline '/' filter on the history view (bubbles/ textinput), esc to clear; live entries arriving while a filter is active are held back with a "+N new" indicator rather than guessed at, since FTS match can't be evaluated against a bare Summary. Two real bugs found via testing against the actual sqlite3 CLI, not assumed from docs: 1. This SQLite build doesn't support MATCH/bm25() against an aliased FTS5 table ("no such column") - only the literal table name resolves. Fixed by leaving history_fts unaliased in the JOIN. 2. FTS5's query grammar treats a wide range of punctuation as syntax, not literal characters - confirmed '.', '-', '/', '@', '(', ')' all produce parse errors (or worse, silently different results, as hyphens get misparsed as column-filter syntax) in an unquoted bareword. Since that covers the most common things people search proxy history for (domains, paths, hyphenated headers, IPs), this would have made the feature fail by default for its primary use case. Fixed with prepareFTSQuery: quote every plain token as an FTS5 phrase (syntactically valid regardless of content) while still recognizing AND/OR/NOT and column:value filters. Also caught, mid-testing, that a query fix wasn't taking effect - traced to the daemon still running an old `go run` build from before the fix while only the TUI had been restarted; not a code bug, but a reminder to restart both. Verified live end-to-end: plain-text search matching header/body/JSON content, a previously-failing dotted-domain search now returning exactly the right single match, a hyphen/host:-filter case, boolean-free numeric search, filter-clear returning to the unfiltered list, and the pending- count indicator when new traffic arrives mid-filter.
2024-08-29Repeater: raw-byte send/resendsrdusr5-26/+220
Implements build-order step 4, the feature the plan calls out as used daily. internal/proxy/repeat.go adds Server.Repeat(scheme, host, raw): dials fresh (HTTP/1.1-only - raw edited text has no equivalent in HTTP/2's binary framing), writes raw exactly as given with no framing correction or header injection, and captures the exact response bytes. This is deliberately separate from forward()'s parsed-*http.Request path since Repeater's entire point is letting a malformed/edited request reach the wire unmodified. Repeater sends are recorded to the same history table as proxy traffic (added a "source" column: "proxy" vs "repeater") so they show up in the unified history view and the live subscribe stream, not a separate silo. internal/ipc gains a "repeat" request/response pair; cmd/mitmuxd wires proxy.Server into ipc.NewServer via a small Repeater interface so the daemon keeps owning all network I/O and the TUI stays a thin client. cmd/mitmux gains a repeater view (bubbles/textarea for the editable raw request, a read-only viewport for the response), reachable with 'r' from either the list or detail view, ctrl+r to send. One real bug found via testing: bubbles/textarea only understands LF, but HTTP/1.1 requires CRLF, so loading raw bytes straight into it split each line in two on render. Fixed by normalizing CRLF<->LF at the editor boundary only (load: strip \r; send: restore it) - documented as a narrow, known trade-off for bodies with their own embedded LF line breaks, which is the cost of being able to edit raw HTTP as text at all. Verified live: edited and sent a plain-HTTP repeater request (confirmed in SQLite that the edit - including an intentional extra blank line from imprecise cursor navigation during testing - went out completely unmodified, which is the correct behavior: mitmux must never "fix" what the user typed), and sent an HTTPS repeater request against a freshly captured entry, both getting real 200 responses with exact response bytes back.
2024-02-14History view: SQLite storage, daemon/TUI split over Unix socketsrdusr6-71/+821
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).
2024-01-27TLS interception: per-host leaf certs, terminate-and-resign MITM, native HTTP/2srdusr2-29/+239
Implements build-order step 2. CA gains LeafFor(host), signing and caching per-host leaf certificates on demand. The proxy's CONNECT handler now terminates TLS with the client using a matching leaf cert instead of tunneling raw bytes, and forwards each request upstream over its own independently negotiated TLS connection. Client-side and upstream-side ALPN are negotiated separately rather than one being forced to mirror the other: an http.Transport configured via http2.ConfigureTransport auto-bridges HTTP/1.1 and HTTP/2 on each side independently, so e.g. an HTTP/1.1-only client reaching an HTTP/2-preferring origin still works instead of failing the handshake (caught by testing curl --http1.1 against example.com before this fix). Verified live: plain HTTP passthrough, HTTPS with default (H2) and forced HTTP/1.1 clients, and that requests without the mitmux CA trusted are correctly rejected.
2024-01-16Scaffold mitmux: proxy daemon, CA generation, HTTP/CONNECT passthroughsrdusr2-0/+306
Implements build-order step 1: headless proxy daemon (mitmuxd) with plaintext HTTP passthrough and raw CONNECT tunneling, plus root CA generation/persistence for later TLS interception. Verified live against real HTTP and HTTPS requests through the proxy.