aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--mitmproxy/certs.py6
-rw-r--r--mitmproxy/contrib/kaitaistruct/png.py8
-rw-r--r--mitmproxy/options.py4
-rw-r--r--mitmproxy/platform/openbsd.py1
-rw-r--r--mitmproxy/tools/cmdline.py1
-rw-r--r--mitmproxy/tools/console/commands.py27
-rw-r--r--mitmproxy/tools/console/defaultkeys.py139
-rw-r--r--mitmproxy/tools/console/eventlog.py12
-rw-r--r--mitmproxy/tools/console/flowlist.py51
-rw-r--r--mitmproxy/tools/console/flowview.py84
-rw-r--r--mitmproxy/tools/console/grideditor/base.py227
-rw-r--r--mitmproxy/tools/console/grideditor/col_bytes.py49
-rw-r--r--mitmproxy/tools/console/grideditor/col_subgrid.py2
-rw-r--r--mitmproxy/tools/console/grideditor/editors.py140
-rw-r--r--mitmproxy/tools/console/help.py123
-rw-r--r--mitmproxy/tools/console/keymap.py15
-rw-r--r--mitmproxy/tools/console/layoutwidget.py42
-rw-r--r--mitmproxy/tools/console/master.py180
-rw-r--r--mitmproxy/tools/console/options.py33
-rw-r--r--mitmproxy/tools/console/overlay.py54
-rw-r--r--mitmproxy/tools/console/select.py120
-rw-r--r--mitmproxy/tools/console/signals.py8
-rw-r--r--mitmproxy/tools/console/statusbar.py33
-rw-r--r--mitmproxy/tools/console/tabs.py4
-rw-r--r--mitmproxy/tools/console/window.py94
-rw-r--r--setup.cfg1
-rw-r--r--setup.py2
-rw-r--r--test/mitmproxy/proxy/test_config.py39
-rw-r--r--test/mitmproxy/tools/console/test_help.py11
-rw-r--r--test/mitmproxy/tools/console/test_keymap.py37
-rw-r--r--test/mitmproxy/tools/console/test_master.py9
-rw-r--r--tox.ini4
32 files changed, 677 insertions, 883 deletions
diff --git a/mitmproxy/certs.py b/mitmproxy/certs.py
index 618b34de..0e441efe 100644
--- a/mitmproxy/certs.py
+++ b/mitmproxy/certs.py
@@ -266,6 +266,12 @@ class CertStore:
with open(os.path.join(path, basename + "-ca-cert.p12"), "wb") as f:
p12 = OpenSSL.crypto.PKCS12()
p12.set_certificate(ca)
+ f.write(p12.export())
+
+ # Dump the certificate and key in a PKCS12 format for Windows devices
+ with open(os.path.join(path, basename + "-ca.p12"), "wb") as f:
+ p12 = OpenSSL.crypto.PKCS12()
+ p12.set_certificate(ca)
p12.set_privatekey(key)
f.write(p12.export())
diff --git a/mitmproxy/contrib/kaitaistruct/png.py b/mitmproxy/contrib/kaitaistruct/png.py
index 98a70693..45074d70 100644
--- a/mitmproxy/contrib/kaitaistruct/png.py
+++ b/mitmproxy/contrib/kaitaistruct/png.py
@@ -34,9 +34,11 @@ class Png(KaitaiStruct):
self.ihdr = self._root.IhdrChunk(self._io, self, self._root)
self.ihdr_crc = self._io.read_bytes(4)
self.chunks = []
- while not self._io.is_eof():
- self.chunks.append(self._root.Chunk(self._io, self, self._root))
-
+ while True:
+ _ = self._root.Chunk(self._io, self, self._root)
+ self.chunks.append(_)
+ if ((_.type == u"IEND") or (self._io.is_eof())) :
+ break
class Rgb(KaitaiStruct):
def __init__(self, _io, _parent=None, _root=None):
diff --git a/mitmproxy/options.py b/mitmproxy/options.py
index 59d44a5d..db276a51 100644
--- a/mitmproxy/options.py
+++ b/mitmproxy/options.py
@@ -386,6 +386,10 @@ class Options(optmanager.OptManager):
choices=sorted(console_layouts),
)
self.add_option(
+ "console_layout_headers", bool, True,
+ "Show layout comonent headers",
+ )
+ self.add_option(
"console_focus_follow", bool, False,
"Focus follows new flows."
)
diff --git a/mitmproxy/platform/openbsd.py b/mitmproxy/platform/openbsd.py
index e8f5ff8e..302dc11b 100644
--- a/mitmproxy/platform/openbsd.py
+++ b/mitmproxy/platform/openbsd.py
@@ -1,2 +1,3 @@
+
def original_addr(csock):
return csock.getsockname()
diff --git a/mitmproxy/tools/cmdline.py b/mitmproxy/tools/cmdline.py
index 5711ce73..97b04e74 100644
--- a/mitmproxy/tools/cmdline.py
+++ b/mitmproxy/tools/cmdline.py
@@ -109,6 +109,7 @@ def mitmproxy(opts):
common_options(parser, opts)
opts.make_parser(parser, "console_layout")
+ opts.make_parser(parser, "console_layout_headers")
group = parser.add_argument_group(
"Filters",
"See help in mitmproxy for filter expression syntax."
diff --git a/mitmproxy/tools/console/commands.py b/mitmproxy/tools/console/commands.py
index 76827a99..e4535314 100644
--- a/mitmproxy/tools/console/commands.py
+++ b/mitmproxy/tools/console/commands.py
@@ -1,30 +1,12 @@
import urwid
import blinker
import textwrap
-from mitmproxy.tools.console import common
+from mitmproxy.tools.console import layoutwidget
from mitmproxy.tools.console import signals
HELP_HEIGHT = 5
-footer = [
- ('heading_key', "enter"), ":edit ",
- ('heading_key', "?"), ":help ",
-]
-
-
-def _mkhelp():
- text = []
- keys = [
- ("enter", "execute command"),
- ]
- text.extend(common.format_keyvals(keys, key="key", val="text", indent=4))
- return text
-
-
-help_context = _mkhelp()
-
-
def fcol(s, width, attr):
s = str(s)
return (
@@ -117,7 +99,7 @@ class CommandsList(urwid.ListBox):
super().__init__(self.walker)
def keypress(self, size, key):
- if key == "enter":
+ if key == "m_select":
foc, idx = self.get_focus()
signals.status_prompt_command.send(partial=foc.cmd.path + " ")
elif key == "m_start":
@@ -151,7 +133,8 @@ class CommandHelp(urwid.Frame):
self.set_body(self.widget(txt))
-class Commands(urwid.Pile):
+class Commands(urwid.Pile, layoutwidget.LayoutWidget):
+ title = "Commands"
keyctx = "commands"
def __init__(self, master):
@@ -165,7 +148,7 @@ class Commands(urwid.Pile):
self.master = master
def keypress(self, size, key):
- if key == "tab":
+ if key == "m_next":
self.focus_position = (
self.focus_position + 1
) % len(self.widget_list)
diff --git a/mitmproxy/tools/console/defaultkeys.py b/mitmproxy/tools/console/defaultkeys.py
new file mode 100644
index 00000000..d5b868d0
--- /dev/null
+++ b/mitmproxy/tools/console/defaultkeys.py
@@ -0,0 +1,139 @@
+
+def map(km):
+ km.add(":", "console.command ''", ["global"], "Command prompt")
+ km.add("?", "console.view.help", ["global"], "View help")
+ km.add("C", "console.view.commands", ["global"], "View commands")
+ km.add("O", "console.view.options", ["global"], "View options")
+ km.add("E", "console.view.eventlog", ["global"], "View event log")
+ km.add("Q", "console.exit", ["global"], "Exit immediately")
+ km.add("q", "console.view.pop", ["global"], "Exit the current view")
+ km.add("-", "console.layout.cycle", ["global"], "Cycle to next layout")
+ km.add("shift tab", "console.panes.next", ["global"], "Focus next layout pane")
+ km.add("P", "console.view.flow @focus", ["global"], "View flow details")
+
+ km.add("g", "console.nav.start", ["global"], "Go to start")
+ km.add("G", "console.nav.end", ["global"], "Go to end")
+ km.add("k", "console.nav.up", ["global"], "Up")
+ km.add("j", "console.nav.down", ["global"], "Down")
+ km.add("l", "console.nav.right", ["global"], "Right")
+ km.add("h", "console.nav.left", ["global"], "Left")
+ km.add("tab", "console.nav.next", ["global"], "Next")
+ km.add("enter", "console.nav.select", ["global"], "Select")
+ km.add(" ", "console.nav.pagedown", ["global"], "Page down")
+ km.add("ctrl f", "console.nav.pagedown", ["global"], "Page down")
+ km.add("ctrl b", "console.nav.pageup", ["global"], "Page up")
+
+ km.add("i", "console.command set intercept=", ["global"], "Set intercept")
+ km.add("W", "console.command set save_stream_file=", ["global"], "Stream to file")
+ km.add("A", "flow.resume @all", ["flowlist", "flowview"], "Resume all intercepted flows")
+ km.add("a", "flow.resume @focus", ["flowlist", "flowview"], "Resume this intercepted flow")
+ km.add(
+ "b", "console.command cut.save s.content|@focus ''",
+ ["flowlist", "flowview"],
+ "Save response body to file"
+ )
+ km.add("d", "view.remove @focus", ["flowlist", "flowview"], "Delete flow from view")
+ km.add("D", "view.duplicate @focus", ["flowlist", "flowview"], "Duplicate flow")
+ km.add(
+ "e",
+ "console.choose.cmd Format export.formats "
+ "console.command export.file {choice} @focus ''",
+ ["flowlist", "flowview"],
+ "Export this flow to file"
+ )
+ km.add("f", "console.command set view_filter=", ["flowlist"], "Set view filter")
+ km.add("F", "set console_focus_follow=toggle", ["flowlist"], "Set focus follow")
+ km.add(
+ "ctrl l",
+ "console.command cut.clip ",
+ ["flowlist", "flowview"],
+ "Send cuts to clipboard"
+ )
+ km.add("L", "console.command view.load ", ["flowlist"], "Load flows from file")
+ km.add("m", "flow.mark.toggle @focus", ["flowlist"], "Toggle mark on this flow")
+ km.add("M", "view.marked.toggle", ["flowlist"], "Toggle viewing marked flows")
+ km.add(
+ "n",
+ "console.command view.create get https://google.com",
+ ["flowlist"],
+ "Create a new flow"
+ )
+ km.add(
+ "o",
+ "console.choose.cmd Order view.order.options "
+ "set console_order={choice}",
+ ["flowlist"],
+ "Set flow list order"
+ )
+ km.add("r", "replay.client @focus", ["flowlist", "flowview"], "Replay this flow")
+ km.add("S", "console.command replay.server ", ["flowlist"], "Start server replay")
+ km.add("v", "set console_order_reversed=toggle", ["flowlist"], "Reverse flow list order")
+ km.add("U", "flow.mark @all false", ["flowlist"], "Un-set all marks")
+ km.add("w", "console.command save.file @shown ", ["flowlist"], "Save listed flows to file")
+ km.add("V", "flow.revert @focus", ["flowlist", "flowview"], "Revert changes to this flow")
+ km.add("X", "flow.kill @focus", ["flowlist"], "Kill this flow")
+ km.add("z", "view.remove @all", ["flowlist"], "Clear flow list")
+ km.add("Z", "view.remove @hidden", ["flowlist"], "Purge all flows not showing")
+ km.add(
+ "|",
+ "console.command script.run @focus ",
+ ["flowlist", "flowview"],
+ "Run a script on this flow"
+ )
+
+ km.add(
+ "e",
+ "console.choose.cmd Part console.edit.focus.options "
+ "console.edit.focus {choice}",
+ ["flowview"],
+ "Edit a flow component"
+ )
+ km.add(
+ "f",
+ "view.setval.toggle @focus fullcontents",
+ ["flowview"],
+ "Toggle viewing full contents on this flow",
+ )
+ km.add("w", "console.command save.file @focus ", ["flowview"], "Save flow to file")
+ km.add(" ", "view.focus.next", ["flowview"], "Go to next flow")
+
+ km.add(
+ "v",
+ "console.choose \"View Part\" request,response "
+ "console.bodyview @focus {choice}",
+ ["flowview"],
+ "View flow body in an external viewer"
+ )
+ km.add("p", "view.focus.prev", ["flowview"], "Go to previous flow")
+ km.add("m", "console.flowview.mode.set", ["flowview"], "Set flow view mode")
+ km.add(
+ "z",
+ "console.choose \"Part\" request,response "
+ "flow.encode.toggle @focus {choice}",
+ ["flowview"],
+ "Encode/decode flow body"
+ )
+
+ km.add("L", "console.command options.load ", ["options"], "Load from file")
+ km.add("S", "console.command options.save ", ["options"], "Save to file")
+ km.add("D", "options.reset", ["options"], "Reset all options")
+ km.add("d", "console.options.reset.current", ["options"], "Reset this option")
+
+ km.add("a", "console.grideditor.add", ["grideditor"], "Add a row after cursor")
+ km.add("A", "console.grideditor.insert", ["grideditor"], "Insert a row before cursor")
+ km.add("d", "console.grideditor.delete", ["grideditor"], "Delete this row")
+ km.add(
+ "r",
+ "console.command console.grideditor.readfile",
+ ["grideditor"],
+ "Read unescaped data from file"
+ )
+ km.add(
+ "R",
+ "console.command console.grideditor.readfile_escaped",
+ ["grideditor"],
+ "Read a Python-style escaped string from file"
+ )
+ km.add("e", "console.grideditor.editor", ["grideditor"], "Edit in external editor")
+
+ km.add("z", "console.eventlog.clear", ["eventlog"], "Clear")
diff --git a/mitmproxy/tools/console/eventlog.py b/mitmproxy/tools/console/eventlog.py
index 0b8a3f8c..5fdada9f 100644
--- a/mitmproxy/tools/console/eventlog.py
+++ b/mitmproxy/tools/console/eventlog.py
@@ -1,5 +1,6 @@
import urwid
from mitmproxy.tools.console import signals
+from mitmproxy.tools.console import layoutwidget
EVENTLOG_SIZE = 10000
@@ -8,24 +9,23 @@ class LogBufferWalker(urwid.SimpleListWalker):
pass
-class EventLog(urwid.ListBox):
+class EventLog(urwid.ListBox, layoutwidget.LayoutWidget):
keyctx = "eventlog"
+ title = "Events"
def __init__(self, master):
self.walker = LogBufferWalker([])
self.master = master
urwid.ListBox.__init__(self, self.walker)
signals.sig_add_log.connect(self.sig_add_log)
+ signals.sig_clear_log.connect(self.sig_clear_log)
def set_focus(self, index):
if 0 <= index < len(self.walker):
super().set_focus(index)
def keypress(self, size, key):
- if key == "z":
- self.master.clear_events()
- key = None
- elif key == "m_end":
+ if key == "m_end":
self.set_focus(len(self.walker) - 1)
elif key == "m_start":
self.set_focus(0)
@@ -43,5 +43,5 @@ class EventLog(urwid.ListBox):
if self.master.options.console_focus_follow:
self.walker.set_focus(len(self.walker) - 1)
- def clear_events(self):
+ def sig_clear_log(self, sender):
self.walker[:] = []
diff --git a/mitmproxy/tools/console/flowlist.py b/mitmproxy/tools/console/flowlist.py
index 4184eeb4..f00ed9fa 100644
--- a/mitmproxy/tools/console/flowlist.py
+++ b/mitmproxy/tools/console/flowlist.py
@@ -1,52 +1,10 @@
import urwid
from mitmproxy.tools.console import common
+from mitmproxy.tools.console import layoutwidget
import mitmproxy.tools.console.master # noqa
-def _mkhelp():
- text = []
- keys = [
- ("A", "accept all intercepted flows"),
- ("a", "accept this intercepted flow"),
- ("b", "save request/response body"),
- ("C", "export flow to clipboard"),
- ("d", "delete flow"),
- ("D", "duplicate flow"),
- ("e", "toggle eventlog"),
- ("E", "export flow to file"),
- ("f", "filter view"),
- ("F", "toggle follow flow list"),
- ("L", "load saved flows"),
- ("m", "toggle flow mark"),
- ("M", "toggle marked flow view"),
- ("n", "create a new request"),
- ("o", "set flow order"),
- ("r", "replay request"),
- ("S", "server replay request/s"),
- ("U", "unmark all marked flows"),
- ("v", "reverse flow order"),
- ("V", "revert changes to request"),
- ("w", "save flows "),
- ("W", "stream flows to file"),
- ("X", "kill and delete flow, even if it's mid-intercept"),
- ("z", "clear flow list or eventlog"),
- ("Z", "clear unmarked flows"),
- ("tab", "tab between eventlog and flow list"),
- ("enter", "view flow"),
- ("|", "run script on this flow"),
- ]
- text.extend(common.format_keyvals(keys, key="key", val="text", indent=4))
- return text
-
-
-help_context = _mkhelp()
-
-footer = [
- ('heading_key', "?"), ":help ",
-]
-
-
class FlowItem(urwid.WidgetWrap):
def __init__(self, master, flow):
@@ -69,7 +27,7 @@ class FlowItem(urwid.WidgetWrap):
def mouse_event(self, size, event, button, col, row, focus):
if event == "mouse press" and button == 1:
if self.flow.request:
- self.master.view_flow(self.flow)
+ self.master.commands.call("console.view.flow @focus")
return True
def keypress(self, xxx_todo_changeme, key):
@@ -109,7 +67,8 @@ class FlowListWalker(urwid.ListWalker):
return f, pos
-class FlowListBox(urwid.ListBox):
+class FlowListBox(urwid.ListBox, layoutwidget.LayoutWidget):
+ title = "Flows"
keyctx = "flowlist"
def __init__(
@@ -123,6 +82,8 @@ class FlowListBox(urwid.ListBox):
self.master.commands.call("view.go 0")
elif key == "m_end":
self.master.commands.call("view.go -1")
+ elif key == "m_select":
+ self.master.commands.call("console.view.flow @focus")
return urwid.ListBox.keypress(self, size, key)
def view_changed(self):
diff --git a/mitmproxy/tools/console/flowview.py b/mitmproxy/tools/console/flowview.py
index 00951610..651c4330 100644
--- a/mitmproxy/tools/console/flowview.py
+++ b/mitmproxy/tools/console/flowview.py
@@ -8,6 +8,7 @@ import urwid
from mitmproxy import contentviews
from mitmproxy import http
from mitmproxy.tools.console import common
+from mitmproxy.tools.console import layoutwidget
from mitmproxy.tools.console import flowdetailview
from mitmproxy.tools.console import searchable
from mitmproxy.tools.console import signals
@@ -19,82 +20,6 @@ class SearchError(Exception):
pass
-def _mkhelp():
- text = []
- keys = [
- ("A", "accept all intercepted flows"),
- ("a", "accept this intercepted flow"),
- ("b", "save request/response body"),
- ("C", "export flow to clipboard"),
- ("D", "duplicate flow"),
- ("d", "delete flow"),
- ("e", "edit request/response"),
- ("f", "load full body data"),
- ("m", "change body display mode for this entity\n(default mode can be changed in the options)"),
- (None,
- common.highlight_key("automatic", "a") +
- [("text", ": automatic detection")]
- ),
- (None,
- common.highlight_key("hex", "e") +
- [("text", ": Hex")]
- ),
- (None,
- common.highlight_key("html", "h") +
- [("text", ": HTML")]
- ),
- (None,
- common.highlight_key("image", "i") +
- [("text", ": Image")]
- ),
- (None,
- common.highlight_key("javascript", "j") +
- [("text", ": JavaScript")]
- ),
- (None,
- common.highlight_key("json", "s") +
- [("text", ": JSON")]
- ),
- (None,
- common.highlight_key("urlencoded", "u") +
- [("text", ": URL-encoded data")]
- ),
- (None,
- common.highlight_key("raw", "r") +
- [("text", ": raw data")]
- ),
- (None,
- common.highlight_key("xml", "x") +
- [("text", ": XML")]
- ),
- ("E", "export flow to file"),
- ("r", "replay request"),
- ("V", "revert changes to request"),
- ("v", "view body in external viewer"),
- ("w", "save all flows matching current view filter"),
- ("W", "save this flow"),
- ("x", "delete body"),
- ("z", "encode/decode a request/response"),
- ("tab", "next tab"),
- ("h, l", "previous tab, next tab"),
- ("space", "next flow"),
- ("|", "run script on this flow"),
- ("/", "search (case sensitive)"),
- ("n", "repeat search forward"),
- ("N", "repeat search backwards"),
- ]
- text.extend(common.format_keyvals(keys, key="key", val="text", indent=4))
- return text
-
-
-help_context = _mkhelp()
-
-footer = [
- ('heading_key', "?"), ":help ",
- ('heading_key', "q"), ":back ",
-]
-
-
class FlowViewHeader(urwid.WidgetWrap):
def __init__(
@@ -269,13 +194,10 @@ class FlowDetails(tabs.Tabs):
]
return searchable.Searchable(txt)
- def keypress(self, size, key):
- key = super().keypress(size, key)
- return self._w.keypress(size, key)
-
-class FlowView(urwid.Frame):
+class FlowView(urwid.Frame, layoutwidget.LayoutWidget):
keyctx = "flowview"
+ title = "Flow Details"
def __init__(self, master):
super().__init__(
diff --git a/mitmproxy/tools/console/grideditor/base.py b/mitmproxy/tools/console/grideditor/base.py
index 35ae655f..cdda3def 100644
--- a/mitmproxy/tools/console/grideditor/base.py
+++ b/mitmproxy/tools/console/grideditor/base.py
@@ -1,26 +1,29 @@
import abc
import copy
-from typing import Any
-from typing import Callable
-from typing import Container
-from typing import Iterable
-from typing import Optional
-from typing import Sequence
-from typing import Tuple
-from typing import Set # noqa
-
+import os
+import typing
import urwid
-from mitmproxy.tools.console import common
+
+from mitmproxy.utils import strutils
+from mitmproxy import exceptions
from mitmproxy.tools.console import signals
+from mitmproxy.tools.console import layoutwidget
import mitmproxy.tools.console.master # noqa
-FOOTER = [
- ('heading_key', "enter"), ":edit ",
- ('heading_key', "q"), ":back ",
-]
-FOOTER_EDITING = [
- ('heading_key', "esc"), ":stop editing ",
-]
+
+def read_file(filename: str, escaped: bool) -> typing.AnyStr:
+ filename = os.path.expanduser(filename)
+ try:
+ with open(filename, "r" if escaped else "rb") as f:
+ d = f.read()
+ except IOError as v:
+ raise exceptions.CommandError(v)
+ if escaped:
+ try:
+ d = strutils.escaped_str_to_bytes(d)
+ except ValueError:
+ raise exceptions.CommandError("Invalid Python-style string encoding.")
+ return d
class Cell(urwid.WidgetWrap):
@@ -50,27 +53,28 @@ class Column(metaclass=abc.ABCMeta):
pass
@abc.abstractmethod
- def blank(self) -> Any:
+ def blank(self) -> typing.Any:
pass
- def keypress(self, key: str, editor: "GridEditor") -> Optional[str]:
+ def keypress(self, key: str, editor: "GridEditor") -> typing.Optional[str]:
return key
class GridRow(urwid.WidgetWrap):
+
def __init__(
self,
- focused: Optional[int],
+ focused: typing.Optional[int],
editing: bool,
editor: "GridEditor",
- values: Tuple[Iterable[bytes], Container[int]]
+ values: typing.Tuple[typing.Iterable[bytes], typing.Container[int]]
) -> None:
self.focused = focused
self.editor = editor
- self.edit_col = None # type: Optional[Cell]
+ self.edit_col = None # type: typing.Optional[Cell]
errors = values[1]
- self.fields = [] # type: Sequence[Any]
+ self.fields = [] # type: typing.Sequence[typing.Any]
for i, v in enumerate(values[0]):
if focused == i and editing:
self.edit_col = self.editor.columns[i].Edit(v)
@@ -116,14 +120,14 @@ class GridWalker(urwid.ListWalker):
def __init__(
self,
- lst: Iterable[list],
+ lst: typing.Iterable[list],
editor: "GridEditor"
) -> None:
- self.lst = [(i, set()) for i in lst] # type: Sequence[Tuple[Any, Set]]
+ self.lst = [(i, set()) for i in lst] # type: typing.Sequence[typing.Tuple[typing.Any, typing.Set]]
self.editor = editor
self.focus = 0
self.focus_col = 0
- self.edit_row = None # type: Optional[GridRow]
+ self.edit_row = None # type: typing.Optional[GridRow]
def _modified(self):
self.editor.show_empty_msg()
@@ -184,12 +188,10 @@ class GridWalker(urwid.ListWalker):
self.edit_row = GridRow(
self.focus_col, True, self.editor, self.lst[self.focus]
)
- signals.footer_help.send(self, helptext=FOOTER_EDITING)
self._modified()
def stop_edit(self):
if self.edit_row:
- signals.footer_help.send(self, helptext=FOOTER)
try:
val = self.edit_row.edit_col.get_data()
except ValueError:
@@ -249,18 +251,19 @@ class GridListBox(urwid.ListBox):
FIRST_WIDTH_MAX = 40
-FIRST_WIDTH_MIN = 20
class BaseGridEditor(urwid.WidgetWrap):
+ title = ""
+ keyctx = "grideditor"
def __init__(
self,
master: "mitmproxy.tools.console.master.ConsoleMaster",
title,
columns,
- value: Any,
- callback: Callable[..., None],
+ value: typing.Any,
+ callback: typing.Callable[..., None],
*cb_args,
**cb_kwargs
) -> None:
@@ -280,36 +283,29 @@ class BaseGridEditor(urwid.WidgetWrap):
first_width = max(len(r), first_width)
self.first_width = min(first_width, FIRST_WIDTH_MAX)
- title = None
- if self.title:
- title = urwid.Text(self.title)
- title = urwid.Padding(title, align="left", width=("relative", 100))
- title = urwid.AttrWrap(title, "heading")
-
- headings = []
- for i, col in enumerate(self.columns):
- c = urwid.Text(col.heading)
- if i == 0 and len(self.columns) > 1:
- headings.append(("fixed", first_width + 2, c))
- else:
- headings.append(c)
- h = urwid.Columns(
- headings,
- dividechars=2
- )
- h = urwid.AttrWrap(h, "heading")
+ h = None
+ if any(col.heading for col in self.columns):
+ headings = []
+ for i, col in enumerate(self.columns):
+ c = urwid.Text(col.heading)
+ if i == 0 and len(self.columns) > 1:
+ headings.append(("fixed", first_width + 2, c))
+ else:
+ headings.append(c)
+ h = urwid.Columns(
+ headings,
+ dividechars=2
+ )
+ h = urwid.AttrWrap(h, "heading")
self.walker = GridWalker(self.value, self)
self.lb = GridListBox(self.walker)
- w = urwid.Frame(
- self.lb,
- header=urwid.Pile([title, h]) if title else None
- )
+ w = urwid.Frame(self.lb, header=h)
+
super().__init__(w)
- signals.footer_help.send(self, helptext="")
self.show_empty_msg()
- def view_popping(self):
+ def layout_popping(self):
res = []
for i in self.walker.lst:
if not i[1] and any([x for x in i[0]]):
@@ -323,9 +319,9 @@ class BaseGridEditor(urwid.WidgetWrap):
self._w.set_footer(
urwid.Text(
[
- ("highlight", "No values. Press "),
- ("key", "a"),
- ("highlight", " to add some."),
+ ("highlight", "No values - you should add some. Press "),
+ ("key", "?"),
+ ("highlight", " for help."),
]
)
)
@@ -335,7 +331,7 @@ class BaseGridEditor(urwid.WidgetWrap):
def keypress(self, size, key):
if self.walker.edit_row:
- if key in ["esc"]:
+ if key == "esc":
self.walker.stop_edit()
elif key == "tab":
pf, pfc = self.walker.focus, self.walker.focus_col
@@ -349,37 +345,31 @@ class BaseGridEditor(urwid.WidgetWrap):
column = self.columns[self.walker.focus_col]
if key == "m_start":
self.walker.set_focus(0)
+ elif key == "m_next":
+ self.walker.tab_next()
elif key == "m_end":
self.walker.set_focus(len(self.walker.lst) - 1)
elif key == "left":
self.walker.left()
elif key == "right":
self.walker.right()
- elif key == "tab":
- self.walker.tab_next()
- elif key == "a":
- self.walker.add()
- elif key == "A":
- self.walker.insert()
- elif key == "d":
- self.walker.delete_focus()
elif column.keypress(key, self) and not self.handle_key(key):
return self._w.keypress(size, key)
- def data_out(self, data: Sequence[list]) -> Any:
+ def data_out(self, data: typing.Sequence[list]) -> typing.Any:
"""
Called on raw list data, before data is returned through the
callback.
"""
return data
- def data_in(self, data: Any) -> Iterable[list]:
+ def data_in(self, data: typing.Any) -> typing.Iterable[list]:
"""
Called to prepare provided data.
"""
return data
- def is_error(self, col: int, val: Any) -> Optional[str]:
+ def is_error(self, col: int, val: typing.Any) -> typing.Optional[str]:
"""
Return None, or a string error message.
"""
@@ -388,60 +378,54 @@ class BaseGridEditor(urwid.WidgetWrap):
def handle_key(self, key):
return False
- def make_help(self):
- text = [
- urwid.Text([("text", "Editor control:\n")])
- ]
- keys = [
- ("A", "insert row before cursor"),
- ("a", "add row after cursor"),
- ("d", "delete row"),
- ("e", "spawn external editor on current field"),
- ("q", "save changes and exit editor"),
- ("r", "read value from file"),
- ("R", "read unescaped value from file"),
- ("esc", "save changes and exit editor"),
- ("tab", "next field"),
- ("enter", "edit field"),
- ]
- text.extend(
- common.format_keyvals(keys, key="key", val="text", indent=4)
- )
- text.append(
- urwid.Text(
- [
- "\n",
- ("text", "Values are escaped Python-style strings.\n"),
- ]
- )
- )
- return text
+ def cmd_add(self):
+ self.walker.add()
+
+ def cmd_insert(self):
+ self.walker.insert()
+
+ def cmd_delete(self):
+ self.walker.delete_focus()
+
+ def cmd_read_file(self, path):
+ self.walker.set_current_value(read_file(path, False))
+
+ def cmd_read_file_escaped(self, path):
+ self.walker.set_current_value(read_file(path, True))
+ def cmd_spawn_editor(self):
+ o = self.walker.get_current_value()
+ if o is not None:
+ n = self.master.spawn_editor(o)
+ n = strutils.clean_hanging_newline(n)
+ self.walker.set_current_value(n)
-class GridEditor(urwid.WidgetWrap):
+
+class GridEditor(BaseGridEditor):
title = None # type: str
- columns = None # type: Sequence[Column]
+ columns = None # type: typing.Sequence[Column]
+ keyctx = "grideditor"
def __init__(
self,
master: "mitmproxy.tools.console.master.ConsoleMaster",
- value: Any,
- callback: Callable[..., None],
+ value: typing.Any,
+ callback: typing.Callable[..., None],
*cb_args,
**cb_kwargs
) -> None:
super().__init__(
master,
- value,
self.title,
self.columns,
+ value,
callback,
*cb_args,
**cb_kwargs
)
-class FocusEditor(urwid.WidgetWrap):
+class FocusEditor(urwid.WidgetWrap, layoutwidget.LayoutWidget):
"""
A specialised GridEditor that edits the current focused flow.
"""
@@ -451,27 +435,11 @@ class FocusEditor(urwid.WidgetWrap):
self.master = master
self.focus_changed()
- def focus_changed(self):
- if self.master.view.focus.flow:
- self._w = BaseGridEditor(
- self.master.view.focus.flow,
- self.title,
- self.columns,
- self.get_data(self.master.view.focus.flow),
- self.set_data_update,
- self.master.view.focus.flow,
- )
- else:
- self._w = urwid.Pile([])
-
def call(self, v, name, *args, **kwargs):
f = getattr(v, name, None)
if f:
f(*args, **kwargs)
- def view_popping(self):
- self.call(self._w, "view_popping")
-
def get_data(self, flow):
"""
Retrieve the data to edit from the current flow.
@@ -487,3 +455,22 @@ class FocusEditor(urwid.WidgetWrap):
def set_data_update(self, vals, flow):
self.set_data(vals, flow)
signals.flow_change.send(self, flow = flow)
+
+ def key_responder(self):
+ return self._w
+
+ def layout_popping(self):
+ self.call(self._w, "layout_popping")
+
+ def focus_changed(self):
+ if self.master.view.focus.flow:
+ self._w = BaseGridEditor(
+ self.master,
+ self.title,
+ self.columns,
+ self.get_data(self.master.view.focus.flow),
+ self.set_data_update,
+ self.master.view.focus.flow,
+ )
+ else:
+ self._w = urwid.Pile([])
diff --git a/mitmproxy/tools/console/grideditor/col_bytes.py b/mitmproxy/tools/console/grideditor/col_bytes.py
index e4a53453..da10cbaf 100644
--- a/mitmproxy/tools/console/grideditor/col_bytes.py
+++ b/mitmproxy/tools/console/grideditor/col_bytes.py
@@ -1,34 +1,9 @@
-import os
-from typing import Callable, Optional
-
import urwid
from mitmproxy.tools.console import signals
from mitmproxy.tools.console.grideditor import base
from mitmproxy.utils import strutils
-def read_file(filename: str, callback: Callable[..., None], escaped: bool) -> Optional[str]:
- if not filename:
- return None
-
- filename = os.path.expanduser(filename)
- try:
- with open(filename, "r" if escaped else "rb") as f:
- d = f.read()
- except IOError as v:
- return str(v)
-
- if escaped:
- try:
- d = strutils.escaped_str_to_bytes(d)
- except ValueError:
- return "Invalid Python-style string encoding."
- # TODO: Refactor the status_prompt_path signal so that we
- # can raise exceptions here and return the content instead.
- callback(d)
- return None
-
-
class Column(base.Column):
def Display(self, data):
return Display(data)
@@ -40,29 +15,7 @@ class Column(base.Column):
return b""
def keypress(self, key, editor):
- if key == "r":
- if editor.walker.get_current_value() is not None:
- signals.status_prompt_path.send(
- self,
- prompt="Read file",
- callback=read_file,
- args=(editor.walker.set_current_value, True)
- )
- elif key == "R":
- if editor.walker.get_current_value() is not None:
- signals.status_prompt_path.send(
- self,
- prompt="Read unescaped file",
- callback=read_file,
- args=(editor.walker.set_current_value, False)
- )
- elif key == "e":
- o = editor.walker.get_current_value()
- if o is not None:
- n = editor.master.spawn_editor(o)
- n = strutils.clean_hanging_newline(n)
- editor.walker.set_current_value(n)
- elif key in ["enter"]:
+ if key in ["m_select"]:
editor.walker.start_edit()
else:
return key
diff --git a/mitmproxy/tools/console/grideditor/col_subgrid.py b/mitmproxy/tools/console/grideditor/col_subgrid.py
index 4db37462..95995cd2 100644
--- a/mitmproxy/tools/console/grideditor/col_subgrid.py
+++ b/mitmproxy/tools/console/grideditor/col_subgrid.py
@@ -26,7 +26,7 @@ class Column(base.Column):
expire=1000
)
return
- elif key in ["enter"]:
+ elif key == "m_select":
editor.master.view_grideditor(
self.subeditor(
editor.master,
diff --git a/mitmproxy/tools/console/grideditor/editors.py b/mitmproxy/tools/console/grideditor/editors.py
index 671e91fb..074cdb77 100644
--- a/mitmproxy/tools/console/grideditor/editors.py
+++ b/mitmproxy/tools/console/grideditor/editors.py
@@ -1,22 +1,16 @@
-import re
-
-import urwid
from mitmproxy import exceptions
-from mitmproxy import flowfilter
-from mitmproxy.addons import script
-from mitmproxy.tools.console import common
+from mitmproxy.tools.console import layoutwidget
from mitmproxy.tools.console.grideditor import base
from mitmproxy.tools.console.grideditor import col_text
from mitmproxy.tools.console.grideditor import col_bytes
from mitmproxy.tools.console.grideditor import col_subgrid
from mitmproxy.tools.console import signals
-from mitmproxy.net.http import user_agents
from mitmproxy.net.http import Headers
class QueryEditor(base.FocusEditor):
- title = "Editing query"
+ title = "Edit Query"
columns = [
col_text.Column("Key"),
col_text.Column("Value")
@@ -35,43 +29,9 @@ class HeaderEditor(base.FocusEditor):
col_bytes.Column("Value")
]
- def make_help(self):
- h = super().make_help()
- text = [
- urwid.Text([("text", "Special keys:\n")])
- ]
- keys = [
- ("U", "add User-Agent header"),
- ]
- text.extend(
- common.format_keyvals(keys, key="key", val="text", indent=4)
- )
- text.append(urwid.Text([("text", "\n")]))
- text.extend(h)
- return text
-
- def set_user_agent(self, k):
- ua = user_agents.get_by_shortcut(k)
- if ua:
- self.walker.add_value(
- [
- b"User-Agent",
- ua[2].encode()
- ]
- )
-
- def handle_key(self, key):
- if key == "U":
- signals.status_prompt_onekey.send(
- prompt="Add User-Agent header:",
- keys=[(i[0], i[1]) for i in user_agents.UASTRINGS],
- callback=self.set_user_agent,
- )
- return True
-
class RequestHeaderEditor(HeaderEditor):
- title = "Editing request headers"
+ title = "Edit Request Headers"
def get_data(self, flow):
return flow.request.headers.fields
@@ -81,7 +41,7 @@ class RequestHeaderEditor(HeaderEditor):
class ResponseHeaderEditor(HeaderEditor):
- title = "Editing response headers"
+ title = "Edit Response Headers"
def get_data(self, flow):
return flow.response.headers.fields
@@ -91,7 +51,7 @@ class ResponseHeaderEditor(HeaderEditor):
class RequestFormEditor(base.FocusEditor):
- title = "Editing URL-encoded form"
+ title = "Edit URL-encoded Form"
columns = [
col_text.Column("Key"),
col_text.Column("Value")
@@ -104,60 +64,10 @@ class RequestFormEditor(base.FocusEditor):
flow.request.urlencoded_form = vals
-class SetHeadersEditor(base.GridEditor):
- title = "Editing header set patterns"
- columns = [
- col_text.Column("Filter"),
- col_text.Column("Header"),
- col_text.Column("Value"),
- ]
-
- def is_error(self, col, val):
- if col == 0:
- if not flowfilter.parse(val):
- return "Invalid filter specification"
- return False
-
- def make_help(self):
- h = super().make_help()
- text = [
- urwid.Text([("text", "Special keys:\n")])
- ]
- keys = [
- ("U", "add User-Agent header"),
- ]
- text.extend(
- common.format_keyvals(keys, key="key", val="text", indent=4)
- )
- text.append(urwid.Text([("text", "\n")]))
- text.extend(h)
- return text
-
- def set_user_agent(self, k):
- ua = user_agents.get_by_shortcut(k)
- if ua:
- self.walker.add_value(
- [
- ".*",
- b"User-Agent",
- ua[2].encode()
- ]
- )
-
- def handle_key(self, key):
- if key == "U":
- signals.status_prompt_onekey.send(
- prompt="Add User-Agent header:",
- keys=[(i[0], i[1]) for i in user_agents.UASTRINGS],
- callback=self.set_user_agent,
- )
- return True
-
-
class PathEditor(base.FocusEditor):
# TODO: Next row on enter?
- title = "Editing URL path components"
+ title = "Edit Path Components"
columns = [
col_text.Column("Component"),
]
@@ -175,40 +85,8 @@ class PathEditor(base.FocusEditor):
flow.request.path_components = self.data_out(vals)
-class ScriptEditor(base.GridEditor):
- title = "Editing scripts"
- columns = [
- col_text.Column("Command"),
- ]
-
- def is_error(self, col, val):
- try:
- script.parse_command(val)
- except exceptions.OptionsError as e:
- return str(e)
-
-
-class HostPatternEditor(base.GridEditor):
- title = "Editing host patterns"
- columns = [
- col_text.Column("Regex (matched on hostname:port / ip:port)")
- ]
-
- def is_error(self, col, val):
- try:
- re.compile(val, re.IGNORECASE)
- except re.error as e:
- return "Invalid regex: %s" % str(e)
-
- def data_in(self, data):
- return [[i] for i in data]
-
- def data_out(self, data):
- return [i[0] for i in data]
-
-
class CookieEditor(base.FocusEditor):
- title = "Editing request Cookie header"
+ title = "Edit Cookies"
columns = [
col_text.Column("Name"),
col_text.Column("Value"),
@@ -242,7 +120,7 @@ class CookieAttributeEditor(base.GridEditor):
class SetCookieEditor(base.FocusEditor):
- title = "Editing response SetCookie header"
+ title = "Edit SetCookie Header"
columns = [
col_text.Column("Name"),
col_text.Column("Value"),
@@ -273,7 +151,7 @@ class SetCookieEditor(base.FocusEditor):
flow.response.cookies = self.data_out(vals)
-class OptionsEditor(base.GridEditor):
+class OptionsEditor(base.GridEditor, layoutwidget.LayoutWidget):
title = None # type: str
columns = [
col_text.Column("")
diff --git a/mitmproxy/tools/console/help.py b/mitmproxy/tools/console/help.py
index ec0c95d9..439289f6 100644
--- a/mitmproxy/tools/console/help.py
+++ b/mitmproxy/tools/console/help.py
@@ -1,65 +1,82 @@
-import platform
-
import urwid
from mitmproxy import flowfilter
from mitmproxy.tools.console import common
+from mitmproxy.tools.console import layoutwidget
+from mitmproxy.tools.console import tabs
+
-from mitmproxy import version
+class CListBox(urwid.ListBox):
+ def __init__(self, contents):
+ self.length = len(contents)
+ contents = contents[:] + [urwid.Text(["\n"])] * 5
+ super().__init__(contents)
-footer = [
- ("heading", 'mitmproxy {} (Python {}) '.format(version.VERSION, platform.python_version())),
- ('heading_key', "q"), ":back ",
-]
+ def keypress(self, size, key):
+ if key == "m_end":
+ self.set_focus(self.length - 1)
+ elif key == "m_start":
+ self.set_focus(0)
+ else:
+ return super().keypress(size, key)
-class HelpView(urwid.ListBox):
+class HelpView(tabs.Tabs, layoutwidget.LayoutWidget):
+ title = "Help"
keyctx = "help"
- def __init__(self, help_context):
- self.help_context = help_context or []
- urwid.ListBox.__init__(
- self,
- self.helptext()
+ def __init__(self, master):
+ self.master = master
+ self.helpctx = ""
+ super().__init__(
+ [
+ [self.keybindings_title, self.keybindings],
+ [self.filtexp_title, self.filtexp],
+ ]
)
- def helptext(self):
- text = []
- text.append(urwid.Text([("head", "This view:\n")]))
- text.extend(self.help_context)
-
- text.append(urwid.Text([("head", "\n\nMovement:\n")]))
- keys = [
- ("j, k", "down, up"),
- ("h, l", "left, right (in some contexts)"),
- ("g, G", "go to beginning, end"),
- ("space", "page down"),
- ("pg up/down", "page up/down"),
- ("ctrl+b/ctrl+f", "page up/down"),
- ("arrows", "up, down, left, right"),
- ]
- text.extend(
- common.format_keyvals(
- keys,
- key="key",
- val="text",
- indent=4))
-
- text.append(urwid.Text([("head", "\n\nGlobal keys:\n")]))
- keys = [
- ("i", "set interception pattern"),
- ("O", "options"),
- ("q", "quit / return to previous page"),
- ("Q", "quit without confirm prompt"),
- ("R", "replay of requests/responses from file"),
+ def keybindings_title(self):
+ return "Key Bindings"
+
+ def format_keys(self, binds):
+ kvs = []
+ for b in binds:
+ k = b.key
+ if b.key == " ":
+ k = "space"
+ kvs.append((k, b.help or b.command))
+ return common.format_keyvals(kvs)
+
+ def keybindings(self):
+ text = [
+ urwid.Text(
+ [
+ ("title", "Keybindings for this view")
+ ]
+ )
]
- text.extend(
- common.format_keyvals(keys, key="key", val="text", indent=4)
+ if self.helpctx:
+ text.extend(self.format_keys(self.master.keymap.list(self.helpctx)))
+
+ text.append(
+ urwid.Text(
+ [
+ "\n",
+ ("title", "Global Keybindings"),
+ ]
+ )
)
- text.append(urwid.Text([("head", "\n\nFilter expressions:\n")]))
- text.extend(common.format_keyvals(flowfilter.help, key="key", val="text", indent=4))
+ text.extend(self.format_keys(self.master.keymap.list("global")))
+
+ return CListBox(text)
+ def filtexp_title(self):
+ return "Filter Expressions"
+
+ def filtexp(self):
+ text = []
+ text.extend(common.format_keyvals(flowfilter.help, key="key", val="text", indent=4))
text.append(
urwid.Text(
[
@@ -81,11 +98,11 @@ class HelpView(urwid.ListBox):
text.extend(
common.format_keyvals(examples, key="key", val="text", indent=4)
)
- return text
+ return CListBox(text)
- def keypress(self, size, key):
- if key == "m_start":
- self.set_focus(0)
- elif key == "m_end":
- self.set_focus(len(self.body.contents))
- return urwid.ListBox.keypress(self, size, key)
+ def layout_pushed(self, prev):
+ """
+ We are just about to push a window onto the stack.
+ """
+ self.helpctx = prev.keyctx
+ self.show()
diff --git a/mitmproxy/tools/console/keymap.py b/mitmproxy/tools/console/keymap.py
index 62e2dcfb..b904f706 100644
--- a/mitmproxy/tools/console/keymap.py
+++ b/mitmproxy/tools/console/keymap.py
@@ -6,6 +6,7 @@ from mitmproxy.tools.console import commandeditor
SupportedContexts = {
"chooser",
"commands",
+ "eventlog",
"flowlist",
"flowview",
"global",
@@ -15,7 +16,10 @@ SupportedContexts = {
}
-Binding = collections.namedtuple("Binding", ["key", "command", "contexts"])
+Binding = collections.namedtuple(
+ "Binding",
+ ["key", "command", "contexts", "help"]
+)
class Keymap:
@@ -24,7 +28,7 @@ class Keymap:
self.keys = {}
self.bindings = []
- def add(self, key: str, command: str, contexts: typing.Sequence[str]) -> None:
+ def add(self, key: str, command: str, contexts: typing.Sequence[str], help="") -> None:
"""
Add a key to the key map. If context is empty, it's considered to be
a global binding.
@@ -35,7 +39,7 @@ class Keymap:
if c not in SupportedContexts:
raise ValueError("Unsupported context: %s" % c)
- b = Binding(key=key, command=command, contexts=contexts)
+ b = Binding(key=key, command=command, contexts=contexts, help=help)
self.bindings.append(b)
self.bind(b)
@@ -49,6 +53,11 @@ class Keymap:
return self.keys[context].get(key, None)
return None
+ def list(self, context: str) -> typing.Sequence[Binding]:
+ b = [b for b in self.bindings if context in b.contexts]
+ b.sort(key=lambda x: x.key)
+ return b
+
def handle(self, context: str, key: str) -> typing.Optional[str]:
"""
Returns the key if it has not been handled, or None.
diff --git a/mitmproxy/tools/console/layoutwidget.py b/mitmproxy/tools/console/layoutwidget.py
new file mode 100644
index 00000000..65332238
--- /dev/null
+++ b/mitmproxy/tools/console/layoutwidget.py
@@ -0,0 +1,42 @@
+
+
+class LayoutWidget:
+ """
+ All top-level layout widgets and all widgets that may be set in an
+ overlay must comply with this API.
+ """
+ # Title is only required for windows, not overlay components
+ title = ""
+ keyctx = ""
+
+ def key_responder(self):
+ """
+ Returns the object responding to key input. Usually self, but may be
+ a wrapped object.
+ """
+ return self
+
+ def focus_changed(self):
+ """
+ The view focus has changed. Layout objects should implement the API
+ rather than directly subscribing to events.
+ """
+ pass
+
+ def view_changed(self):
+ """
+ The view list has changed.
+ """
+ pass
+
+ def layout_popping(self):
+ """
+ We are just about to pop a window off the stack, or exit an overlay.
+ """
+ pass
+
+ def layout_pushed(self, prev):
+ """
+ We have just pushed a window onto the stack.
+ """
+ pass
diff --git a/mitmproxy/tools/console/master.py b/mitmproxy/tools/console/master.py
index 998d452d..ce4e4d9d 100644
--- a/mitmproxy/tools/console/master.py
+++ b/mitmproxy/tools/console/master.py
@@ -23,6 +23,7 @@ from mitmproxy import flow
from mitmproxy.addons import intercept
from mitmproxy.addons import readfile
from mitmproxy.addons import view
+from mitmproxy.tools.console import defaultkeys
from mitmproxy.tools.console import keymap
from mitmproxy.tools.console import overlay
from mitmproxy.tools.console import palettes
@@ -130,6 +131,20 @@ class ConsoleAddon:
"""
self.master.inject_key("m_end")
+ @command.command("console.nav.next")
+ def nav_next(self) -> None:
+ """
+ Go to the next navigatable item.
+ """
+ self.master.inject_key("m_next")
+
+ @command.command("console.nav.select")
+ def nav_select(self) -> None:
+ """
+ Select a navigable item for viewing or editing.
+ """
+ self.master.inject_key("m_select")
+
@command.command("console.nav.up")
def nav_up(self) -> None:
"""
@@ -322,6 +337,55 @@ class ConsoleAddon:
"console.command flow.set @focus %s " % part
)
+ def _grideditor(self):
+ gewidget = self.master.window.current("grideditor")
+ if not gewidget:
+ raise exceptions.CommandError("Not in a grideditor.")
+ return gewidget.key_responder()
+
+ @command.command("console.grideditor.add")
+ def grideditor_add(self) -> None:
+ """
+ Add a row after the cursor.
+ """
+ self._grideditor().cmd_add()
+
+ @command.command("console.grideditor.insert")
+ def grideditor_insert(self) -> None:
+ """
+ Insert a row before the cursor.
+ """
+ self._grideditor().cmd_insert()
+
+ @command.command("console.grideditor.delete")
+ def grideditor_delete(self) -> None:
+ """
+ Delete row
+ """
+ self._grideditor().cmd_delete()
+
+ @command.command("console.grideditor.readfile")
+ def grideditor_readfile(self, path: str) -> None:
+ """
+ Read a file into the currrent cell.
+ """
+ self._grideditor().cmd_read_file(path)
+
+ @command.command("console.grideditor.readfile_escaped")
+ def grideditor_readfile_escaped(self, path: str) -> None:
+ """
+ Read a file containing a Python-style escaped stringinto the
+ currrent cell.
+ """
+ self._grideditor().cmd_read_file_escaped(path)
+
+ @command.command("console.grideditor.editor")
+ def grideditor_editor(self) -> None:
+ """
+ Spawn an external editor on the current cell.
+ """
+ self._grideditor().cmd_spawn_editor()
+
@command.command("console.flowview.mode.set")
def flowview_mode_set(self) -> None:
"""
@@ -349,7 +413,7 @@ class ConsoleAddon:
"""
Get the display mode for the current flow view.
"""
- fv = self.master.window.any("flowview")
+ fv = self.master.window.current_window("flowview")
if not fv:
raise exceptions.CommandError("Not viewing a flow.")
idx = fv.body.tab_offset
@@ -362,6 +426,13 @@ class ConsoleAddon:
]
)
+ @command.command("console.eventlog.clear")
+ def eventlog_clear(self) -> None:
+ """
+ Clear the event log.
+ """
+ signals.sig_clear_log.send(self)
+
def running(self):
self.started = True
@@ -372,111 +443,6 @@ class ConsoleAddon:
signals.flow_change.send(self, flow=f)
-def default_keymap(km):
- km.add(":", "console.command ''", ["global"])
- km.add("?", "console.view.help", ["global"])
- km.add("C", "console.view.commands", ["global"])
- km.add("O", "console.view.options", ["global"])
- km.add("E", "console.view.eventlog", ["global"])
- km.add("Q", "console.exit", ["global"])
- km.add("q", "console.view.pop", ["global"])
- km.add("-", "console.layout.cycle", ["global"])
- km.add("shift tab", "console.panes.next", ["global"])
- km.add("P", "console.view.flow @focus", ["global"])
-
- km.add("g", "console.nav.start", ["global"])
- km.add("G", "console.nav.end", ["global"])
- km.add("k", "console.nav.up", ["global"])
- km.add("j", "console.nav.down", ["global"])
- km.add("l", "console.nav.right", ["global"])
- km.add("h", "console.nav.left", ["global"])
- km.add(" ", "console.nav.pagedown", ["global"])
- km.add("ctrl f", "console.nav.pagedown", ["global"])
- km.add("ctrl b", "console.nav.pageup", ["global"])
-
- km.add("i", "console.command set intercept=", ["global"])
- km.add("W", "console.command set save_stream_file=", ["global"])
- km.add("A", "flow.resume @all", ["flowlist", "flowview"])
- km.add("a", "flow.resume @focus", ["flowlist", "flowview"])
- km.add(
- "b", "console.command cut.save s.content|@focus ''",
- ["flowlist", "flowview"]
- )
- km.add("d", "view.remove @focus", ["flowlist", "flowview"])
- km.add("D", "view.duplicate @focus", ["flowlist", "flowview"])
- km.add(
- "e",
- "console.choose.cmd Format export.formats "
- "console.command export.file {choice} @focus ''",
- ["flowlist", "flowview"]
- )
- km.add("f", "console.command set view_filter=", ["flowlist"])
- km.add("F", "set console_focus_follow=toggle", ["flowlist"])
- km.add("ctrl l", "console.command cut.clip ", ["flowlist", "flowview"])
- km.add("L", "console.command view.load ", ["flowlist"])
- km.add("m", "flow.mark.toggle @focus", ["flowlist"])
- km.add("M", "view.marked.toggle", ["flowlist"])
- km.add(
- "n",
- "console.command view.create get https://google.com",
- ["flowlist"]
- )
- km.add(
- "o",
- "console.choose.cmd Order view.order.options "
- "set console_order={choice}",
- ["flowlist"]
- )
- km.add("r", "replay.client @focus", ["flowlist", "flowview"])
- km.add("S", "console.command replay.server ", ["flowlist"])
- km.add("v", "set console_order_reversed=toggle", ["flowlist"])
- km.add("U", "flow.mark @all false", ["flowlist"])
- km.add("w", "console.command save.file @shown ", ["flowlist"])
- km.add("V", "flow.revert @focus", ["flowlist", "flowview"])
- km.add("X", "flow.kill @focus", ["flowlist"])
- km.add("z", "view.remove @all", ["flowlist"])
- km.add("Z", "view.remove @hidden", ["flowlist"])
- km.add("|", "console.command script.run @focus ", ["flowlist", "flowview"])
- km.add("enter", "console.view.flow @focus", ["flowlist"])
-
- km.add(
- "e",
- "console.choose.cmd Part console.edit.focus.options "
- "console.edit.focus {choice}",
- ["flowview"]
- )
- km.add("f", "view.setval.toggle @focus fullcontents", ["flowview"])
- km.add("w", "console.command save.file @focus ", ["flowview"])
- km.add(" ", "view.focus.next", ["flowview"])
- km.add(
- "o",
- "console.choose.cmd Order view.order.options "
- "set console_order={choice}",
- ["flowlist"]
- )
-
- km.add(
- "v",
- "console.choose \"View Part\" request,response "
- "console.bodyview @focus {choice}",
- ["flowview"]
- )
- km.add("p", "view.focus.prev", ["flowview"])
- km.add("m", "console.flowview.mode.set", ["flowview"])
- km.add("tab", "console.nav.right", ["flowview"])
- km.add(
- "z",
- "console.choose \"Part\" request,response "
- "flow.encode.toggle @focus {choice}",
- ["flowview"]
- )
-
- km.add("L", "console.command options.load ", ["options"])
- km.add("S", "console.command options.save ", ["options"])
- km.add("D", "options.reset", ["options"])
- km.add("d", "console.options.reset.current", ["options"])
-
-
class ConsoleMaster(master.Master):
def __init__(self, options, server):
@@ -492,7 +458,7 @@ class ConsoleMaster(master.Master):
# This line is just for type hinting
self.options = self.options # type: Options
self.keymap = keymap.Keymap(self)
- default_keymap(self.keymap)
+ defaultkeys.map(self.keymap)
self.options.errored.connect(self.options_error)
self.view_stack = []
diff --git a/mitmproxy/tools/console/options.py b/mitmproxy/tools/console/options.py
index fee61fe5..4d55aeec 100644
--- a/mitmproxy/tools/console/options.py
+++ b/mitmproxy/tools/console/options.py
@@ -6,7 +6,7 @@ from typing import Optional, Sequence
from mitmproxy import exceptions
from mitmproxy import optmanager
-from mitmproxy.tools.console import common
+from mitmproxy.tools.console import layoutwidget
from mitmproxy.tools.console import signals
from mitmproxy.tools.console import overlay
@@ -20,28 +20,6 @@ def can_edit_inplace(opt):
return True
-footer = [
- ('heading_key', "enter"), ":edit ",
- ('heading_key', "?"), ":help ",
-]
-
-
-def _mkhelp():
- text = []
- keys = [
- ("enter", "edit option"),
- ("D", "reset all to defaults"),
- ("d", "reset this option to default"),
- ("l", "load options from file"),
- ("w", "save options to file"),
- ]
- text.extend(common.format_keyvals(keys, key="key", val="text", indent=4))
- return text
-
-
-help_context = _mkhelp()
-
-
def fcol(s, width, attr):
s = str(s)
return (
@@ -198,8 +176,10 @@ class OptionsList(urwid.ListBox):
except exceptions.OptionsError as v:
signals.status_message.send(message=str(v))
self.walker.stop_editing()
+ return None
elif key == "esc":
self.walker.stop_editing()
+ return None
else:
if key == "m_start":
self.set_focus(0)
@@ -207,7 +187,7 @@ class OptionsList(urwid.ListBox):
elif key == "m_end":
self.set_focus(len(self.walker.opts) - 1)
self.walker._modified()
- elif key == "enter":
+ elif key == "m_select":
foc, idx = self.get_focus()
if foc.opt.typespec == bool:
self.master.options.toggler(foc.opt.name)()
@@ -263,7 +243,8 @@ class OptionHelp(urwid.Frame):
self.set_body(self.widget(txt))
-class Options(urwid.Pile):
+class Options(urwid.Pile, layoutwidget.LayoutWidget):
+ title = "Options"
keyctx = "options"
def __init__(self, master):
@@ -282,7 +263,7 @@ class Options(urwid.Pile):
return foc.opt.name
def keypress(self, size, key):
- if key == "tab":
+ if key == "m_next":
self.focus_position = (
self.focus_position + 1
) % len(self.widget_list)
diff --git a/mitmproxy/tools/console/overlay.py b/mitmproxy/tools/console/overlay.py
index abfb3909..7072d00e 100644
--- a/mitmproxy/tools/console/overlay.py
+++ b/mitmproxy/tools/console/overlay.py
@@ -2,13 +2,12 @@ import math
import urwid
-from mitmproxy.tools.console import common
from mitmproxy.tools.console import signals
from mitmproxy.tools.console import grideditor
+from mitmproxy.tools.console import layoutwidget
-class SimpleOverlay(urwid.Overlay):
- keyctx = "overlay"
+class SimpleOverlay(urwid.Overlay, layoutwidget.LayoutWidget):
def __init__(self, master, widget, parent, width, valign="middle"):
self.widget = widget
@@ -22,14 +21,21 @@ class SimpleOverlay(urwid.Overlay):
height="pack"
)
- def keypress(self, size, key):
- key = super().keypress(size, key)
- if key == "esc":
- signals.pop_view_state.send(self)
- if key == "?":
- self.master.view_help(self.widget.make_help())
- else:
- return key
+ @property
+ def keyctx(self):
+ return getattr(self.widget, "keyctx")
+
+ def key_responder(self):
+ return self.widget.key_responder()
+
+ def focus_changed(self):
+ return self.widget.focus_changed()
+
+ def view_changed(self):
+ return self.widget.view_changed()
+
+ def layout_popping(self):
+ return self.widget.layout_popping()
class Choice(urwid.WidgetWrap):
@@ -81,7 +87,9 @@ class ChooserListWalker(urwid.ListWalker):
return self._get(pos, False), pos
-class Chooser(urwid.WidgetWrap):
+class Chooser(urwid.WidgetWrap, layoutwidget.LayoutWidget):
+ keyctx = "chooser"
+
def __init__(self, master, title, choices, current, callback):
self.master = master
self.choices = choices
@@ -107,22 +115,17 @@ class Chooser(urwid.WidgetWrap):
def keypress(self, size, key):
key = self.master.keymap.handle("chooser", key)
- if key == "enter":
+ if key == "m_select":
self.callback(self.choices[self.walker.index])
signals.pop_view_state.send(self)
+ elif key == "esc":
+ signals.pop_view_state.send(self)
return super().keypress(size, key)
- def make_help(self):
- text = []
- keys = [
- ("enter", "choose option"),
- ("esc", "exit chooser"),
- ]
- text.extend(common.format_keyvals(keys, key="key", val="text", indent=4))
- return text
+class OptionsOverlay(urwid.WidgetWrap, layoutwidget.LayoutWidget):
+ keyctx = "grideditor"
-class OptionsOverlay(urwid.WidgetWrap):
def __init__(self, master, name, vals, vspace):
"""
vspace: how much vertical space to keep clear
@@ -140,5 +143,8 @@ class OptionsOverlay(urwid.WidgetWrap):
)
self.width = math.ceil(cols * 0.8)
- def make_help(self):
- return self.ge.make_help()
+ def key_responder(self):
+ return self.ge.key_responder()
+
+ def layout_popping(self):
+ return self.ge.layout_popping()
diff --git a/mitmproxy/tools/console/select.py b/mitmproxy/tools/console/select.py
deleted file mode 100644
index f7e5d950..00000000
--- a/mitmproxy/tools/console/select.py
+++ /dev/null
@@ -1,120 +0,0 @@
-import urwid
-
-from mitmproxy.tools.console import common
-
-
-class _OptionWidget(urwid.WidgetWrap):
-
- def __init__(self, option, text, shortcut, active, focus):
- self.option = option
- textattr = "text"
- keyattr = "key"
- if focus and active:
- textattr = "option_active_selected"
- keyattr = "option_selected_key"
- elif focus:
- textattr = "option_selected"
- keyattr = "option_selected_key"
- elif active:
- textattr = "option_active"
- if shortcut:
- text = common.highlight_key(
- text,
- shortcut,
- textattr = textattr,
- keyattr = keyattr
- )
- opt = urwid.Text(text, align="left")
- opt = urwid.AttrWrap(opt, textattr)
- opt = urwid.Padding(opt, align = "center", width = 40)
- urwid.WidgetWrap.__init__(self, opt)
-
- def keypress(self, size, key):
- return key
-
- def selectable(self):
- return True
-
-
-class OptionWalker(urwid.ListWalker):
-
- def __init__(self, options):
- urwid.ListWalker.__init__(self)
- self.options = options
- self.focus = 0
-
- def set_focus(self, pos):
- self.focus = pos
-
- def get_focus(self):
- return self.options[self.focus].render(True), self.focus
-
- def get_next(self, pos):
- if pos >= len(self.options) - 1:
- return None, None
- return self.options[pos + 1].render(False), pos + 1
-
- def get_prev(self, pos):
- if pos <= 0:
- return None, None
- return self.options[pos - 1].render(False), pos - 1
-
-
-class Heading:
-
- def __init__(self, text):
- self.text = text
-
- def render(self, focus):
- opt = urwid.Text("\n" + self.text, align="left")
- opt = urwid.AttrWrap(opt, "title")
- opt = urwid.Padding(opt, align = "center", width = 40)
- return opt
-
-
-def _neg(*args):
- return False
-
-
-class Option:
-
- def __init__(self, text, shortcut, getstate=None, activate=None):
- self.text = text
- self.shortcut = shortcut
- self.getstate = getstate or _neg
- self.activate = activate or _neg
-
- def render(self, focus):
- return _OptionWidget(
- self,
- self.text,
- self.shortcut,
- self.getstate(),
- focus)
-
-
-class Select(urwid.ListBox):
-
- def __init__(self, options):
- self.walker = OptionWalker(options)
- urwid.ListBox.__init__(
- self,
- self.walker
- )
- self.options = options
- self.keymap = {}
- for i in options:
- if hasattr(i, "shortcut") and i.shortcut:
- if i.shortcut in self.keymap:
- raise ValueError("Duplicate shortcut key: %s" % i.shortcut)
- self.keymap[i.shortcut] = i
-
- def keypress(self, size, key):
- if key == "enter" or key == " ":
- self.get_focus()[0].option.activate()
- return None
- if key in self.keymap:
- self.keymap[key].activate()
- self.set_focus(self.options.index(self.keymap[key]))
- return None
- return super().keypress(size, key)
diff --git a/mitmproxy/tools/console/signals.py b/mitmproxy/tools/console/signals.py
index 5cbbd875..49115a5d 100644
--- a/mitmproxy/tools/console/signals.py
+++ b/mitmproxy/tools/console/signals.py
@@ -1,6 +1,9 @@
import blinker
-# Show a status message in the action bar
+# Clear the eventlog
+sig_clear_log = blinker.Signal()
+
+# Add an entry to the eventlog
sig_add_log = blinker.Signal()
@@ -33,9 +36,6 @@ call_in = blinker.Signal()
# Focus the body, footer or header of the main window
focus = blinker.Signal()
-# Set the mini help text in the footer of the main window
-footer_help = blinker.Signal()
-
# Fired when settings change
update_settings = blinker.Signal()
diff --git a/mitmproxy/tools/console/statusbar.py b/mitmproxy/tools/console/statusbar.py
index 7e471b90..a37ecbd8 100644
--- a/mitmproxy/tools/console/statusbar.py
+++ b/mitmproxy/tools/console/statusbar.py
@@ -3,7 +3,6 @@ import os.path
import urwid
from mitmproxy.tools.console import common
-from mitmproxy.tools.console import pathedit
from mitmproxy.tools.console import signals
from mitmproxy.tools.console import commandeditor
import mitmproxy.tools.console.master # noqa
@@ -39,16 +38,12 @@ class ActionBar(urwid.WidgetWrap):
self.clear()
signals.status_message.connect(self.sig_message)
signals.status_prompt.connect(self.sig_prompt)
- signals.status_prompt_path.connect(self.sig_path_prompt)
signals.status_prompt_onekey.connect(self.sig_prompt_onekey)
signals.status_prompt_command.connect(self.sig_prompt_command)
- self.last_path = ""
-
self.prompting = None
self.onekey = False
- self.pathprompt = False
def sig_message(self, sender, message, expire=1):
if self.prompting:
@@ -74,15 +69,6 @@ class ActionBar(urwid.WidgetWrap):
self._w = commandeditor.CommandEdit(partial)
self.prompting = commandeditor.CommandExecutor(self.master)
- def sig_path_prompt(self, sender, prompt, callback, args=()):
- signals.focus.send(self, section="footer")
- self._w = pathedit.PathEdit(
- self.prep_prompt(prompt),
- os.path.dirname(self.last_path)
- )
- self.pathprompt = True
- self.prompting = PromptPath(callback, args)
-
def sig_prompt_onekey(self, sender, prompt, keys, callback, args=()):
"""
Keys are a set of (word, key) tuples. The appropriate key in the
@@ -128,13 +114,10 @@ class ActionBar(urwid.WidgetWrap):
def prompt_done(self):
self.prompting = None
self.onekey = False
- self.pathprompt = False
signals.status_message.send(message="")
signals.focus.send(self, section="body")
def prompt_execute(self, txt):
- if self.pathprompt:
- self.last_path = txt
p = self.prompting
self.prompt_done()
msg = p(txt)
@@ -146,24 +129,18 @@ class StatusBar(urwid.WidgetWrap):
keyctx = ""
def __init__(
- self, master: "mitmproxy.tools.console.master.ConsoleMaster", helptext
+ self, master: "mitmproxy.tools.console.master.ConsoleMaster"
) -> None:
self.master = master
- self.helptext = helptext
self.ib = urwid.WidgetWrap(urwid.Text(""))
self.ab = ActionBar(self.master)
super().__init__(urwid.Pile([self.ib, self.ab]))
signals.update_settings.connect(self.sig_update)
signals.flowlist_change.connect(self.sig_update)
- signals.footer_help.connect(self.sig_footer_help)
master.options.changed.connect(self.sig_update)
master.view.focus.sig_change.connect(self.sig_update)
self.redraw()
- def sig_footer_help(self, sender, helptext):
- self.helptext = helptext
- self.redraw()
-
def sig_update(self, sender, updated=None):
self.redraw()
@@ -288,13 +265,7 @@ class StatusBar(urwid.WidgetWrap):
t.extend(self.get_status())
status = urwid.AttrWrap(urwid.Columns([
urwid.Text(t),
- urwid.Text(
- [
- self.helptext,
- boundaddr
- ],
- align="right"
- ),
+ urwid.Text(boundaddr, align="right"),
]), "heading")
self.ib._w = status
diff --git a/mitmproxy/tools/console/tabs.py b/mitmproxy/tools/console/tabs.py
index 93d6909e..77873086 100644
--- a/mitmproxy/tools/console/tabs.py
+++ b/mitmproxy/tools/console/tabs.py
@@ -35,7 +35,9 @@ class Tabs(urwid.WidgetWrap):
def keypress(self, size, key):
n = len(self.tabs)
- if key == "right":
+ if key == "m_next":
+ self.change_tab((self.tab_offset + 1) % n)
+ elif key == "right":
self.change_tab((self.tab_offset + 1) % n)
elif key == "left":
self.change_tab((self.tab_offset - 1) % n)
diff --git a/mitmproxy/tools/console/window.py b/mitmproxy/tools/console/window.py
index ea5b7f3b..43e5cceb 100644
--- a/mitmproxy/tools/console/window.py
+++ b/mitmproxy/tools/console/window.py
@@ -11,6 +11,17 @@ from mitmproxy.tools.console import grideditor
from mitmproxy.tools.console import eventlog
+class Header(urwid.Frame):
+ def __init__(self, widget, title, focus):
+ super().__init__(
+ widget,
+ header = urwid.AttrWrap(
+ urwid.Text(title),
+ "heading" if focus else "heading_inactive"
+ )
+ )
+
+
class WindowStack:
def __init__(self, master, base):
self.master = master
@@ -19,7 +30,7 @@ class WindowStack:
flowview = flowview.FlowView(master),
commands = commands.Commands(master),
options = options.Options(master),
- help = help.HelpView(None),
+ help = help.HelpView(master),
eventlog = eventlog.EventLog(master),
edit_focus_query = grideditor.QueryEditor(master),
@@ -34,43 +45,57 @@ class WindowStack:
self.overlay = None
def set_overlay(self, o, **kwargs):
- self.overlay = overlay.SimpleOverlay(self, o, self.top(), o.width, **kwargs)
+ self.overlay = overlay.SimpleOverlay(
+ self, o, self.top_widget(), o.width, **kwargs,
+ )
- @property
- def topwin(self):
+ def top_window(self):
+ """
+ The current top window, ignoring overlays.
+ """
return self.windows[self.stack[-1]]
- def top(self):
+ def top_widget(self):
+ """
+ The current top widget - either a window or the active overlay.
+ """
if self.overlay:
return self.overlay
- return self.topwin
+ return self.top_window()
def push(self, wname):
if self.stack[-1] == wname:
return
+ prev = self.top_window()
self.stack.append(wname)
+ self.call("layout_pushed", prev)
def pop(self, *args, **kwargs):
"""
Pop off the stack, return True if we're already at the top.
"""
+ if not self.overlay and len(self.stack) == 1:
+ return True
+ self.call("layout_popping")
if self.overlay:
self.overlay = None
- elif len(self.stack) > 1:
- self.call("view_popping")
- self.stack.pop()
else:
- return True
+ self.stack.pop()
def call(self, name, *args, **kwargs):
- f = getattr(self.topwin, name, None)
- if f:
- f(*args, **kwargs)
+ """
+ Call a function on both the top window, and the overlay if there is
+ one. If the widget has a key_responder, we call the function on the
+ responder instead.
+ """
+ getattr(self.top_window(), name)(*args, **kwargs)
+ if self.overlay:
+ getattr(self.overlay, name)(*args, **kwargs)
class Window(urwid.Frame):
def __init__(self, master):
- self.statusbar = statusbar.StatusBar(master, "")
+ self.statusbar = statusbar.StatusBar(master)
super().__init__(
None,
header = None,
@@ -90,6 +115,7 @@ class Window(urwid.Frame):
signals.push_view_state.connect(self.push)
self.master.options.subscribe(self.configure, ["console_layout"])
+ self.master.options.subscribe(self.configure, ["console_layout_headers"])
self.pane = 0
self.stacks = [
WindowStack(master, "flowlist"),
@@ -107,21 +133,33 @@ class Window(urwid.Frame):
Redraw the layout.
"""
c = self.master.options.console_layout
+ if c == "single":
+ self.pane = 0
+
+ def wrapped(idx):
+ window = self.stacks[idx].top_window()
+ widget = self.stacks[idx].top_widget()
+ if self.master.options.console_layout_headers and window.title:
+ return Header(widget, window.title, self.pane == idx)
+ else:
+ return widget
w = None
if c == "single":
- w = self.stacks[0].top()
+ w = wrapped(0)
elif c == "vertical":
w = urwid.Pile(
- [i.top() for i in self.stacks]
+ [
+ wrapped(i) for i, s in enumerate(self.stacks)
+ ]
)
else:
w = urwid.Columns(
- [i.top() for i in self.stacks], dividechars=1
+ [wrapped(i) for i, s in enumerate(self.stacks)],
+ dividechars=1
)
+
self.body = urwid.AttrWrap(w, "background")
- if c == "single":
- self.pane = 0
def flow_changed(self, sender, flow):
if self.master.view.focus.flow:
@@ -173,11 +211,18 @@ class Window(urwid.Frame):
def current(self, keyctx):
"""
+ Returns the active widget, but only the current focus or overlay has
+ a matching key context.
+ """
+ t = self.focus_stack().top_widget()
+ if t.keyctx == keyctx:
+ return t
- Returns the top window of the current stack, IF the current focus
- has a matching key context.
+ def current_window(self, keyctx):
+ """
+ Returns the active window, ignoring overlays.
"""
- t = self.focus_stack().topwin
+ t = self.focus_stack().top_window()
if t.keyctx == keyctx:
return t
@@ -185,7 +230,7 @@ class Window(urwid.Frame):
"""
Returns the top window of either stack if they match the context.
"""
- for t in [x.topwin for x in self.stacks]:
+ for t in [x.top_window() for x in self.stacks]:
if t.keyctx == keyctx:
return t
@@ -200,6 +245,7 @@ class Window(urwid.Frame):
self.pane = 0
else:
self.pane = (self.pane + 1) % len(self.stacks)
+ self.refresh()
def mouse_event(self, *args, **kwargs):
# args: (size, event, button, col, row)
@@ -222,7 +268,7 @@ class Window(urwid.Frame):
if self.focus_part == "footer":
return super().keypress(size, k)
else:
- fs = self.focus_stack().top()
+ fs = self.focus_stack().top_widget()
k = fs.keypress(size, k)
if k:
return self.master.keymap.handle(fs.keyctx, k)
diff --git a/setup.cfg b/setup.cfg
index 1ec17ecb..42be63db 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -22,7 +22,6 @@ exclude_lines =
[tool:full_coverage]
exclude =
mitmproxy/proxy/protocol/
- mitmproxy/proxy/config.py
mitmproxy/proxy/root_context.py
mitmproxy/proxy/server.py
mitmproxy/tools/
diff --git a/setup.py b/setup.py
index 54913e6f..f3a42ac5 100644
--- a/setup.py
+++ b/setup.py
@@ -78,7 +78,7 @@ setup(
"pyparsing>=2.1.3, <2.3",
"pyperclip>=1.5.22, <1.6",
"requests>=2.9.1, <3",
- "ruamel.yaml>=0.13.2, <0.15",
+ "ruamel.yaml>=0.13.2, <0.16",
"sortedcontainers>=1.5.4, <1.6",
"tornado>=4.3, <4.6",
"urwid>=1.3.1, <1.4",
diff --git a/test/mitmproxy/proxy/test_config.py b/test/mitmproxy/proxy/test_config.py
index 777ab4dd..a7da980b 100644
--- a/test/mitmproxy/proxy/test_config.py
+++ b/test/mitmproxy/proxy/test_config.py
@@ -1 +1,38 @@
-# TODO: write tests
+import pytest
+
+from mitmproxy import options
+from mitmproxy import exceptions
+from mitmproxy.proxy.config import ProxyConfig
+from mitmproxy.test import tutils
+
+
+class TestProxyConfig:
+ def test_upstream_cert_insecure(self):
+ opts = options.Options()
+ opts.add_upstream_certs_to_client_chain = True
+ with pytest.raises(exceptions.OptionsError, match="verify-upstream-cert"):
+ ProxyConfig(opts)
+
+ def test_invalid_cadir(self):
+ opts = options.Options()
+ opts.cadir = "foo"
+ with pytest.raises(exceptions.OptionsError, match="parent directory does not exist"):
+ ProxyConfig(opts)
+
+ def test_invalid_client_certs(self):
+ opts = options.Options()
+ opts.client_certs = "foo"
+ with pytest.raises(exceptions.OptionsError, match="certificate path does not exist"):
+ ProxyConfig(opts)
+
+ def test_valid_client_certs(self):
+ opts = options.Options()
+ opts.client_certs = tutils.test_data.path("mitmproxy/data/clientcert/")
+ p = ProxyConfig(opts)
+ assert p.client_certs
+
+ def test_invalid_certificate(self):
+ opts = options.Options()
+ opts.certs = [tutils.test_data.path("mitmproxy/data/dumpfile-011")]
+ with pytest.raises(exceptions.OptionsError, match="Invalid certificate format"):
+ ProxyConfig(opts)
diff --git a/test/mitmproxy/tools/console/test_help.py b/test/mitmproxy/tools/console/test_help.py
deleted file mode 100644
index 0ebc2d6a..00000000
--- a/test/mitmproxy/tools/console/test_help.py
+++ /dev/null
@@ -1,11 +0,0 @@
-import mitmproxy.tools.console.help as help
-
-from ....conftest import skip_appveyor
-
-
-@skip_appveyor
-class TestHelp:
-
- def test_helptext(self):
- h = help.HelpView(None)
- assert h.helptext()
diff --git a/test/mitmproxy/tools/console/test_keymap.py b/test/mitmproxy/tools/console/test_keymap.py
index 6a75800e..bbca4ac9 100644
--- a/test/mitmproxy/tools/console/test_keymap.py
+++ b/test/mitmproxy/tools/console/test_keymap.py
@@ -5,25 +5,28 @@ import pytest
def test_bind():
- with taddons.context() as tctx:
- km = keymap.Keymap(tctx.master)
- km.executor = mock.Mock()
+ with taddons.context() as tctx:
+ km = keymap.Keymap(tctx.master)
+ km.executor = mock.Mock()
- with pytest.raises(ValueError):
- km.add("foo", "bar", ["unsupported"])
+ with pytest.raises(ValueError):
+ km.add("foo", "bar", ["unsupported"])
- km.add("key", "str", ["options", "commands"])
- assert km.get("options", "key")
- assert km.get("commands", "key")
- assert not km.get("flowlist", "key")
+ km.add("key", "str", ["options", "commands"])
+ assert km.get("options", "key")
+ assert km.get("commands", "key")
+ assert not km.get("flowlist", "key")
+ assert len((km.list("commands"))) == 1
- km.handle("unknown", "unknown")
- assert not km.executor.called
+ km.handle("unknown", "unknown")
+ assert not km.executor.called
- km.handle("options", "key")
- assert km.executor.called
+ km.handle("options", "key")
+ assert km.executor.called
- km.add("glob", "str", ["global"])
- km.executor = mock.Mock()
- km.handle("options", "glob")
- assert km.executor.called
+ km.add("glob", "str", ["global"])
+ km.executor = mock.Mock()
+ km.handle("options", "glob")
+ assert km.executor.called
+
+ assert len((km.list("global"))) == 1
diff --git a/test/mitmproxy/tools/console/test_master.py b/test/mitmproxy/tools/console/test_master.py
index c87c9e83..7732483f 100644
--- a/test/mitmproxy/tools/console/test_master.py
+++ b/test/mitmproxy/tools/console/test_master.py
@@ -1,3 +1,5 @@
+import pytest
+
from mitmproxy.test import tflow
from mitmproxy.test import tutils
from mitmproxy.tools import console
@@ -6,6 +8,13 @@ from mitmproxy import options
from mitmproxy.tools.console import common
from ... import tservers
import urwid
+from unittest import mock
+
+
+@pytest.fixture(scope="module", autouse=True)
+def definitely_atty():
+ with mock.patch("sys.stdout.isatty", lambda: True):
+ yield
def test_format_keyvals():
diff --git a/tox.ini b/tox.ini
index c5e2d5fc..0859ddae 100644
--- a/tox.ini
+++ b/tox.ini
@@ -54,8 +54,8 @@ commands =
deps =
-rrequirements.txt
-e./release
- # The 3.2 release is broken 🎉
- # the next commit after this updates the bootloaders, which then segfault! 🎉
+ # The 3.2 release is broken
+ # the next commit after this updates the bootloaders, which then segfault!
# https://github.com/pyinstaller/pyinstaller/issues/2232
git+https://github.com/pyinstaller/pyinstaller.git@483c819d6a256b58db6740696a901bd41c313f0c; sys_platform == 'win32'
git+https://github.com/mhils/pyinstaller.git@d094401e4196b1a6a03818b80164a5f555861cef; sys_platform != 'win32'