Merge pull request #2386 from cortesi/help

console help
This commit is contained in:
Aldo Cortesi 2017-06-11 17:45:59 +12:00 committed by GitHub
commit 40703afd0a
18 changed files with 395 additions and 541 deletions

View File

@ -1,30 +1,12 @@
import urwid import urwid
import blinker import blinker
import textwrap import textwrap
from mitmproxy.tools.console import common from mitmproxy.tools.console import layoutwidget
from mitmproxy.tools.console import signals from mitmproxy.tools.console import signals
HELP_HEIGHT = 5 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): def fcol(s, width, attr):
s = str(s) s = str(s)
return ( return (
@ -151,7 +133,7 @@ class CommandHelp(urwid.Frame):
self.set_body(self.widget(txt)) self.set_body(self.widget(txt))
class Commands(urwid.Pile): class Commands(urwid.Pile, layoutwidget.LayoutWidget):
title = "Commands" title = "Commands"
keyctx = "commands" keyctx = "commands"

View File

@ -1,5 +1,6 @@
import urwid import urwid
from mitmproxy.tools.console import signals from mitmproxy.tools.console import signals
from mitmproxy.tools.console import layoutwidget
EVENTLOG_SIZE = 10000 EVENTLOG_SIZE = 10000
@ -8,7 +9,7 @@ class LogBufferWalker(urwid.SimpleListWalker):
pass pass
class EventLog(urwid.ListBox): class EventLog(urwid.ListBox, layoutwidget.LayoutWidget):
keyctx = "eventlog" keyctx = "eventlog"
title = "Events" title = "Events"

View File

@ -1,52 +1,10 @@
import urwid import urwid
from mitmproxy.tools.console import common from mitmproxy.tools.console import common
from mitmproxy.tools.console import layoutwidget
import mitmproxy.tools.console.master # noqa 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): class FlowItem(urwid.WidgetWrap):
def __init__(self, master, flow): def __init__(self, master, flow):
@ -109,7 +67,7 @@ class FlowListWalker(urwid.ListWalker):
return f, pos return f, pos
class FlowListBox(urwid.ListBox): class FlowListBox(urwid.ListBox, layoutwidget.LayoutWidget):
title = "Flows" title = "Flows"
keyctx = "flowlist" keyctx = "flowlist"

View File

@ -8,6 +8,7 @@ import urwid
from mitmproxy import contentviews from mitmproxy import contentviews
from mitmproxy import http from mitmproxy import http
from mitmproxy.tools.console import common from mitmproxy.tools.console import common
from mitmproxy.tools.console import layoutwidget
from mitmproxy.tools.console import flowdetailview from mitmproxy.tools.console import flowdetailview
from mitmproxy.tools.console import searchable from mitmproxy.tools.console import searchable
from mitmproxy.tools.console import signals from mitmproxy.tools.console import signals
@ -19,82 +20,6 @@ class SearchError(Exception):
pass 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): class FlowViewHeader(urwid.WidgetWrap):
def __init__( def __init__(
@ -274,7 +199,7 @@ class FlowDetails(tabs.Tabs):
return self._w.keypress(size, key) return self._w.keypress(size, key)
class FlowView(urwid.Frame): class FlowView(urwid.Frame, layoutwidget.LayoutWidget):
keyctx = "flowview" keyctx = "flowview"
title = "Flow Details" title = "Flow Details"

View File

@ -1,17 +1,14 @@
import abc import abc
import copy import copy
from typing import Any import os
from typing import Callable import typing
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 urwid import urwid
from mitmproxy.utils import strutils
from mitmproxy import exceptions
from mitmproxy.tools.console import common from mitmproxy.tools.console import common
from mitmproxy.tools.console import signals from mitmproxy.tools.console import signals
from mitmproxy.tools.console import layoutwidget
import mitmproxy.tools.console.master # noqa import mitmproxy.tools.console.master # noqa
FOOTER = [ FOOTER = [
@ -23,6 +20,21 @@ FOOTER_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): class Cell(urwid.WidgetWrap):
def get_data(self): def get_data(self):
""" """
@ -50,27 +62,28 @@ class Column(metaclass=abc.ABCMeta):
pass pass
@abc.abstractmethod @abc.abstractmethod
def blank(self) -> Any: def blank(self) -> typing.Any:
pass pass
def keypress(self, key: str, editor: "GridEditor") -> Optional[str]: def keypress(self, key: str, editor: "GridEditor") -> typing.Optional[str]:
return key return key
class GridRow(urwid.WidgetWrap): class GridRow(urwid.WidgetWrap):
def __init__( def __init__(
self, self,
focused: Optional[int], focused: typing.Optional[int],
editing: bool, editing: bool,
editor: "GridEditor", editor: "GridEditor",
values: Tuple[Iterable[bytes], Container[int]] values: typing.Tuple[typing.Iterable[bytes], typing.Container[int]]
) -> None: ) -> None:
self.focused = focused self.focused = focused
self.editor = editor self.editor = editor
self.edit_col = None # type: Optional[Cell] self.edit_col = None # type: typing.Optional[Cell]
errors = values[1] errors = values[1]
self.fields = [] # type: Sequence[Any] self.fields = [] # type: typing.Sequence[typing.Any]
for i, v in enumerate(values[0]): for i, v in enumerate(values[0]):
if focused == i and editing: if focused == i and editing:
self.edit_col = self.editor.columns[i].Edit(v) self.edit_col = self.editor.columns[i].Edit(v)
@ -116,14 +129,14 @@ class GridWalker(urwid.ListWalker):
def __init__( def __init__(
self, self,
lst: Iterable[list], lst: typing.Iterable[list],
editor: "GridEditor" editor: "GridEditor"
) -> None: ) -> 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.editor = editor
self.focus = 0 self.focus = 0
self.focus_col = 0 self.focus_col = 0
self.edit_row = None # type: Optional[GridRow] self.edit_row = None # type: typing.Optional[GridRow]
def _modified(self): def _modified(self):
self.editor.show_empty_msg() self.editor.show_empty_msg()
@ -253,14 +266,16 @@ FIRST_WIDTH_MIN = 20
class BaseGridEditor(urwid.WidgetWrap): class BaseGridEditor(urwid.WidgetWrap):
title = ""
keyctx = "grideditor"
def __init__( def __init__(
self, self,
master: "mitmproxy.tools.console.master.ConsoleMaster", master: "mitmproxy.tools.console.master.ConsoleMaster",
title, title,
columns, columns,
value: Any, value: typing.Any,
callback: Callable[..., None], callback: typing.Callable[..., None],
*cb_args, *cb_args,
**cb_kwargs **cb_kwargs
) -> None: ) -> None:
@ -280,36 +295,30 @@ class BaseGridEditor(urwid.WidgetWrap):
first_width = max(len(r), first_width) first_width = max(len(r), first_width)
self.first_width = min(first_width, FIRST_WIDTH_MAX) self.first_width = min(first_width, FIRST_WIDTH_MAX)
title = None h = None
if self.title: if any(col.heading for col in self.columns):
title = urwid.Text(self.title) headings = []
title = urwid.Padding(title, align="left", width=("relative", 100)) for i, col in enumerate(self.columns):
title = urwid.AttrWrap(title, "heading") c = urwid.Text(col.heading)
if i == 0 and len(self.columns) > 1:
headings = [] headings.append(("fixed", first_width + 2, c))
for i, col in enumerate(self.columns): else:
c = urwid.Text(col.heading) headings.append(c)
if i == 0 and len(self.columns) > 1: h = urwid.Columns(
headings.append(("fixed", first_width + 2, c)) headings,
else: dividechars=2
headings.append(c) )
h = urwid.Columns( h = urwid.AttrWrap(h, "heading")
headings,
dividechars=2
)
h = urwid.AttrWrap(h, "heading")
self.walker = GridWalker(self.value, self) self.walker = GridWalker(self.value, self)
self.lb = GridListBox(self.walker) self.lb = GridListBox(self.walker)
w = urwid.Frame( w = urwid.Frame(self.lb, header=h)
self.lb,
header=urwid.Pile([title, h]) if title else None
)
super().__init__(w) super().__init__(w)
signals.footer_help.send(self, helptext="") signals.footer_help.send(self, helptext="")
self.show_empty_msg() self.show_empty_msg()
def view_popping(self): def layout_popping(self):
res = [] res = []
for i in self.walker.lst: for i in self.walker.lst:
if not i[1] and any([x for x in i[0]]): if not i[1] and any([x for x in i[0]]):
@ -323,9 +332,9 @@ class BaseGridEditor(urwid.WidgetWrap):
self._w.set_footer( self._w.set_footer(
urwid.Text( urwid.Text(
[ [
("highlight", "No values. Press "), ("highlight", "No values - you should add some. Press "),
("key", "a"), ("key", "?"),
("highlight", " to add some."), ("highlight", " for help."),
] ]
) )
) )
@ -355,31 +364,23 @@ class BaseGridEditor(urwid.WidgetWrap):
self.walker.left() self.walker.left()
elif key == "right": elif key == "right":
self.walker.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): elif column.keypress(key, self) and not self.handle_key(key):
return self._w.keypress(size, 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 Called on raw list data, before data is returned through the
callback. callback.
""" """
return data 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. Called to prepare provided data.
""" """
return 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. Return None, or a string error message.
""" """
@ -417,31 +418,57 @@ class BaseGridEditor(urwid.WidgetWrap):
) )
return text return text
def cmd_next(self):
self.walker.tab_next()
class GridEditor(urwid.WidgetWrap): 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(BaseGridEditor):
title = None # type: str title = None # type: str
columns = None # type: Sequence[Column] columns = None # type: typing.Sequence[Column]
keyctx = "grideditor"
def __init__( def __init__(
self, self,
master: "mitmproxy.tools.console.master.ConsoleMaster", master: "mitmproxy.tools.console.master.ConsoleMaster",
value: Any, value: typing.Any,
callback: Callable[..., None], callback: typing.Callable[..., None],
*cb_args, *cb_args,
**cb_kwargs **cb_kwargs
) -> None: ) -> None:
super().__init__( super().__init__(
master, master,
value,
self.title, self.title,
self.columns, self.columns,
value,
callback, callback,
*cb_args, *cb_args,
**cb_kwargs **cb_kwargs
) )
class FocusEditor(urwid.WidgetWrap): class FocusEditor(urwid.WidgetWrap, layoutwidget.LayoutWidget):
""" """
A specialised GridEditor that edits the current focused flow. A specialised GridEditor that edits the current focused flow.
""" """
@ -451,27 +478,11 @@ class FocusEditor(urwid.WidgetWrap):
self.master = master self.master = master
self.focus_changed() 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): def call(self, v, name, *args, **kwargs):
f = getattr(v, name, None) f = getattr(v, name, None)
if f: if f:
f(*args, **kwargs) f(*args, **kwargs)
def view_popping(self):
self.call(self._w, "view_popping")
def get_data(self, flow): def get_data(self, flow):
""" """
Retrieve the data to edit from the current flow. Retrieve the data to edit from the current flow.
@ -487,3 +498,22 @@ class FocusEditor(urwid.WidgetWrap):
def set_data_update(self, vals, flow): def set_data_update(self, vals, flow):
self.set_data(vals, flow) self.set_data(vals, flow)
signals.flow_change.send(self, flow = 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([])

View File

@ -1,34 +1,9 @@
import os
from typing import Callable, Optional
import urwid import urwid
from mitmproxy.tools.console import signals from mitmproxy.tools.console import signals
from mitmproxy.tools.console.grideditor import base from mitmproxy.tools.console.grideditor import base
from mitmproxy.utils import strutils 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): class Column(base.Column):
def Display(self, data): def Display(self, data):
return Display(data) return Display(data)
@ -40,29 +15,7 @@ class Column(base.Column):
return b"" return b""
def keypress(self, key, editor): def keypress(self, key, editor):
if key == "r": if key in ["enter"]:
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"]:
editor.walker.start_edit() editor.walker.start_edit()
else: else:
return key return key

View File

@ -1,17 +1,14 @@
import re
import urwid import urwid
from mitmproxy import exceptions from mitmproxy import exceptions
from mitmproxy import flowfilter
from mitmproxy.addons import script
from mitmproxy.tools.console import common 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 base
from mitmproxy.tools.console.grideditor import col_text from mitmproxy.tools.console.grideditor import col_text
from mitmproxy.tools.console.grideditor import col_bytes from mitmproxy.tools.console.grideditor import col_bytes
from mitmproxy.tools.console.grideditor import col_subgrid from mitmproxy.tools.console.grideditor import col_subgrid
from mitmproxy.tools.console import signals from mitmproxy.tools.console import signals
from mitmproxy.net.http import user_agents
from mitmproxy.net.http import Headers from mitmproxy.net.http import Headers
@ -41,7 +38,6 @@ class HeaderEditor(base.FocusEditor):
urwid.Text([("text", "Special keys:\n")]) urwid.Text([("text", "Special keys:\n")])
] ]
keys = [ keys = [
("U", "add User-Agent header"),
] ]
text.extend( text.extend(
common.format_keyvals(keys, key="key", val="text", indent=4) common.format_keyvals(keys, key="key", val="text", indent=4)
@ -50,25 +46,6 @@ class HeaderEditor(base.FocusEditor):
text.extend(h) text.extend(h)
return text 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): class RequestHeaderEditor(HeaderEditor):
title = "Edit Request Headers" title = "Edit Request Headers"
@ -104,56 +81,6 @@ class RequestFormEditor(base.FocusEditor):
flow.request.urlencoded_form = vals 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): class PathEditor(base.FocusEditor):
# TODO: Next row on enter? # TODO: Next row on enter?
@ -175,38 +102,6 @@ class PathEditor(base.FocusEditor):
flow.request.path_components = self.data_out(vals) 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): class CookieEditor(base.FocusEditor):
title = "Edit Cookies" title = "Edit Cookies"
columns = [ columns = [
@ -273,7 +168,7 @@ class SetCookieEditor(base.FocusEditor):
flow.response.cookies = self.data_out(vals) flow.response.cookies = self.data_out(vals)
class OptionsEditor(base.GridEditor): class OptionsEditor(base.GridEditor, layoutwidget.LayoutWidget):
title = None # type: str title = None # type: str
columns = [ columns = [
col_text.Column("") col_text.Column("")

View File

@ -1,66 +1,67 @@
import platform
import urwid import urwid
from mitmproxy import flowfilter from mitmproxy import flowfilter
from mitmproxy.tools.console import common from mitmproxy.tools.console import common
from mitmproxy.tools.console import layoutwidget
from mitmproxy import version from mitmproxy.tools.console import tabs
footer = [
("heading", 'mitmproxy {} (Python {}) '.format(version.VERSION, platform.python_version())),
('heading_key', "q"), ":back ",
]
class HelpView(urwid.ListBox): class HelpView(tabs.Tabs, layoutwidget.LayoutWidget):
title = "Help" title = "Help"
keyctx = "help" keyctx = "help"
def __init__(self, help_context): def __init__(self, master):
self.help_context = help_context or [] self.master = master
urwid.ListBox.__init__( self.helpctx = ""
self, super().__init__(
self.helptext() [
[self.keybindings_title, self.keybindings],
[self.filtexp_title, self.filtexp],
]
) )
def helptext(self): 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.command))
return common.format_keyvals(kvs)
def keybindings(self):
text = [
urwid.Text(
[
("title", "Keybindings for this view")
]
)
]
if self.helpctx:
text.extend(self.format_keys(self.master.keymap.list(self.helpctx)))
text.append(
urwid.Text(
[
"\n",
("title", "Global Keybindings"),
]
)
)
text.extend(self.format_keys(self.master.keymap.list("global")))
return urwid.ListBox(text)
def filtexp_title(self):
return "Filter Expressions"
def filtexp(self):
text = [] 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"),
]
text.extend(
common.format_keyvals(keys, key="key", val="text", indent=4)
)
text.append(urwid.Text([("head", "\n\nFilter expressions:\n")]))
text.extend(common.format_keyvals(flowfilter.help, key="key", val="text", indent=4)) text.extend(common.format_keyvals(flowfilter.help, key="key", val="text", indent=4))
text.append( text.append(
urwid.Text( urwid.Text(
[ [
@ -82,11 +83,11 @@ class HelpView(urwid.ListBox):
text.extend( text.extend(
common.format_keyvals(examples, key="key", val="text", indent=4) common.format_keyvals(examples, key="key", val="text", indent=4)
) )
return text return urwid.ListBox(text)
def keypress(self, size, key): def layout_pushed(self, prev):
if key == "m_start": """
self.set_focus(0) We are just about to push a window onto the stack.
elif key == "m_end": """
self.set_focus(len(self.body.contents)) self.helpctx = prev.keyctx
return urwid.ListBox.keypress(self, size, key) self.show()

View File

@ -49,6 +49,11 @@ class Keymap:
return self.keys[context].get(key, None) return self.keys[context].get(key, None)
return 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]: def handle(self, context: str, key: str) -> typing.Optional[str]:
""" """
Returns the key if it has not been handled, or None. Returns the key if it has not been handled, or None.

View File

@ -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

View File

@ -322,6 +322,62 @@ class ConsoleAddon:
"console.command flow.set @focus %s " % part "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.next")
def grideditor_next(self) -> None:
"""
Go to next cell.
"""
self._grideditor().cmd_next()
@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") @command.command("console.flowview.mode.set")
def flowview_mode_set(self) -> None: def flowview_mode_set(self) -> None:
""" """
@ -349,7 +405,7 @@ class ConsoleAddon:
""" """
Get the display mode for the current flow view. 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: if not fv:
raise exceptions.CommandError("Not viewing a flow.") raise exceptions.CommandError("Not viewing a flow.")
idx = fv.body.tab_offset idx = fv.body.tab_offset
@ -476,6 +532,14 @@ def default_keymap(km):
km.add("D", "options.reset", ["options"]) km.add("D", "options.reset", ["options"])
km.add("d", "console.options.reset.current", ["options"]) km.add("d", "console.options.reset.current", ["options"])
km.add("a", "console.grideditor.add", ["grideditor"])
km.add("A", "console.grideditor.insert", ["grideditor"])
km.add("tab", "console.grideditor.next", ["grideditor"])
km.add("d", "console.grideditor.delete", ["grideditor"])
km.add("r", "console.command console.grideditor.readfile", ["grideditor"])
km.add("R", "console.command console.grideditor.readfile_escaped", ["grideditor"])
km.add("e", "console.grideditor.editor", ["grideditor"])
class ConsoleMaster(master.Master): class ConsoleMaster(master.Master):

View File

@ -6,7 +6,7 @@ from typing import Optional, Sequence
from mitmproxy import exceptions from mitmproxy import exceptions
from mitmproxy import optmanager 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 signals
from mitmproxy.tools.console import overlay from mitmproxy.tools.console import overlay
@ -20,28 +20,6 @@ def can_edit_inplace(opt):
return True 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): def fcol(s, width, attr):
s = str(s) s = str(s)
return ( return (
@ -263,7 +241,7 @@ class OptionHelp(urwid.Frame):
self.set_body(self.widget(txt)) self.set_body(self.widget(txt))
class Options(urwid.Pile): class Options(urwid.Pile, layoutwidget.LayoutWidget):
title = "Options" title = "Options"
keyctx = "options" keyctx = "options"

View File

@ -5,10 +5,10 @@ import urwid
from mitmproxy.tools.console import common from mitmproxy.tools.console import common
from mitmproxy.tools.console import signals from mitmproxy.tools.console import signals
from mitmproxy.tools.console import grideditor from mitmproxy.tools.console import grideditor
from mitmproxy.tools.console import layoutwidget
class SimpleOverlay(urwid.Overlay): class SimpleOverlay(urwid.Overlay, layoutwidget.LayoutWidget):
keyctx = "overlay"
def __init__(self, master, widget, parent, width, valign="middle"): def __init__(self, master, widget, parent, width, valign="middle"):
self.widget = widget self.widget = widget
@ -22,14 +22,21 @@ class SimpleOverlay(urwid.Overlay):
height="pack" height="pack"
) )
def keypress(self, size, key): @property
key = super().keypress(size, key) def keyctx(self):
if key == "esc": return getattr(self.widget, "keyctx")
signals.pop_view_state.send(self)
if key == "?": def key_responder(self):
self.master.view_help(self.widget.make_help()) return self.widget.key_responder()
else:
return key 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): class Choice(urwid.WidgetWrap):
@ -81,7 +88,9 @@ class ChooserListWalker(urwid.ListWalker):
return self._get(pos, False), pos 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): def __init__(self, master, title, choices, current, callback):
self.master = master self.master = master
self.choices = choices self.choices = choices
@ -122,7 +131,9 @@ class Chooser(urwid.WidgetWrap):
return text return text
class OptionsOverlay(urwid.WidgetWrap): class OptionsOverlay(urwid.WidgetWrap, layoutwidget.LayoutWidget):
keyctx = "grideditor"
def __init__(self, master, name, vals, vspace): def __init__(self, master, name, vals, vspace):
""" """
vspace: how much vertical space to keep clear vspace: how much vertical space to keep clear
@ -142,3 +153,9 @@ class OptionsOverlay(urwid.WidgetWrap):
def make_help(self): def make_help(self):
return self.ge.make_help() return self.ge.make_help()
def key_responder(self):
return self.ge.key_responder()
def layout_popping(self):
return self.ge.layout_popping()

View File

@ -146,24 +146,18 @@ class StatusBar(urwid.WidgetWrap):
keyctx = "" keyctx = ""
def __init__( def __init__(
self, master: "mitmproxy.tools.console.master.ConsoleMaster", helptext self, master: "mitmproxy.tools.console.master.ConsoleMaster"
) -> None: ) -> None:
self.master = master self.master = master
self.helptext = helptext
self.ib = urwid.WidgetWrap(urwid.Text("")) self.ib = urwid.WidgetWrap(urwid.Text(""))
self.ab = ActionBar(self.master) self.ab = ActionBar(self.master)
super().__init__(urwid.Pile([self.ib, self.ab])) super().__init__(urwid.Pile([self.ib, self.ab]))
signals.update_settings.connect(self.sig_update) signals.update_settings.connect(self.sig_update)
signals.flowlist_change.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.options.changed.connect(self.sig_update)
master.view.focus.sig_change.connect(self.sig_update) master.view.focus.sig_change.connect(self.sig_update)
self.redraw() self.redraw()
def sig_footer_help(self, sender, helptext):
self.helptext = helptext
self.redraw()
def sig_update(self, sender, updated=None): def sig_update(self, sender, updated=None):
self.redraw() self.redraw()
@ -288,13 +282,7 @@ class StatusBar(urwid.WidgetWrap):
t.extend(self.get_status()) t.extend(self.get_status())
status = urwid.AttrWrap(urwid.Columns([ status = urwid.AttrWrap(urwid.Columns([
urwid.Text(t), urwid.Text(t),
urwid.Text( urwid.Text(boundaddr, align="right"),
[
self.helptext,
boundaddr
],
align="right"
),
]), "heading") ]), "heading")
self.ib._w = status self.ib._w = status

View File

@ -30,7 +30,7 @@ class WindowStack:
flowview = flowview.FlowView(master), flowview = flowview.FlowView(master),
commands = commands.Commands(master), commands = commands.Commands(master),
options = options.Options(master), options = options.Options(master),
help = help.HelpView(None), help = help.HelpView(master),
eventlog = eventlog.EventLog(master), eventlog = eventlog.EventLog(master),
edit_focus_query = grideditor.QueryEditor(master), edit_focus_query = grideditor.QueryEditor(master),
@ -45,43 +45,57 @@ class WindowStack:
self.overlay = None self.overlay = None
def set_overlay(self, o, **kwargs): 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 top_window(self):
def topwin(self): """
The current top window, ignoring overlays.
"""
return self.windows[self.stack[-1]] 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: if self.overlay:
return self.overlay return self.overlay
return self.topwin return self.top_window()
def push(self, wname): def push(self, wname):
if self.stack[-1] == wname: if self.stack[-1] == wname:
return return
prev = self.top_window()
self.stack.append(wname) self.stack.append(wname)
self.call("layout_pushed", prev)
def pop(self, *args, **kwargs): def pop(self, *args, **kwargs):
""" """
Pop off the stack, return True if we're already at the top. 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: if self.overlay:
self.overlay = None self.overlay = None
elif len(self.stack) > 1:
self.call("view_popping")
self.stack.pop()
else: else:
return True self.stack.pop()
def call(self, name, *args, **kwargs): def call(self, name, *args, **kwargs):
f = getattr(self.topwin, name, None) """
if f: Call a function on both the top window, and the overlay if there is
f(*args, **kwargs) 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): class Window(urwid.Frame):
def __init__(self, master): def __init__(self, master):
self.statusbar = statusbar.StatusBar(master, "") self.statusbar = statusbar.StatusBar(master)
super().__init__( super().__init__(
None, None,
header = None, header = None,
@ -122,24 +136,26 @@ class Window(urwid.Frame):
if c == "single": if c == "single":
self.pane = 0 self.pane = 0
def wrap(w, idx): def wrapped(idx):
if self.master.options.console_layout_headers and hasattr(w, "title"): window = self.stacks[idx].top_window()
return Header(w, w.title, self.pane == idx) 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: else:
return w return widget
w = None w = None
if c == "single": if c == "single":
w = wrap(self.stacks[0].top(), 0) w = wrapped(0)
elif c == "vertical": elif c == "vertical":
w = urwid.Pile( w = urwid.Pile(
[ [
wrap(s.top(), i) for i, s in enumerate(self.stacks) wrapped(i) for i, s in enumerate(self.stacks)
] ]
) )
else: else:
w = urwid.Columns( w = urwid.Columns(
[wrap(s.top(), i) for i, s in enumerate(self.stacks)], [wrapped(i) for i, s in enumerate(self.stacks)],
dividechars=1 dividechars=1
) )
@ -195,11 +211,18 @@ class Window(urwid.Frame):
def current(self, keyctx): def current(self, keyctx):
""" """
Returns the active widget, but only the current focus or overlay has
Returns the top window of the current stack, IF the current focus a matching key context.
has a matching key context.
""" """
t = self.focus_stack().topwin t = self.focus_stack().top_widget()
if t.keyctx == keyctx:
return t
def current_window(self, keyctx):
"""
Returns the active window, ignoring overlays.
"""
t = self.focus_stack().top_window()
if t.keyctx == keyctx: if t.keyctx == keyctx:
return t return t
@ -207,7 +230,7 @@ class Window(urwid.Frame):
""" """
Returns the top window of either stack if they match the context. 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: if t.keyctx == keyctx:
return t return t
@ -245,7 +268,7 @@ class Window(urwid.Frame):
if self.focus_part == "footer": if self.focus_part == "footer":
return super().keypress(size, k) return super().keypress(size, k)
else: else:
fs = self.focus_stack().top() fs = self.focus_stack().top_widget()
k = fs.keypress(size, k) k = fs.keypress(size, k)
if k: if k:
return self.master.keymap.handle(fs.keyctx, k) return self.master.keymap.handle(fs.keyctx, k)

View File

@ -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()

View File

@ -5,25 +5,28 @@ import pytest
def test_bind(): def test_bind():
with taddons.context() as tctx: with taddons.context() as tctx:
km = keymap.Keymap(tctx.master) km = keymap.Keymap(tctx.master)
km.executor = mock.Mock() km.executor = mock.Mock()
with pytest.raises(ValueError): with pytest.raises(ValueError):
km.add("foo", "bar", ["unsupported"]) km.add("foo", "bar", ["unsupported"])
km.add("key", "str", ["options", "commands"]) km.add("key", "str", ["options", "commands"])
assert km.get("options", "key") assert km.get("options", "key")
assert km.get("commands", "key") assert km.get("commands", "key")
assert not km.get("flowlist", "key") assert not km.get("flowlist", "key")
assert len((km.list("commands"))) == 1
km.handle("unknown", "unknown") km.handle("unknown", "unknown")
assert not km.executor.called assert not km.executor.called
km.handle("options", "key") km.handle("options", "key")
assert km.executor.called assert km.executor.called
km.add("glob", "str", ["global"]) km.add("glob", "str", ["global"])
km.executor = mock.Mock() km.executor = mock.Mock()
km.handle("options", "glob") km.handle("options", "glob")
assert km.executor.called assert km.executor.called
assert len((km.list("global"))) == 1

View File

@ -54,8 +54,8 @@ commands =
deps = deps =
-rrequirements.txt -rrequirements.txt
-e./release -e./release
# The 3.2 release is broken 🎉 # The 3.2 release is broken
# the next commit after this updates the bootloaders, which then segfault! 🎉 # the next commit after this updates the bootloaders, which then segfault!
# https://github.com/pyinstaller/pyinstaller/issues/2232 # https://github.com/pyinstaller/pyinstaller/issues/2232
git+https://github.com/pyinstaller/pyinstaller.git@483c819d6a256b58db6740696a901bd41c313f0c; sys_platform == 'win32' git+https://github.com/pyinstaller/pyinstaller.git@483c819d6a256b58db6740696a901bd41c313f0c; sys_platform == 'win32'
git+https://github.com/mhils/pyinstaller.git@d094401e4196b1a6a03818b80164a5f555861cef; sys_platform != 'win32' git+https://github.com/mhils/pyinstaller.git@d094401e4196b1a6a03818b80164a5f555861cef; sys_platform != 'win32'