| Age | Commit message (Collapse) | Author | Files | Lines |
|
Adds internal/clientcert: a cert/key pair matched to hosts by the same
substring-or-regex pattern model as scope.Rule, so mitmux can present
a client certificate on an upstream TLS handshake that requires one -
the previous behavior was a hard handshake failure with no way to
authenticate. Wired into both places mitmux dials an https:// upstream
over its own TLS client connection: proxy.go's handleConnect (live
proxied traffic) and repeat.go's dialForRepeat (Repeater/Intruder
resends), both through a new Server.clientCertFor(host) helper.
Stored in a new client_certs table, mirroring the existing scope_rules
persistence pattern. The TUI (`t` from history) is add-only like
scope, for the same reason: delete and re-add covers changing
anything, and it's a rarely-touched, low-cardinality list. The add
form takes cert/key file paths and reads them once at save time - PEM
content, not the path, is what's stored and later presented, so a
cert keeps working even if the original file moves afterward.
Verified live against a real mutual-TLS-requiring origin server:
without a matching cert the handshake correctly fails; with one
configured, the origin receives it and the request succeeds; toggling
it off reproduces the failure, confirming the enable/disable path
works end to end.
|
|
internal/proxy/tee.go's teeConn silently drops bytes past
maxCaptureBytes (10 MiB) but callers unconditionally marked the result
"exact" anyway. Confirmed live: proxying a 15 MiB response worked
correctly end-to-end (the client got the full, real 15 MiB - proxying
itself is unbounded, only storage is capped), but the stored history
entry was exactly 10485760 bytes with response_exact=1 still set. For a
tool whose core value proposition is "raw bytes are the source of
truth," a silently truncated capture presented as complete could hide
the very evidence a smuggling or parser-differential investigation is
looking for in the tail of a large body - and give false confidence
that it isn't there.
teeConn.Take() now returns (data, truncated) instead of just data;
truncated is true whenever a Read had to drop bytes because the buffer
was already at cap. Every caller (proxy.go's forward() on both the
request and response side, repeat.go's sendRaw for Repeater/Intruder)
now folds truncated into exact - a truncated capture is never marked
exact - and additionally threads a distinct RequestTruncated/
ResponseTruncated bool through store.Entry, ipc.EntryDetail, and the
TUI, since "truncated" and "reconstructed" (HTTP/2, which never had
wire-exact bytes to begin with) are different situations worth telling
apart: a truncated capture is still real wire bytes, just incomplete,
not a synthesized reconstruction. The detail/repeater views now show
"truncated (hit capture size limit)" specifically rather than lumping
it in with "reconstructed", which would have implied more
transformation happened than actually did.
Schema: history gains request_truncated/response_truncated columns via
the same ALTER-TABLE-and-ignore-duplicate-column pattern already used
for source/flagged, so existing databases upgrade in place.
Verified live: a target server returning a 15 MiB body (over the 10 MiB
cap) proxied through cleanly - full body reached the client - while the
stored entry shows length=10485760, response_exact=0,
response_truncated=1 (previously would have shown response_exact=1);
the TUI's Detail view correctly displays "Response (10485760 bytes,
truncated (hit capture size limit))" instead of "exact".
go build/vet/gofmt/test/mod tidy all clean.
|
|
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).
|
|
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 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.
|