diff options
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) @@ -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/ @@ -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(): @@ -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' |