srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-rw-r--r--cmd/mitmux/main.go172
-rw-r--r--cmd/mitmuxd/main.go2
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)