diff options
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/mitmux/main.go | 223 |
1 files changed, 221 insertions, 2 deletions
diff --git a/cmd/mitmux/main.go b/cmd/mitmux/main.go index 467f0d5..21209bf 100644 --- a/cmd/mitmux/main.go +++ b/cmd/mitmux/main.go @@ -21,6 +21,7 @@ import ( "mitmux/internal/ca" "mitmux/internal/ipc" "mitmux/internal/rules" + "mitmux/internal/scope" "mitmux/internal/store" ) @@ -76,6 +77,7 @@ const ( viewIntruder viewCompare viewDecoder + viewScope viewHelp ) @@ -160,6 +162,16 @@ type model struct { ruleRegex bool ruleField ruleField + // Target scope: which captured traffic gets recorded to history. + // Simpler than match-and-replace rules (one pattern + a regex + // toggle, no name/scope/replace fields), so add-only - no + // edit-in-place, delete and re-add covers changing a pattern. + scopeTable table.Model + scopeRows []scope.Rule + scopeForm bool + scopePattern textinput.Model + scopeIsRegex bool + intruderScheme string intruderHost string intruderTemplate viTextarea @@ -269,6 +281,17 @@ func newModel(client *ipc.Client, subCh <-chan store.Summary, socketPath string) replaceIn := textinput.New() replaceIn.Placeholder = "replacement" + scopeCols := []table.Column{ + {Title: "On", Width: 3}, + {Title: "Pattern", Width: 40}, + {Title: "Regex", Width: 5}, + } + scopeTbl := table.New(table.WithColumns(scopeCols), table.WithFocused(true)) + scopeTbl.SetStyles(st) + + scopePatternIn := textinput.New() + scopePatternIn.Placeholder = "host substring, or a regex - e.g. example.com" + itmpl := newViTextarea() itmpl.ta.Placeholder = "raw request bytes - wrap positions to fuzz in § markers, e.g. /users/§123§" itmpl.ta.ShowLineNumbers = false @@ -311,6 +334,8 @@ func newModel(client *ipc.Client, subCh <-chan store.Summary, socketPath string) repeaterIndex: -1, searchInput: si, rulesTable: rt, + scopeTable: scopeTbl, + scopePattern: scopePatternIn, ruleName: nameIn, ruleMatch: matchIn, ruleReplace: replaceIn, @@ -744,6 +769,64 @@ func (m *model) focusRuleField() { } } +type scopeRulesLoadedMsg struct { + rules []scope.Rule + err error +} + +type scopeRuleWriteDoneMsg struct { + action string // "added", "deleted", "toggled" - for the status line + err error +} + +func (m *model) loadScopeRules() tea.Msg { + rs, err := m.client.ListScopeRules() + return scopeRulesLoadedMsg{rules: rs, err: err} +} + +func (m *model) addScopeRule(r scope.Rule) tea.Cmd { + return func() tea.Msg { + _, err := m.client.AddScopeRule(r) + return scopeRuleWriteDoneMsg{action: "added", err: err} + } +} + +func (m *model) deleteSelectedScopeRule() tea.Cmd { + row := m.scopeTable.Cursor() + if row < 0 || row >= len(m.scopeRows) { + return nil + } + id := m.scopeRows[row].ID + return func() tea.Msg { + err := m.client.DeleteScopeRule(id) + return scopeRuleWriteDoneMsg{action: "deleted", err: err} + } +} + +func (m *model) toggleSelectedScopeRule() tea.Cmd { + row := m.scopeTable.Cursor() + if row < 0 || row >= len(m.scopeRows) { + return nil + } + r := m.scopeRows[row] + return func() tea.Msg { + err := m.client.SetScopeRuleEnabled(r.ID, !r.Enabled) + return scopeRuleWriteDoneMsg{action: "toggled", err: err} + } +} + +// enterScopeForm opens the (add-only) scope rule form. +func (m *model) enterScopeForm() { + m.scopeForm = true + m.scopePattern.SetValue("") + m.scopeIsRegex = false + m.scopePattern.Focus() +} + +func (m *model) scopeRuleFromForm() scope.Rule { + return scope.Rule{Enabled: true, Pattern: m.scopePattern.Value(), IsRegex: m.scopeIsRegex} +} + func (m *model) Init() tea.Cmd { return tea.Batch(m.loadList, m.waitForEntry, m.loadStatus) } @@ -779,6 +862,10 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.ruleMatch.Width = formWidth m.ruleReplace.Width = formWidth + m.scopeTable.SetWidth(msg.Width) + m.scopeTable.SetHeight(h - 5) + m.scopePattern.Width = formWidth + // h-9 rather than h-8: one extra line reserved for the payload // rules / grep-match-extract status line in intruderView. itmplHeight := (h - 9) / 3 @@ -937,6 +1024,24 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.statusMsg = "rule " + msg.action return m, m.loadRules + case scopeRulesLoadedMsg: + if msg.err != nil { + m.statusMsg = "scope error: " + msg.err.Error() + return m, nil + } + m.scopeRows = msg.rules + setTableRows(&m.scopeTable, scopeRowsFor(m.scopeRows)) + return m, nil + + case scopeRuleWriteDoneMsg: + if msg.err != nil { + m.statusMsg = "scope " + msg.action + " error: " + msg.err.Error() + return m, nil + } + m.scopeForm = false + m.statusMsg = "scope rule " + msg.action + return m, m.loadScopeRules + case intrudeStartedMsg: if msg.err != nil { m.intruderRunning = false @@ -1097,6 +1202,10 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.mode = viewRules m.statusMsg = "" return m, m.loadRules + case "s": + m.mode = viewScope + m.statusMsg = "" + return m, m.loadScopeRules case "esc": if m.query != "" { m.query = "" @@ -1344,6 +1453,49 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.rulesTable, cmd = m.rulesTable.Update(msg) return m, cmd + case viewScope: + if m.scopeForm { + switch msg.String() { + case "esc": + m.scopeForm = false + return m, nil + case "ctrl+c": + return m, tea.Quit + case "ctrl+s": + return m, m.addScopeRule(m.scopeRuleFromForm()) + case "tab": + m.scopeIsRegex = !m.scopeIsRegex + return m, nil + } + var cmd tea.Cmd + m.scopePattern, cmd = m.scopePattern.Update(msg) + return m, cmd + } + + switch msg.String() { + case "q", "esc": + m.mode = viewList + return m, nil + case "ctrl+c": + return m, tea.Quit + case "?": + m.prevMode = viewScope + m.mode = viewHelp + return m, nil + case "a": + m.enterScopeForm() + return m, nil + case "d": + m.statusMsg = "" + return m, m.deleteSelectedScopeRule() + case " ": + m.statusMsg = "" + return m, m.toggleSelectedScopeRule() + } + var cmd tea.Cmd + m.scopeTable, cmd = m.scopeTable.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 @@ -1584,6 +1736,12 @@ func (m *model) View() string { } else { body = m.rulesView() } + case viewScope: + if m.scopeForm { + body = m.scopeFormView() + } else { + body = m.scopeView() + } case viewIntruder: body = m.intruderView() case viewCompare: @@ -1610,6 +1768,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", }[m.mode] return statusBarStyle.Render(fmt.Sprintf(" mitmux · proxy %s%s · %s ", proxy, count, view)) } @@ -1648,6 +1807,7 @@ func (m *model) helpView() string { " source:repeater, flagged:true", "esc clear active search filter", "m match-and-replace rules", + "s target scope (what gets recorded)", "q quit", ) section("Detail view", @@ -1691,6 +1851,15 @@ func (m *model) helpView() string { "tab/shift+tab move between form fields ctrl+s save form", "◀▶ change scope/regex toggle esc cancel/back", ) + section("Scope", + "What gets recorded to history - out-of-scope traffic still", + "proxies normally, it's just not stored. No rules means", + "everything is recorded (today's default, unchanged).", + "a add rule (pattern + regex toggle)", + "d delete selected space toggle enabled", + "tab toggle regex (in the add form)", + "ctrl+s save form esc cancel/back", + ) b.WriteString(helpStyle.Render("press any key to go back")) return b.String() @@ -1739,9 +1908,9 @@ func (m *model) listView() string { b.WriteString(statusStyle.Render(sanitizeLine(m.statusMsg))) b.WriteString("\n") } - help := "enter view · r/i/c/d tools · f flag · x delete · X clear all · E export HAR · / search · m rules · ? help · q quit" + help := "enter view · r/i/c/d tools · f flag · x delete · X clear all · E export HAR · / search · m rules · s scope · ? help · q quit" if m.query != "" { - help = "enter view · r/i/c/d tools · f flag · x delete · X clear all · E export HAR (this filter) · / search · esc clear filter · ? help · q quit" + help = "enter view · r/i/c/d tools · f flag · x delete · X clear all · E export HAR (this filter) · / search · esc clear filter · s scope · ? help · q quit" } b.WriteString(helpStyle.Render(help)) return b.String() @@ -1909,6 +2078,40 @@ func (m *model) ruleFormView() string { return b.String() } +func (m *model) scopeView() string { + var b strings.Builder + title := fmt.Sprintf(" scope (%d) - no rules means everything is recorded ", len(m.scopeRows)) + b.WriteString(titleStyle.Render(title)) + b.WriteString("\n") + b.WriteString(m.scopeTable.View()) + b.WriteString("\n") + if m.statusMsg != "" { + b.WriteString(statusStyle.Render(sanitizeLine(m.statusMsg))) + b.WriteString("\n") + } + b.WriteString(helpStyle.Render("a add · d delete · space toggle · esc back · q quit")) + return b.String() +} + +func (m *model) scopeFormView() string { + var b strings.Builder + b.WriteString(titleStyle.Render(" add scope rule ")) + b.WriteString("\n\n") + b.WriteString(tabActive.Render("Pattern (substring match, or a regex against the host)") + "\n") + b.WriteString(m.scopePattern.View() + "\n\n") + regexText := "Regex: off (tab to toggle)" + if m.scopeIsRegex { + regexText = "Regex: on (tab to toggle)" + } + b.WriteString(helpStyle.Render(regexText) + "\n\n") + if m.statusMsg != "" { + b.WriteString(statusStyle.Render(sanitizeLine(m.statusMsg))) + b.WriteString("\n") + } + b.WriteString(helpStyle.Render("tab toggle regex · ctrl+s save · esc cancel · ctrl+c quit")) + return b.String() +} + func rulesRowsFor(rs []rules.Rule) []table.Row { rows := make([]table.Row, len(rs)) for i, r := range rs { @@ -1925,6 +2128,22 @@ func rulesRowsFor(rs []rules.Rule) []table.Row { return rows } +func scopeRowsFor(rs []scope.Rule) []table.Row { + rows := make([]table.Row, len(rs)) + for i, r := range rs { + on := " " + if r.Enabled { + on = "✓" + } + regex := "" + if r.IsRegex { + regex = "yes" + } + rows[i] = table.Row{on, r.Pattern, regex} + } + return rows +} + func (m *model) intruderView() string { var b strings.Builder title := fmt.Sprintf(" intruder - %s://%s ", sanitizeLine(m.intruderScheme), sanitizeLine(m.intruderHost)) |