srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal/store/store.go
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-08-29 22:03:00 +0200
committersrdusr <[email protected]>2024-08-29 22:03:00 +0200
commit70f675e59ac4bb0e469ca0073f8cc0506b17677e (patch)
treee3a525b26dfcf16ce4f9bde97b1408a373d87944 /internal/store/store.go
parent8d15c2e0b326933f8fc912e3b13f37e78a9bc0b6 (diff)
downloadmitmux-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.go26
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)