diff options
| author | srdusr <[email protected]> | 2026-08-25 15:58:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2026-08-25 15:58:00 +0200 |
| commit | 9e94bcbc939afd38b45f9ef42e1b1666fafd8d45 (patch) | |
| tree | 88a36226ee332c74e2dba98a9568a3a09ad074a8 /internal | |
| parent | dde73a349a68f942a5bd89b27ac980d7973148e0 (diff) | |
| download | mitmux-9e94bcbc939afd38b45f9ef42e1b1666fafd8d45.tar.gz mitmux-9e94bcbc939afd38b45f9ef42e1b1666fafd8d45.zip | |
Wire-format JSON tag consistency fix, and the first real plugin
Writing PLUGINS.md as an authoritative external spec surfaced a real,
pre-existing bug: store.Summary/Entry/EntryTag/WSMessage, rules.Rule,
scope.Rule, and clientcert.Cert had no JSON struct tags at all, so Go's
default marshaling serialized them PascalCase ("ID", "StartedAt")
while the rest of the protocol (EntryDetail's own fields, every
Request/Response wrapper field) uses snake_case. Confirmed live against
a real daemon before touching anything: a raw socket "list" request
came back with "ID"/"StartedAt"/"StatusCode", exactly the mismatch
suspected. Nothing outside this repo's own Go code consumes this wire
format yet, so this was a free, purely additive fix rather than
something to work around - every affected struct now tags snake_case
consistently.
plugins/authcheck is the first real plugin: an Autorize-style
authorization checker. For every proxied request carrying an
Authorization or Cookie header, resends it with that header stripped
and compares status classes - a resend that still succeeds where the
original did too is a likely missing-function-level-access-control
bug, tagged authcheck:bypass with structured detail. Deliberately
speaks the wire protocol directly (its own local request/response/
summary/entryDetail structs mirroring the real ones field-for-field,
not imported from internal/ipc) rather than taking the shortcut a Go
plugin could - proof the documented protocol is actually sufficient on
its own, since that's all a non-Go plugin author has to work with.
Verified live end to end: a real daemon, a real Python origin with one
endpoint that looks like it enforces auth but doesn't (vulnerable by
design) and one that actually does (the control case) - the broken
endpoint was correctly tagged, the secure one correctly left alone, no
false positive, confirmed both via the stored tag data directly and
visually in the TUI (tmux, real keystrokes): the Tags column badge, T's
tag list, and the tag detail view's JSON-colorized data (ANSI-verified,
not eyeballed) all showing the plugin's actual findings.
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/clientcert/clientcert.go | 14 | ||||
| -rw-r--r-- | internal/rules/rules.go | 18 | ||||
| -rw-r--r-- | internal/scope/scope.go | 8 | ||||
| -rw-r--r-- | internal/store/store.go | 91 |
4 files changed, 67 insertions, 64 deletions
diff --git a/internal/clientcert/clientcert.go b/internal/clientcert/clientcert.go index 4bba835..1d94879 100644 --- a/internal/clientcert/clientcert.go +++ b/internal/clientcert/clientcert.go @@ -15,13 +15,13 @@ import ( // Cert is one client certificate, scoped to hosts matching Pattern. type Cert struct { - ID int64 - Enabled bool - Name string - Pattern string - IsRegex bool - CertPEM []byte - KeyPEM []byte + ID int64 `json:"id"` + Enabled bool `json:"enabled"` + Name string `json:"name"` + Pattern string `json:"pattern"` + IsRegex bool `json:"is_regex"` + CertPEM []byte `json:"cert_pem"` + KeyPEM []byte `json:"key_pem"` } func (c Cert) matches(host string) bool { diff --git a/internal/rules/rules.go b/internal/rules/rules.go index e0547db..76d2ce7 100644 --- a/internal/rules/rules.go +++ b/internal/rules/rules.go @@ -14,17 +14,17 @@ import ( // Rule is one match-and-replace rule. type Rule struct { - ID int64 - Enabled bool - Name string - Scope string // "request" or "response" - Part string // "header" or "body" - Match string - Replace string - IsRegex bool + ID int64 `json:"id"` + Enabled bool `json:"enabled"` + Name string `json:"name"` + Scope string `json:"scope"` // "request" or "response" + Part string `json:"part"` // "header" or "body" + Match string `json:"match"` + Replace string `json:"replace"` + IsRegex bool `json:"is_regex"` // Position orders rule application (ascending) when several rules // could touch the same text. - Position int + Position int `json:"position"` } // ApplyHeaders rewrites h in place by serializing it to a raw diff --git a/internal/scope/scope.go b/internal/scope/scope.go index d8d2e80..a259801 100644 --- a/internal/scope/scope.go +++ b/internal/scope/scope.go @@ -22,10 +22,10 @@ import ( // match-and-replace rules already use, for the same reason: one // consistent mental model across both rule types in this tool. type Rule struct { - ID int64 - Enabled bool - Pattern string - IsRegex bool + ID int64 `json:"id"` + Enabled bool `json:"enabled"` + Pattern string `json:"pattern"` + IsRegex bool `json:"is_regex"` } // InScope reports whether host should be recorded, given rules. diff --git a/internal/store/store.go b/internal/store/store.go index ecc3854..8e9ab2b 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -159,66 +159,69 @@ func (s *Store) Close() error { return s.db.Close() } -// Entry is one captured request/response pair. +// Entry is one captured request/response pair. Never itself put on the +// IPC wire (see EntryDetail, which is) - tagged anyway for consistency +// with the rest of this package's now-uniformly-snake_case convention, +// and in case that ever changes. type Entry struct { - ID int64 - StartedAt time.Time - Duration time.Duration - Method string - Scheme string - Host string - Path string - StatusCode int // 0 if no response was received - RequestRaw []byte - 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 + ID int64 `json:"id"` + StartedAt time.Time `json:"started_at"` + Duration time.Duration `json:"duration"` + Method string `json:"method"` + Scheme string `json:"scheme"` + Host string `json:"host"` + Path string `json:"path"` + StatusCode int `json:"status_code"` // 0 if no response was received + RequestRaw []byte `json:"request_raw"` + ResponseRaw []byte `json:"response_raw"` // nil if no response was received + RequestExact bool `json:"request_exact"` // true if RequestRaw is wire-exact, false if reconstructed (e.g. HTTP/2) + ResponseExact bool `json:"response_exact"` // 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 + RequestTruncated bool `json:"request_truncated"` + ResponseTruncated bool `json:"response_truncated"` + Error string `json:"error"` // network/transport error, if the request never got a response + Source string `json:"source"` // "proxy" or "repeater" + Flagged bool `json:"flagged"` } // Summary is the lightweight metadata used for the history list view - // no request/response bodies. type Summary struct { - ID int64 - StartedAt time.Time - Duration time.Duration - Method string - Scheme string - Host string - Path string - StatusCode int - ReqSize int - RespSize int - Error string - Source string - Flagged bool + ID int64 `json:"id"` + StartedAt time.Time `json:"started_at"` + Duration time.Duration `json:"duration"` + Method string `json:"method"` + Scheme string `json:"scheme"` + Host string `json:"host"` + Path string `json:"path"` + StatusCode int `json:"status_code"` + ReqSize int `json:"req_size"` + RespSize int `json:"resp_size"` + Error string `json:"error"` + Source string `json:"source"` + Flagged bool `json:"flagged"` // Tags is every distinct plugin tag on this entry, comma-joined - // cheap enough to compute per row (a correlated subquery, see List/ // Search) that a plugin-tagged entry shows a badge in the history // list itself, not just in its detail view. - Tags string + Tags string `json:"tags"` } // EntryTag is one plugin-contributed marker on a history entry - see // the "tag_entry" IPC request and entry_tags' own schema comment for // what Plugin/Data mean. type EntryTag struct { - ID int64 - EntryID int64 - Plugin string - Tag string - Data string - CreatedAt time.Time + ID int64 `json:"id"` + EntryID int64 `json:"entry_id"` + Plugin string `json:"plugin"` + Tag string `json:"tag"` + Data string `json:"data"` + CreatedAt time.Time `json:"created_at"` } // AddEntryTag stores t and returns its assigned ID. @@ -785,12 +788,12 @@ func (s *Store) DeleteClientCert(id int64) error { // internal/proxy/websocket.go for why it's one row per frame rather than // per reassembled logical message. type WSMessage struct { - ID int64 - EntryID int64 - StartedAt time.Time - Direction string // "client_to_server" or "server_to_client" - Opcode int // RFC 6455 opcode: 1 text, 2 binary, 8 close, 9 ping, 10 pong - Payload []byte + ID int64 `json:"id"` + EntryID int64 `json:"entry_id"` + StartedAt time.Time `json:"started_at"` + Direction string `json:"direction"` // "client_to_server" or "server_to_client" + Opcode int `json:"opcode"` // RFC 6455 opcode: 1 text, 2 binary, 8 close, 9 ping, 10 pong + Payload []byte `json:"payload"` } // AddWSMessage stores one captured frame and returns its assigned ID. |