| Age | Commit message (Collapse) | Author | Files | Lines |
|
Filtering already existed (FTS5 search plus status:/source:/flagged:
and column filters). Sorting and highlighting didn't, at all.
Sorting: o/O cycle and reverse the sort column (status, size, time
taken, method, host, path) applied client-side on top of whatever
order List/Search already returned. refreshTable() reorders m.entries
itself, not just what's rendered - every "act on the selected row" key
handler indexes m.table.Cursor() straight into m.entries with no
indirection, so keeping the two in identical order sidesteps an entire
class of "highlighted row and actual target silently disagree" bugs
rather than updating every one of those call sites.
JSON syntax highlighting (cmd/mitmux/jsoncolor.go): walks the token
stream via json.Decoder.Token() with an explicit stack, not recursive
calls, so depth is bounded by memory rather than Go's call stack for
adversarial nesting. Every string re-escaped via json.Marshal before
writing, which is also why the colorized output is deliberately never
run through sanitizeControl afterward (unlike every other raw-text
view here): JSON's own encoding rules already forbid a literal control
character in a string, so re-marshaling neutralizes one as a side
effect of producing valid JSON - running sanitizeControl on top would
instead corrupt the ANSI codes this adds.
Status-code color-coding (2xx green through 5xx red) does not live in
the history/Intruder tables, despite an initial attempt to put it
there. Confirmed live: bubbles/table v1.0.0 (the newest available)
fits cell text to its column width via go-runewidth's Truncate, which
has no ANSI awareness - it counts every character of a color escape
sequence as real display width. Coloring the Status cell silently
deleted the status text from the row; the width-fitting truncation cut
into the escape sequence itself. styledStatus is used once instead, in
detailView's title, a plain string rendered whole with no width
constraint.
Verified live in tmux against a running daemon: ascending/descending
sort by status across six real entries: pretty-printed JSON confirmed
correctly colored and indented via raw ANSI capture, not just
eyeballed; the detail title's status color confirmed red for a 500
entry the same way; the request tab (never JSON) confirmed unaffected.
|
|
Next item off the "worth considering" list from the Burp/ZAP/Caido gap
research. Mark an entry with 'c' (from the history list or detail
view - no fetch yet, just remembers the ID), then 'c' on a different
entry fetches both and opens a colored unified diff of either side's
request or response, tab to switch between them.
Unified (git-diff style: +/- prefixed lines) rather than Burp's
side-by-side two-pane layout - a two-column view fights terminal width
for anything but a wide window, and unified reuses the same scrollable
viewport pattern already used everywhere else in this TUI rather than
needing new layout machinery. Uses github.com/pmezard/go-difflib
(SequenceMatcher-based, a tested port of Python's difflib) rather than
hand-rolling LCS/Myers diff, which has real edge cases worth not
reinventing. CRLF is normalized to LF before diffing - display-only,
same reasoning as the JSON pretty-printer - so an HTTP/1.1 exact
capture doesn't show every single line as changed purely from an
invisible trailing \r.
Verified live against two real, distinctly different captured POST
requests (different form bodies, different Content-Length): the request
diff correctly isolated exactly the two changed lines with the
unchanged headers shown as context, colors confirmed via raw ANSI
codes in the captured pane output (red 203 for removed, green 42 for
added) rather than assumed from the code, and the response tab showed
a correct independent diff of the two responses (Date header, JSON
body). Also confirmed the "same entry marked twice" path shows a hint
rather than silently doing something confusing.
|
|
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.
|