diff options
| author | srdusr <[email protected]> | 2024-08-29 22:03:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-08-29 22:03:00 +0200 |
| commit | 70f675e59ac4bb0e469ca0073f8cc0506b17677e (patch) | |
| tree | e3a525b26dfcf16ce4f9bde97b1408a373d87944 /internal/store/store.go | |
| parent | 8d15c2e0b326933f8fc912e3b13f37e78a9bc0b6 (diff) | |
| download | mitmux-70f675e59ac4bb0e469ca0073f8cc0506b17677e.tar.gz mitmux-70f675e59ac4bb0e469ca0073f8cc0506b17677e.zip | |
Repeater: raw-byte send/resend
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.
Diffstat (limited to 'internal/store/store.go')
| -rw-r--r-- | internal/store/store.go | 26 |
1 files changed, 18 insertions, 8 deletions
diff --git a/internal/store/store.go b/internal/store/store.go index c0219e4..88c9991 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -25,7 +25,8 @@ CREATE TABLE IF NOT EXISTS history ( response_raw BLOB, request_exact INTEGER NOT NULL, response_exact INTEGER NOT NULL, - error TEXT NOT NULL DEFAULT '' + error TEXT NOT NULL DEFAULT '', + source TEXT NOT NULL DEFAULT 'proxy' ); ` @@ -59,6 +60,9 @@ func Open(path string) (*Store, error) { db.Close() return nil, fmt.Errorf("create schema: %w", err) } + // Added after the initial schema; ignore the "duplicate column" error + // on databases that already have it. + db.Exec("ALTER TABLE history ADD COLUMN source TEXT NOT NULL DEFAULT 'proxy'") return &Store{db: db}, nil } @@ -82,6 +86,7 @@ type Entry struct { RequestExact bool // true if RequestRaw is wire-exact, false if reconstructed (e.g. HTTP/2) ResponseExact bool Error string // network/transport error, if the request never got a response + Source string // "proxy" or "repeater" } // Summary is the lightweight metadata used for the history list view - @@ -98,6 +103,7 @@ type Summary struct { ReqSize int RespSize int Error string + Source string } // Insert stores e and returns its assigned ID. @@ -106,13 +112,17 @@ func (s *Store) Insert(e *Entry) (int64, error) { if e.StatusCode != 0 { statusCode = e.StatusCode } + source := e.Source + if source == "" { + source = "proxy" + } res, err := s.db.Exec( `INSERT INTO history (started_at, duration_ms, method, scheme, host, path, status_code, - request_raw, response_raw, request_exact, response_exact, error) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + request_raw, response_raw, request_exact, response_exact, error, source) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, e.StartedAt.UnixMilli(), e.Duration.Milliseconds(), e.Method, e.Scheme, e.Host, e.Path, - statusCode, e.RequestRaw, e.ResponseRaw, boolToInt(e.RequestExact), boolToInt(e.ResponseExact), e.Error, + statusCode, e.RequestRaw, e.ResponseRaw, boolToInt(e.RequestExact), boolToInt(e.ResponseExact), e.Error, source, ) if err != nil { return 0, fmt.Errorf("insert history entry: %w", err) @@ -131,7 +141,7 @@ func (s *Store) List(limit int, beforeID int64) ([]Summary, error) { } rows, err := s.db.Query( `SELECT id, started_at, duration_ms, method, scheme, host, path, - COALESCE(status_code, 0), length(request_raw), COALESCE(length(response_raw), 0), error + COALESCE(status_code, 0), length(request_raw), COALESCE(length(response_raw), 0), error, source FROM history WHERE id < ? ORDER BY id DESC LIMIT ?`, beforeID, limit, ) @@ -145,7 +155,7 @@ func (s *Store) List(limit int, beforeID int64) ([]Summary, error) { var sum Summary var startedAt, durationMs int64 if err := rows.Scan(&sum.ID, &startedAt, &durationMs, &sum.Method, &sum.Scheme, &sum.Host, &sum.Path, - &sum.StatusCode, &sum.ReqSize, &sum.RespSize, &sum.Error); err != nil { + &sum.StatusCode, &sum.ReqSize, &sum.RespSize, &sum.Error, &sum.Source); err != nil { return nil, fmt.Errorf("scan history row: %w", err) } sum.StartedAt = time.UnixMilli(startedAt) @@ -160,7 +170,7 @@ func (s *Store) Get(id int64) (*Entry, error) { row := s.db.QueryRow( `SELECT id, started_at, duration_ms, method, scheme, host, path, COALESCE(status_code, 0), request_raw, response_raw, - request_exact, response_exact, error + request_exact, response_exact, error, source FROM history WHERE id = ?`, id, ) @@ -168,7 +178,7 @@ func (s *Store) Get(id int64) (*Entry, error) { var startedAt, durationMs int64 var reqExact, respExact int if err := row.Scan(&e.ID, &startedAt, &durationMs, &e.Method, &e.Scheme, &e.Host, &e.Path, - &e.StatusCode, &e.RequestRaw, &e.ResponseRaw, &reqExact, &respExact, &e.Error); err != nil { + &e.StatusCode, &e.RequestRaw, &e.ResponseRaw, &reqExact, &respExact, &e.Error, &e.Source); err != nil { return nil, fmt.Errorf("get history entry %d: %w", id, err) } e.StartedAt = time.UnixMilli(startedAt) |