srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-rw-r--r--cmd/mitmux/main.go187
1 files changed, 185 insertions, 2 deletions
diff --git a/cmd/mitmux/main.go b/cmd/mitmux/main.go
index 6e07069..afb345b 100644
--- a/cmd/mitmux/main.go
+++ b/cmd/mitmux/main.go
@@ -116,6 +116,7 @@ const (
viewDecoder
viewScope
viewClientCerts
+ viewWebSocket
viewHelp
)
@@ -236,6 +237,15 @@ type model struct {
clientCertKeyPath textinput.Model
clientCertIsRegex bool
+ // WebSocket messages captured for one history entry's upgraded
+ // connection (see proxy.go's handleWebSocketUpgrade) - reached from
+ // the detail view, not its own top-level list.
+ wsTable table.Model
+ wsRows []store.WSMessage
+ wsEntryID int64
+ wsShowingDetail bool
+ wsDetailViewport viewport.Model
+
intruderScheme string
intruderHost string
intruderTemplate viTextarea
@@ -389,6 +399,16 @@ func newModel(client *ipc.Client, subCh <-chan store.Summary, socketPath string)
ccKeyPathIn := textinput.New()
ccKeyPathIn.Placeholder = "path to PEM private key file"
+ wsCols := []table.Column{
+ {Title: "#", Width: 4},
+ {Title: "Dir", Width: 3},
+ {Title: "Op", Width: 6},
+ {Title: "Size", Width: 8},
+ {Title: "Preview", Width: 50},
+ }
+ wsTbl := table.New(table.WithColumns(wsCols), table.WithFocused(true))
+ wsTbl.SetStyles(st)
+
itmpl := newViTextarea()
itmpl.ta.Placeholder = "raw request bytes - wrap positions to fuzz in § markers, e.g. /users/§123§"
itmpl.ta.ShowLineNumbers = false
@@ -441,6 +461,7 @@ func newModel(client *ipc.Client, subCh <-chan store.Summary, socketPath string)
clientCertPattern: ccPatternIn,
clientCertCertPath: ccCertPathIn,
clientCertKeyPath: ccKeyPathIn,
+ wsTable: wsTbl,
ruleName: nameIn,
ruleMatch: matchIn,
ruleReplace: replaceIn,
@@ -638,6 +659,19 @@ func (m *model) loadDetail(id int64, dest string) tea.Cmd {
}
}
+type wsMessagesLoadedMsg struct {
+ entryID int64
+ messages []store.WSMessage
+ err error
+}
+
+func (m *model) loadWSMessages(entryID int64) tea.Cmd {
+ return func() tea.Msg {
+ msgs, err := m.client.ListWSMessages(entryID)
+ return wsMessagesLoadedMsg{entryID: entryID, messages: msgs, err: err}
+ }
+}
+
// markOrCompare implements 'c': the first press on an entry marks it as
// the comparison base (no fetch yet - cheap, no round trip until there's
// actually something to compare). A second press on a *different* entry
@@ -1154,6 +1188,9 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.searchInput.Width = msg.Width - 2
m.viewport = viewport.New(msg.Width, h-5)
m.compareViewport = viewport.New(msg.Width, h-5)
+ m.wsTable.SetWidth(msg.Width)
+ m.wsTable.SetHeight(h - 5)
+ m.wsDetailViewport = viewport.New(msg.Width, h-5)
decInHeight := (h - 8) / 2
m.decoderInput.SetWidth(msg.Width)
@@ -1298,6 +1335,23 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.viewport.GotoTop()
return m, nil
+ case wsMessagesLoadedMsg:
+ if msg.err != nil {
+ m.statusMsg = "websocket messages error: " + msg.err.Error()
+ return m, nil
+ }
+ if len(msg.messages) == 0 {
+ m.statusMsg = "no websocket messages captured for this entry"
+ return m, nil
+ }
+ m.wsRows = msg.messages
+ m.wsEntryID = msg.entryID
+ m.wsShowingDetail = false
+ setTableRows(&m.wsTable, wsRowsFor(m.wsRows))
+ m.mode = viewWebSocket
+ m.statusMsg = ""
+ return m, nil
+
case compareLoadedMsg:
if msg.err != nil {
m.statusMsg = "compare error: " + msg.err.Error()
@@ -1695,6 +1749,12 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.detail != nil {
return m, m.markOrCompare(m.detail.ID)
}
+ case "w":
+ if m.detail != nil {
+ m.statusMsg = "loading websocket messages..."
+ return m, m.loadWSMessages(m.detail.ID)
+ }
+ return m, nil
case "tab":
if m.activeTab == tabRequest {
m.activeTab = tabResponse
@@ -1978,6 +2038,41 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.clientCertTable, cmd = m.clientCertTable.Update(msg)
return m, cmd
+ case viewWebSocket:
+ if m.wsShowingDetail {
+ switch msg.String() {
+ case "q", "esc":
+ m.wsShowingDetail = false
+ return m, nil
+ case "ctrl+c":
+ return m, tea.Quit
+ }
+ var cmd tea.Cmd
+ m.wsDetailViewport, cmd = m.wsDetailViewport.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 = viewWebSocket
+ m.mode = viewHelp
+ return m, nil
+ case "enter":
+ if row := m.wsTable.Cursor(); row >= 0 && row < len(m.wsRows) {
+ m.wsShowingDetail = true
+ m.wsDetailViewport.SetContent(wsMessageDetail(m.wsRows[row]))
+ m.wsDetailViewport.GotoTop()
+ }
+ return m, nil
+ }
+ var cmd tea.Cmd
+ m.wsTable, cmd = m.wsTable.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
@@ -2235,6 +2330,12 @@ func (m *model) View() string {
} else {
body = m.clientCertView()
}
+ case viewWebSocket:
+ if m.wsShowingDetail {
+ body = m.wsDetailView()
+ } else {
+ body = m.wsView()
+ }
case viewIntruder:
body = m.intruderView()
case viewCompare:
@@ -2264,7 +2365,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",
+ viewScope: "scope", viewClientCerts: "client certs", viewWebSocket: "websocket",
}[m.mode]
return statusBarStyle.Render(fmt.Sprintf(" mitmux · proxy %s%s · %s ", proxy, count, view))
}
@@ -2323,8 +2424,16 @@ func (m *model) helpView() string {
"c mark/compare (same as history list)",
"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)",
"esc / q back to history",
)
+ section("WebSocket messages",
+ "One row per captured frame (not per reassembled message - see",
+ "PLAN.md) for the entry's upgraded connection.",
+ "↑/↓ or j/k navigate (also g/G, ctrl+u/d)",
+ "enter view this frame's full decoded payload",
+ "esc / q back (from payload view: back to the message list)",
+ )
section("Comparer",
"tab switch request/response diff",
"↑/↓ or j/k scroll (also g/G, ctrl+u/d - same as history list)",
@@ -2477,7 +2586,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 · esc back · ? help · q quit"))
+ b.WriteString(helpStyle.Render("tab switch · p pretty-print · c compare · r repeater · i intruder · e export · w websocket · esc back · ? help · q quit"))
return b.String()
}
@@ -2733,6 +2842,80 @@ func clientCertRowsFor(cs []clientcert.Cert) []table.Row {
return rows
}
+func (m *model) wsView() string {
+ var b strings.Builder
+ title := fmt.Sprintf(" websocket messages (%d) - entry #%d ", len(m.wsRows), m.wsEntryID)
+ b.WriteString(titleStyle.Render(title))
+ b.WriteString("\n")
+ b.WriteString(m.wsTable.View())
+ b.WriteString("\n")
+ if m.statusMsg != "" {
+ b.WriteString(statusStyle.Render(sanitizeLine(m.statusMsg)))
+ b.WriteString("\n")
+ }
+ b.WriteString(helpStyle.Render("enter view payload · esc back · q quit"))
+ return b.String()
+}
+
+func (m *model) wsDetailView() string {
+ var b strings.Builder
+ b.WriteString(titleStyle.Render(" websocket message "))
+ b.WriteString("\n")
+ b.WriteString(m.wsDetailViewport.View())
+ b.WriteString("\n")
+ b.WriteString(helpStyle.Render("↑/↓ scroll · esc back · q quit"))
+ return b.String()
+}
+
+func wsOpcodeName(opcode int) string {
+ switch opcode {
+ case 0x1:
+ return "text"
+ case 0x2:
+ return "binary"
+ case 0x8:
+ return "close"
+ case 0x9:
+ return "ping"
+ case 0xa:
+ return "pong"
+ default:
+ return fmt.Sprintf("0x%x", opcode)
+ }
+}
+
+func wsRowsFor(msgs []store.WSMessage) []table.Row {
+ rows := make([]table.Row, len(msgs))
+ for i, m := range msgs {
+ dir := "->"
+ if m.Direction == "server_to_client" {
+ dir = "<-"
+ }
+ rows[i] = table.Row{
+ fmt.Sprintf("%d", i+1),
+ dir,
+ wsOpcodeName(m.Opcode),
+ humanBytes(len(m.Payload)),
+ sanitizeLine(string(m.Payload)),
+ }
+ }
+ return rows
+}
+
+// wsMessageDetail is the full, sanitized content shown when viewing one
+// captured frame's payload - sanitizeBlock because, same as request/
+// response bodies elsewhere, this is attacker- or origin-controlled data
+// reaching the operator's real terminal, not just the display width
+// truncation the table preview gets away with.
+func wsMessageDetail(m store.WSMessage) string {
+ dir := "client -> server"
+ if m.Direction == "server_to_client" {
+ dir = "server -> client"
+ }
+ return fmt.Sprintf("%s · opcode: %s · %s\n\n%s",
+ dir, wsOpcodeName(m.Opcode), humanBytes(len(m.Payload)), sanitizeBlock(string(m.Payload)))
+}
+
// nextAttackMode cycles Sniper -> BatteringRam -> Pitchfork -> ClusterBomb
// -> Sniper.
func nextAttackMode(mode proxy.AttackMode) proxy.AttackMode {