diff options
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/mitmux/main.go | 172 | ||||
| -rw-r--r-- | cmd/mitmuxd/main.go | 2 |
2 files changed, 159 insertions, 15 deletions
diff --git a/cmd/mitmux/main.go b/cmd/mitmux/main.go index d9e1685..55ab263 100644 --- a/cmd/mitmux/main.go +++ b/cmd/mitmux/main.go @@ -73,6 +73,7 @@ const ( viewRepeater viewRules viewIntruder + viewHelp ) type detailTab int @@ -158,6 +159,9 @@ type model struct { intruderCh <-chan ipc.IntrudeResultMsg intruderClose func() error + daemonStatus *ipc.StatusMsg + prevMode viewMode // for the ? help screen's "esc back" target + statusMsg string width int height int @@ -271,6 +275,16 @@ type repeatSentMsg struct { err error } +type statusLoadedMsg struct { + status *ipc.StatusMsg + err error +} + +func (m *model) loadStatus() tea.Msg { + s, err := m.client.Status() + return statusLoadedMsg{status: s, err: err} +} + func (m *model) loadList() tea.Msg { var entries []store.Summary var err error @@ -474,7 +488,7 @@ func (m *model) focusRuleField() { } func (m *model) Init() tea.Cmd { - return tea.Batch(m.loadList, m.waitForEntry) + return tea.Batch(m.loadList, m.waitForEntry, m.loadStatus) } func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -482,31 +496,35 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: m.width, m.height = msg.Width, msg.Height m.ready = true + // Reserve one line for the persistent status bar (see statusBar, + // appended in View); every height below is against h, not the + // raw terminal height, to leave room for it. + h := msg.Height - 1 m.table.SetWidth(msg.Width) - m.table.SetHeight(msg.Height - 5) + m.table.SetHeight(h - 5) m.searchInput.Width = msg.Width - 2 - m.viewport = viewport.New(msg.Width, msg.Height-5) + m.viewport = viewport.New(msg.Width, h-5) - reqHeight := (msg.Height - 6) / 2 + reqHeight := (h - 6) / 2 m.reqArea.SetWidth(msg.Width) m.reqArea.SetHeight(reqHeight) - m.respView = viewport.New(msg.Width, msg.Height-6-reqHeight) + m.respView = viewport.New(msg.Width, h-6-reqHeight) m.rulesTable.SetWidth(msg.Width) - m.rulesTable.SetHeight(msg.Height - 5) + m.rulesTable.SetHeight(h - 5) formWidth := msg.Width - 12 m.ruleName.Width = formWidth m.ruleMatch.Width = formWidth m.ruleReplace.Width = formWidth - itmplHeight := (msg.Height - 8) / 3 + itmplHeight := (h - 8) / 3 ipayloadsHeight := itmplHeight m.intruderTemplate.SetWidth(msg.Width) m.intruderTemplate.SetHeight(itmplHeight) m.intruderPayloads.SetWidth(msg.Width) m.intruderPayloads.SetHeight(ipayloadsHeight) m.intruderResults.SetWidth(msg.Width) - m.intruderResults.SetHeight(msg.Height - 8 - itmplHeight - ipayloadsHeight) + m.intruderResults.SetHeight(h - 8 - itmplHeight - ipayloadsHeight) return m, nil case listLoadedMsg: @@ -523,6 +541,9 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.statusMsg = "live updates disconnected" return m, nil } + if m.daemonStatus != nil { + m.daemonStatus.HistoryCount++ + } if m.query != "" { // Whether it matches the active filter isn't knowable without // re-querying; rather than guess, just surface a count and let @@ -535,6 +556,12 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.table.SetRows(rowsFor(m.entries)) return m, m.waitForEntry + case statusLoadedMsg: + if msg.err == nil { + m.daemonStatus = msg.status + } + return m, nil + case detailLoadedMsg: if msg.err != nil { m.statusMsg = "get error: " + msg.err.Error() @@ -633,6 +660,10 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg.String() { case "q", "ctrl+c": return m, tea.Quit + case "?": + m.prevMode = viewList + m.mode = viewHelp + return m, nil case "enter": if row := m.table.Cursor(); row >= 0 && row < len(m.entries) { m.mode = viewDetail @@ -677,6 +708,10 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case "ctrl+c": return m, tea.Quit + case "?": + m.prevMode = viewDetail + m.mode = viewHelp + return m, nil case "r": if m.detail != nil { m.enterRepeater(m.detail) @@ -718,6 +753,12 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case "ctrl+c": return m, tea.Quit + case "?": + if m.repeaterFocus != focusRequest || m.reqArea.Mode() != viInsert { + m.prevMode = viewRepeater + m.mode = viewHelp + return m, nil + } case "ctrl+r": if !m.sending { m.sending = true @@ -795,6 +836,10 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case "ctrl+c": return m, tea.Quit + case "?": + m.prevMode = viewRules + m.mode = viewHelp + return m, nil case "a": m.enterRuleForm(nil) return m, nil @@ -835,6 +880,12 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case "ctrl+c": return m, tea.Quit + case "?": + if !editing { + m.prevMode = viewIntruder + m.mode = viewHelp + return m, nil + } case "ctrl+r": if !m.intruderRunning { m.statusMsg = "starting attack..." @@ -881,6 +932,15 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.intruderResults, cmd = m.intruderResults.Update(msg) } return m, cmd + + case viewHelp: + switch msg.String() { + case "ctrl+c": + return m, tea.Quit + default: + m.mode = m.prevMode + return m, nil + } } } return m, nil @@ -890,21 +950,103 @@ func (m *model) View() string { if !m.ready { return "loading..." } + if m.mode == viewHelp { + return m.helpView() + } + + var body string switch m.mode { case viewDetail: - return m.detailView() + body = m.detailView() case viewRepeater: - return m.repeaterView() + body = m.repeaterView() case viewRules: if m.ruleForm { - return m.ruleFormView() + body = m.ruleFormView() + } else { + body = m.rulesView() } - return m.rulesView() case viewIntruder: - return m.intruderView() + body = m.intruderView() default: - return m.listView() + body = m.listView() + } + return body + "\n" + m.statusBar() +} + +// statusBar is a persistent one-line footer: daemon connection info and +// the current view, so it's always visible regardless of which screen +// is showing (see the WindowSizeMsg handler, which reserves a line for +// it in every height calculation). +func (m *model) statusBar() string { + proxy := "connecting..." + count := "" + if m.daemonStatus != nil { + proxy = m.daemonStatus.ProxyAddr + count = fmt.Sprintf(" · %d requests", m.daemonStatus.HistoryCount) } + view := map[viewMode]string{ + viewList: "history", viewDetail: "detail", viewRepeater: "repeater", + viewRules: "rules", viewIntruder: "intruder", + }[m.mode] + return statusBarStyle.Render(fmt.Sprintf(" mitmux · proxy %s%s · %s ", proxy, count, view)) +} + +func (m *model) helpView() string { + var b strings.Builder + b.WriteString(titleStyle.Render(" mitmux - keybindings ")) + b.WriteString("\n\n") + + section := func(title string, lines ...string) { + b.WriteString(tabActive.Render(" " + title + " ")) + b.WriteString("\n") + for _, l := range lines { + b.WriteString(" " + l + "\n") + } + b.WriteString("\n") + } + + section("Global", + "? this screen (any key returns)", + "ctrl+c quit", + ) + section("History list", + "↑/↓ or j/k navigate (also g/G top/bottom, ctrl+u/d half-page)", + "enter view request/response detail", + "r open in Repeater", + "i open in Intruder", + "/ search (plain text, host:value, AND/OR/NOT)", + "esc clear active search filter", + "m match-and-replace rules", + "q quit", + ) + section("Detail view", + "tab switch request/response", + "↑/↓ or j/k scroll (also g/G, ctrl+u/d - same as history list)", + "r / i open in Repeater / Intruder", + "esc / q back to history", + ) + section("Repeater / Intruder editors - vi-modal", + "Starts in NORMAL mode (not insert) - press i to type, esc to stop.", + "h j k l left/down/up/right 0 / $ line start/end", + "w / b word forward/back x delete character", + "i a I A insert (before/after/line-start/line-end)", + "o / O open line below/above and insert", + "dd / yy delete/yank line p / P paste below/above", + "dw d$ d0 delete word/to-end/to-start-of-line", + "gg / G top/bottom of buffer esc back to normal mode", + "ctrl+r send / start attack tab switch pane", + "ctrl+g (Intruder template only) insert a § marker at cursor", + ) + section("Rules", + "a add rule enter / e edit selected", + "d delete selected space toggle enabled", + "tab/shift+tab move between form fields ctrl+s save form", + "◀▶ change scope/regex toggle esc cancel/back", + ) + + b.WriteString(helpStyle.Render("press any key to go back")) + return b.String() } var ( @@ -913,6 +1055,8 @@ var ( statusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("208")) tabActive = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("0")).Background(lipgloss.Color("39")).Padding(0, 1) tabInactive = lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Padding(0, 1) + + statusBarStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("0")).Background(lipgloss.Color("240")) ) func (m *model) listView() string { diff --git a/cmd/mitmuxd/main.go b/cmd/mitmuxd/main.go index 812e146..9db31e5 100644 --- a/cmd/mitmuxd/main.go +++ b/cmd/mitmuxd/main.go @@ -74,7 +74,7 @@ func main() { hub := ipc.NewHub() srv.OnEntry = ipc.LogAndBroadcast(hub) - ipcSrv := ipc.NewServer(db, hub, srv) + ipcSrv := ipc.NewServer(db, hub, srv, *listen) go func() { if err := ipcSrv.Serve(sockLn); err != nil { log.Printf("control socket: %v", err) |