| Age | Commit message (Collapse) | Author | Files | Lines |
|
Repeater previously had one shared request/response buffer - sending a
new entry to Repeater silently overwrote whatever was already open,
even mid-edit. Replaced the singular reqArea/respView/repeaterScheme/
etc. model fields with a []*repeaterTab slice plus an active index;
'r' now opens a new tab and switches to it, existing tabs stay put.
New keys, all gated to normal mode so they stay inert while typing
(]/[ show up in JSON bodies constantly, and ctrl+w is the textarea's
own delete-word-backward that must still work mid-edit):
] next tab
[ previous tab
ctrl+w close the active tab (falls back to a neighbor, or to the
history list if it was the last one)
Async send results now carry the tab index they belong to, so a slow
send whose response lands after the user has switched tabs (or closed
one) updates the right tab rather than whichever happens to be active
when the result arrives; the status line and response pane only reflect
it live if that tab is still the one being viewed.
Verified live in tmux against a running daemon: opened two tabs from
different history entries, confirmed independent buffers, sent from a
background tab while another was active and confirmed the result routed
to the correct (non-visible) tab, switched with ]/[, closed with ctrl+w
down to zero tabs (falls back to the history list), and confirmed [, ],
and ctrl+w are all correctly inert in insert mode (typed "[a]" literally,
ctrl+w did textarea's word-delete instead of closing the tab).
go build/vet/gofmt/test/mod tidy all clean.
|
|
First of the remaining "worth considering" items. A self-contained
tool ('d' from the history list, not seeded from any entry - this is
for arbitrary snippets, pasted tokens, encoded parameter values) with
a vi-modal input pane and a live output pane that updates on every
keystroke and every transform switch (tab/shift+tab cycles through the
8 transforms).
decoder.go is pure logic, deliberately kept separate from the TUI
wiring so it's directly testable: urlEncodeAll implements strict RFC
3986 percent-encoding (space -> %20) rather than using Go's
url.QueryEscape, whose form-encoding behavior (space -> '+') isn't what
"URL encode" means to a pentester reaching for this tool. Base64 decode
tries standard/URL-safe/padded/unpadded encodings in turn rather than
requiring the user to know which one they're looking at - real pasted
data is as likely to be one as the other. Decode failures return a
visible "(error: ...)" placeholder rather than blanking the output, so
a bad guess at the transform is obviously wrong rather than looking
like nothing happened.
decoder_test.go covers each transform directly, three "this input isn't
valid for this transform" error cases, and a round-trip matrix (all 4
encode/decode pairs against 5 inputs chosen to be awkward for at least
one encoding - spaces, slashes, HTML-special characters, empty string,
embedded newlines) confirming encode-then-decode always recovers the
original.
Single-transform only, not chained/pipelined like Burp's Decoder - v1
scope, tracked in PLAN.md.
Verified live: typed text and watched the output pane update in real
time; confirmed URL-encoding, then cycled to Base64 via tab and watched
it re-encode the same input live; confirmed the active-transform
highlighting via raw ANSI codes in the captured pane; fed invalid input
to Base64 decode and confirmed the error placeholder renders instead of
silently showing stale output; confirmed esc correctly backs out to the
history list.
|
|
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.
|
|
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.
|
|
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.
|
|
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 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.
|