srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal/store
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2026-05-15 19:48:00 +0200
committersrdusr <[email protected]>2026-05-15 19:48:00 +0200
commitdd1621c0077e079e0693f16fe83f17d50338216e (patch)
tree411a06d723a16aff1077d5d3e724680436aac85a /internal/store
parentd7d50ae9902d69b2e52d446684b5ab01a5ecab1e (diff)
downloadmitmux-dd1621c0077e079e0693f16fe83f17d50338216e.tar.gz
mitmux-dd1621c0077e079e0693f16fe83f17d50338216e.zip
Stop mislabeling truncated captures as "exact"
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.
Diffstat (limited to 'internal/store')
-rw-r--r--internal/store/store.go36
1 files changed, 27 insertions, 9 deletions
diff --git a/internal/store/store.go b/internal/store/store.go
index 4de4fa1..19b71b1 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -34,7 +34,9 @@ CREATE TABLE IF NOT EXISTS history (
response_exact INTEGER NOT NULL,
error TEXT NOT NULL DEFAULT '',
source TEXT NOT NULL DEFAULT 'proxy',
- flagged INTEGER NOT NULL DEFAULT 0
+ flagged INTEGER NOT NULL DEFAULT 0,
+ request_truncated INTEGER NOT NULL DEFAULT 0,
+ response_truncated INTEGER NOT NULL DEFAULT 0
);
CREATE VIRTUAL TABLE IF NOT EXISTS history_fts USING fts5(
@@ -89,6 +91,8 @@ func Open(path string) (*Store, error) {
// on databases that already have it.
db.Exec("ALTER TABLE history ADD COLUMN source TEXT NOT NULL DEFAULT 'proxy'")
db.Exec("ALTER TABLE history ADD COLUMN flagged INTEGER NOT NULL DEFAULT 0")
+ db.Exec("ALTER TABLE history ADD COLUMN request_truncated INTEGER NOT NULL DEFAULT 0")
+ db.Exec("ALTER TABLE history ADD COLUMN response_truncated INTEGER NOT NULL DEFAULT 0")
// Backfill history_fts for rows inserted before it existed. A no-op
// once caught up, since every Insert keeps both tables in sync.
if _, err := db.Exec(`
@@ -121,9 +125,17 @@ type Entry struct {
ResponseRaw []byte // nil if no response was received
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"
- Flagged bool
+ // Truncated is true when the corresponding *Raw field would have
+ // been an exact capture but hit maxCaptureBytes and had bytes
+ // dropped off the end - distinct from a false *Exact, which also
+ // covers HTTP/2's inherently-reconstructed (never wire-exact to
+ // begin with) captures. Only meaningful when the matching *Exact
+ // field is false; a capture can't be both exact and truncated.
+ RequestTruncated bool
+ ResponseTruncated bool
+ Error string // network/transport error, if the request never got a response
+ Source string // "proxy" or "repeater"
+ Flagged bool
}
// Summary is the lightweight metadata used for the history list view -
@@ -164,10 +176,12 @@ func (s *Store) Insert(e *Entry) (int64, error) {
res, err := tx.Exec(
`INSERT INTO history
(started_at, duration_ms, method, scheme, host, path, status_code,
- request_raw, response_raw, request_exact, response_exact, error, source)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ request_raw, response_raw, request_exact, response_exact, error, source,
+ request_truncated, response_truncated)
+ 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, source,
+ boolToInt(e.RequestTruncated), boolToInt(e.ResponseTruncated),
)
if err != nil {
return 0, fmt.Errorf("insert history entry: %w", err)
@@ -394,15 +408,17 @@ 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, source, flagged
+ request_exact, response_exact, error, source, flagged,
+ request_truncated, response_truncated
FROM history WHERE id = ?`,
id,
)
var e Entry
var startedAt, durationMs int64
- var reqExact, respExact, flagged int
+ var reqExact, respExact, flagged, reqTrunc, respTrunc 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, &e.Source, &flagged); err != nil {
+ &e.StatusCode, &e.RequestRaw, &e.ResponseRaw, &reqExact, &respExact, &e.Error, &e.Source, &flagged,
+ &reqTrunc, &respTrunc); err != nil {
return nil, fmt.Errorf("get history entry %d: %w", id, err)
}
e.StartedAt = time.UnixMilli(startedAt)
@@ -410,6 +426,8 @@ func (s *Store) Get(id int64) (*Entry, error) {
e.RequestExact = reqExact != 0
e.ResponseExact = respExact != 0
e.Flagged = flagged != 0
+ e.RequestTruncated = reqTrunc != 0
+ e.ResponseTruncated = respTrunc != 0
return &e, nil
}