| Age | Commit message (Collapse) | Author | Files | Lines |
|
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).
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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).
|
|
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.
|
|
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.
|