srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-rw-r--r--cmd/mitmux/main.go157
-rw-r--r--cmd/mitmux/mouse.go30
2 files changed, 183 insertions, 4 deletions
diff --git a/cmd/mitmux/main.go b/cmd/mitmux/main.go
index 30b71e7..4664a6b 100644
--- a/cmd/mitmux/main.go
+++ b/cmd/mitmux/main.go
@@ -118,6 +118,7 @@ const (
viewScope
viewClientCerts
viewWebSocket
+ viewTags
viewHelp
)
@@ -143,6 +144,7 @@ const (
sortByMethod
sortByHost
sortByPath
+ sortByTags
)
func (c sortColumn) String() string {
@@ -159,6 +161,8 @@ func (c sortColumn) String() string {
return "host"
case sortByPath:
return "path"
+ case sortByTags:
+ return "tags"
default:
return "captured"
}
@@ -288,6 +292,15 @@ type model struct {
wsShowingDetail bool
wsDetailViewport viewport.Model
+ // Plugin-contributed tags on the currently viewed entry (see
+ // internal/ipc's "tag_entry") - already loaded as part of
+ // ipc.EntryDetail by the time detail view opens, no separate fetch
+ // needed. Reached from the detail view via 'T'.
+ tagsTable table.Model
+ tagsRows []store.EntryTag
+ tagsShowingDetail bool
+ tagsDetailViewport viewport.Model
+
intruderScheme string
intruderHost string
intruderTemplate viTextarea
@@ -376,6 +389,7 @@ func newModel(client *ipc.Client, subCh <-chan store.Summary, socketPath string)
{Title: "Method", Width: 7},
{Title: "Host", Width: 27},
{Title: "Path", Width: 31},
+ {Title: "Tags", Width: 12},
{Title: "Status", Width: 6},
{Title: "Size", Width: 10},
{Title: "Time", Width: 8},
@@ -451,6 +465,14 @@ func newModel(client *ipc.Client, subCh <-chan store.Summary, socketPath string)
wsTbl := table.New(table.WithColumns(wsCols), table.WithFocused(true))
wsTbl.SetStyles(st)
+ tagsCols := []table.Column{
+ {Title: "Plugin", Width: 16},
+ {Title: "Tag", Width: 20},
+ {Title: "Preview", Width: 40},
+ }
+ tagsTbl := table.New(table.WithColumns(tagsCols), table.WithFocused(true))
+ tagsTbl.SetStyles(st)
+
itmpl := newViTextarea()
itmpl.ta.Placeholder = "raw request bytes - wrap positions to fuzz in § markers, e.g. /users/§123§"
itmpl.ta.ShowLineNumbers = false
@@ -504,6 +526,7 @@ func newModel(client *ipc.Client, subCh <-chan store.Summary, socketPath string)
clientCertCertPath: ccCertPathIn,
clientCertKeyPath: ccKeyPathIn,
wsTable: wsTbl,
+ tagsTable: tagsTbl,
ruleName: nameIn,
ruleMatch: matchIn,
ruleReplace: replaceIn,
@@ -1233,6 +1256,9 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.wsTable.SetWidth(msg.Width)
m.wsTable.SetHeight(h - 5)
m.wsDetailViewport = viewport.New(msg.Width, h-5)
+ m.tagsTable.SetWidth(msg.Width)
+ m.tagsTable.SetHeight(h - 5)
+ m.tagsDetailViewport = viewport.New(msg.Width, h-5)
decInHeight := (h - 8) / 2
m.decoderInput.SetWidth(msg.Width)
@@ -1706,7 +1732,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.statusMsg = ""
return m, m.loadClientCerts
case "o":
- m.sortColumn = (m.sortColumn + 1) % 7
+ m.sortColumn = (m.sortColumn + 1) % 8
m.sortDesc = false
m.refreshTable()
if m.sortColumn == sortByTime {
@@ -1819,6 +1845,19 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.loadWSMessages(m.detail.ID)
}
return m, nil
+ case "T":
+ if m.detail != nil {
+ if len(m.detail.Tags) == 0 {
+ m.statusMsg = "no plugin tags on this entry"
+ return m, nil
+ }
+ m.tagsRows = m.detail.Tags
+ m.tagsShowingDetail = false
+ setTableRows(&m.tagsTable, tagsRowsFor(m.tagsRows))
+ m.mode = viewTags
+ m.statusMsg = ""
+ }
+ return m, nil
case "tab":
if m.activeTab == tabRequest {
m.activeTab = tabResponse
@@ -2137,6 +2176,41 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.wsTable, cmd = m.wsTable.Update(msg)
return m, cmd
+ case viewTags:
+ if m.tagsShowingDetail {
+ switch msg.String() {
+ case "q", "esc":
+ m.tagsShowingDetail = false
+ return m, nil
+ case "ctrl+c":
+ return m, tea.Quit
+ }
+ var cmd tea.Cmd
+ m.tagsDetailViewport, cmd = m.tagsDetailViewport.Update(msg)
+ return m, cmd
+ }
+ switch msg.String() {
+ case "q", "esc":
+ m.mode = viewDetail
+ return m, nil
+ case "ctrl+c":
+ return m, tea.Quit
+ case "?":
+ m.prevMode = viewTags
+ m.mode = viewHelp
+ return m, nil
+ case "enter":
+ if row := m.tagsTable.Cursor(); row >= 0 && row < len(m.tagsRows) {
+ m.tagsShowingDetail = true
+ m.tagsDetailViewport.SetContent(tagDetailContent(m.tagsRows[row]))
+ m.tagsDetailViewport.GotoTop()
+ }
+ return m, nil
+ }
+ var cmd tea.Cmd
+ m.tagsTable, cmd = m.tagsTable.Update(msg)
+ return m, cmd
+
case viewIntruder:
// Editing a grep pattern is a modal overlay on top of the
// normal template/payloads/results panes, same pattern as
@@ -2400,6 +2474,12 @@ func (m *model) View() string {
} else {
body = m.wsView()
}
+ case viewTags:
+ if m.tagsShowingDetail {
+ body = m.tagsDetailView()
+ } else {
+ body = m.tagsView()
+ }
case viewIntruder:
body = m.intruderView()
case viewCompare:
@@ -2429,7 +2509,7 @@ func (m *model) statusBar() string {
view := map[viewMode]string{
viewList: "history", viewDetail: "detail", viewRepeater: "repeater",
viewRules: "rules", viewIntruder: "intruder", viewCompare: "comparer", viewDecoder: "decoder",
- viewScope: "scope", viewClientCerts: "client certs", viewWebSocket: "websocket",
+ viewScope: "scope", viewClientCerts: "client certs", viewWebSocket: "websocket", viewTags: "tags",
}[m.mode]
return statusBarStyle.Render(fmt.Sprintf(" mitmux · proxy %s%s · %s ", proxy, count, view))
}
@@ -2477,7 +2557,7 @@ func (m *model) helpView() string {
" source:repeater, flagged:true",
"esc clear active search filter",
"o cycle sort column (captured/status/size/time taken/",
- " method/host/path) - sorts the currently loaded page",
+ " method/host/path/tags) - sorts the currently loaded page",
"O reverse the current sort column's direction",
"m match-and-replace rules",
"s target scope (what gets recorded)",
@@ -2492,6 +2572,7 @@ func (m *model) helpView() string {
"r / i open in Repeater / Intruder",
"e export this entry - .txt (raw request+response) or .sh/.curl (curl command)",
"w view captured WebSocket messages (only for an upgraded connection)",
+ "T view plugin tags on this entry, if any",
"esc / q back to history",
)
section("WebSocket messages",
@@ -2501,6 +2582,14 @@ func (m *model) helpView() string {
"enter view this frame's full decoded payload",
"esc / q back (from payload view: back to the message list)",
)
+ section("Plugin tags",
+ "Markers a connected plugin attached to this entry (see",
+ "PLAN.md's plugin protocol) - e.g. \"jwt\" from a JWT-decoding",
+ "plugin, with the decoded token as that tag's data.",
+ "↑/↓ or j/k navigate (also g/G, ctrl+u/d)",
+ "enter view this tag's full data (JSON-colored if it is JSON)",
+ "esc / q back (from data view: back to the tag list)",
+ )
section("Comparer",
"tab switch request/response diff",
"↑/↓ or j/k scroll (also g/G, ctrl+u/d - same as history list)",
@@ -2678,7 +2767,7 @@ func (m *model) detailView() string {
b.WriteString(statusStyle.Render(sanitizeLine(m.statusMsg)))
b.WriteString("\n")
}
- b.WriteString(helpStyle.Render("tab switch · p pretty-print · c compare · r repeater · i intruder · e export · w websocket · esc back · ? help · q quit"))
+ b.WriteString(helpStyle.Render("tab switch · p pretty-print · c compare · r repeater · i intruder · e export · w websocket · T tags · esc back · ? help · q quit"))
return b.String()
}
@@ -3008,6 +3097,63 @@ func wsMessageDetail(m store.WSMessage) string {
dir, wsOpcodeName(m.Opcode), humanBytes(len(m.Payload)), sanitizeBlock(string(m.Payload)))
}
+func (m *model) tagsView() string {
+ var b strings.Builder
+ title := fmt.Sprintf(" plugin tags (%d) - entry #%d ", len(m.tagsRows), m.detail.ID)
+ b.WriteString(titleStyle.Render(title))
+ b.WriteString("\n")
+ b.WriteString(m.tagsTable.View())
+ b.WriteString("\n")
+ if m.statusMsg != "" {
+ b.WriteString(statusStyle.Render(sanitizeLine(m.statusMsg)))
+ b.WriteString("\n")
+ }
+ b.WriteString(helpStyle.Render("enter view tag data · esc back · q quit"))
+ return b.String()
+}
+
+func (m *model) tagsDetailView() string {
+ var b strings.Builder
+ b.WriteString(titleStyle.Render(" tag data "))
+ b.WriteString("\n")
+ b.WriteString(m.tagsDetailViewport.View())
+ b.WriteString("\n")
+ b.WriteString(helpStyle.Render("↑/↓ scroll · esc back · q quit"))
+ return b.String()
+}
+
+func tagsRowsFor(tags []store.EntryTag) []table.Row {
+ rows := make([]table.Row, len(tags))
+ for i, t := range tags {
+ rows[i] = table.Row{
+ sanitizeLine(t.Plugin),
+ sanitizeLine(t.Tag),
+ sanitizeLine(t.Data),
+ }
+ }
+ return rows
+}
+
+// tagDetailContent is the full content shown when viewing one tag's
+// data: JSON-colorized (reusing jsoncolor.go, same as a pretty-printed
+// response body) if it parses as JSON - the common case, since a
+// decoding plugin like a JWT parser has structured data to show - or
+// plain sanitized text otherwise. Either way this is plugin-supplied,
+// not necessarily attacker-controlled, but a plugin can echo back
+// attacker-influenced content (e.g. a header value it parsed), so it
+// gets the same sanitizeBlock/sanitizeControl treatment as any other
+// text reaching the real terminal from outside this process.
+func tagDetailContent(t store.EntryTag) string {
+ header := fmt.Sprintf("plugin: %s · tag: %s", sanitizeLine(t.Plugin), sanitizeLine(t.Tag))
+ if t.Data == "" {
+ return header + "\n\n(no data attached)"
+ }
+ if colored, ok := colorizeJSON([]byte(t.Data)); ok {
+ return header + "\n\n" + colored
+ }
+ return header + "\n\n" + sanitizeBlock(t.Data)
+}
+
// nextAttackMode cycles Sniper -> BatteringRam -> Pitchfork -> ClusterBomb
// -> Sniper.
func nextAttackMode(mode proxy.AttackMode) proxy.AttackMode {
@@ -3266,6 +3412,8 @@ func (m *model) sortedEntries() []store.Summary {
return a.Host < b.Host
case sortByPath:
return a.Path < b.Path
+ case sortByTags:
+ return a.Tags < b.Tags
default:
return false
}
@@ -3297,6 +3445,7 @@ func rowsFor(entries []store.Summary) []table.Row {
sanitizeLine(e.Method),
sanitizeLine(e.Host),
sanitizeLine(e.Path),
+ sanitizeLine(e.Tags),
status,
size,
e.Duration.Round(time.Millisecond).String(),
diff --git a/cmd/mitmux/mouse.go b/cmd/mitmux/mouse.go
index e2d4936..b1c435b 100644
--- a/cmd/mitmux/mouse.go
+++ b/cmd/mitmux/mouse.go
@@ -81,6 +81,36 @@ func (m *model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
return m.handleScopeMouse(msg)
case viewClientCerts:
return m.handleClientCertMouse(msg)
+ case viewWebSocket:
+ if isWheel(msg) {
+ var cmd tea.Cmd
+ if m.wsShowingDetail {
+ m.wsDetailViewport, cmd = m.wsDetailViewport.Update(msg)
+ } else {
+ switch msg.Button {
+ case tea.MouseButtonWheelUp:
+ m.wsTable.MoveUp(3)
+ case tea.MouseButtonWheelDown:
+ m.wsTable.MoveDown(3)
+ }
+ }
+ return m, cmd
+ }
+ case viewTags:
+ if isWheel(msg) {
+ var cmd tea.Cmd
+ if m.tagsShowingDetail {
+ m.tagsDetailViewport, cmd = m.tagsDetailViewport.Update(msg)
+ } else {
+ switch msg.Button {
+ case tea.MouseButtonWheelUp:
+ m.tagsTable.MoveUp(3)
+ case tea.MouseButtonWheelDown:
+ m.tagsTable.MoveDown(3)
+ }
+ }
+ return m, cmd
+ }
}
return m, nil
}