diff options
-rw-r--r-- | libmproxy/console/__init__.py | 64 | ||||
-rw-r--r-- | libmproxy/console/common.py | 10 | ||||
-rw-r--r-- | libmproxy/console/flowdetailview.py | 74 | ||||
-rw-r--r-- | libmproxy/console/flowview.py | 15 | ||||
-rw-r--r-- | libmproxy/console/help.py | 27 | ||||
-rw-r--r-- | libmproxy/console/options.py | 200 | ||||
-rw-r--r-- | libmproxy/console/palettes.py | 46 | ||||
-rw-r--r-- | libmproxy/console/searchable.py | 2 | ||||
-rw-r--r-- | libmproxy/console/tabs.py | 9 | ||||
-rw-r--r-- | libmproxy/console/window.py | 35 | ||||
-rwxr-xr-x | test/tools/testpatt | 12 |
11 files changed, 369 insertions, 125 deletions
diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index 660024cc..3b3f2df2 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -15,8 +15,8 @@ import urwid import weakref from .. import controller, flow, script -from . import flowlist, flowview, help, common, window, signals -from . import grideditor, palettes, contentview, flowdetailview, statusbar +from . import flowlist, flowview, help, window, signals, options +from . import grideditor, palettes, contentview, statusbar EVENTLOG_SIZE = 500 @@ -73,6 +73,8 @@ class ConsoleState(flow.State): elif idx < 0: idx = 0 self.focus = idx + else: + self.focus = None def set_focus_flow(self, f): self.set_focus(self.view.index(f)) @@ -227,6 +229,16 @@ class ConsoleMaster(flow.FlowMaster): def sig_pop_view_state(self, sender): if self.view_stack: self.loop.widget = self.view_stack.pop() + else: + signals.status_prompt_onekey.send( + self, + prompt = "Quit", + keys = ( + ("yes", "y"), + ("no", "n"), + ), + callback = self.quit, + ) def sig_push_view_state(self, sender): self.view_stack.append(self.loop.widget) @@ -393,7 +405,6 @@ class ConsoleMaster(flow.FlowMaster): self.ui.set_terminal_properties(256) self.ui.register_palette(self.palette.palette()) self.flow_list_walker = flowlist.FlowListWalker(self, self.state) - self.help_context = None self.loop = urwid.MainLoop( urwid.SolidFill("x"), screen = self.ui, @@ -444,23 +455,34 @@ class ConsoleMaster(flow.FlowMaster): sys.stderr.flush() self.shutdown() - def view_help(self): + def view_help(self, helpctx): signals.push_view_state.send(self) self.loop.widget = window.Window( self, - help.HelpView(self.help_context), + help.HelpView(helpctx), None, - statusbar.StatusBar(self, help.footer) + statusbar.StatusBar(self, help.footer), + None + ) + + def view_options(self): + signals.push_view_state.send(self) + self.loop.widget = window.Window( + self, + options.Options(self), + None, + statusbar.StatusBar(self, options.footer), + None ) def view_grideditor(self, ge): signals.push_view_state.send(self) - self.help_context = ge.make_help() self.loop.widget = window.Window( self, ge, None, - statusbar.StatusBar(self, grideditor.FOOTER) + statusbar.StatusBar(self, grideditor.FOOTER), + ge.make_help() ) def view_flowlist(self): @@ -474,24 +496,24 @@ class ConsoleMaster(flow.FlowMaster): else: body = flowlist.FlowListBox(self) - self.help_context = flowlist.help_context self.loop.widget = window.Window( self, body, None, - statusbar.StatusBar(self, flowlist.footer) + statusbar.StatusBar(self, flowlist.footer), + flowlist.help_context ) self.loop.draw_screen() def view_flow(self, flow, tab_offset=0): signals.push_view_state.send(self) self.state.set_focus_flow(flow) - self.help_context = flowview.help_context self.loop.widget = window.Window( self, flowview.FlowView(self, self.state, flow, tab_offset), flowview.FlowViewHeader(self, flow), - statusbar.StatusBar(self, flowview.footer) + statusbar.StatusBar(self, flowview.footer), + flowview.help_context ) def _write_flows(self, path, flows): @@ -574,24 +596,6 @@ class ConsoleMaster(flow.FlowMaster): if a != "n": raise urwid.ExitMainLoop - def _change_options(self, a): - if a == "a": - self.anticache = not self.anticache - if a == "c": - self.anticomp = not self.anticomp - if a == "h": - self.showhost = not self.showhost - self.sync_list_view() - self.refresh_focus() - elif a == "k": - self.killextra = not self.killextra - elif a == "n": - self.refresh_server_playback = not self.refresh_server_playback - elif a == "u": - self.server.config.no_upstream_cert =\ - not self.server.config.no_upstream_cert - signals.update_settings.send(self) - def shutdown(self): self.state.killall(self) flow.FlowMaster.shutdown(self) diff --git a/libmproxy/console/common.py b/libmproxy/console/common.py index bc8a2aad..23d3a4a4 100644 --- a/libmproxy/console/common.py +++ b/libmproxy/console/common.py @@ -37,14 +37,14 @@ def is_keypress(k): return True -def highlight_key(s, k): +def highlight_key(str, key, textattr="text", keyattr="key"): l = [] - parts = s.split(k, 1) + parts = str.split(key, 1) if parts[0]: - l.append(("text", parts[0])) - l.append(("key", k)) + l.append((textattr, parts[0])) + l.append((keyattr, key)) if parts[1]: - l.append(("text", parts[1])) + l.append((textattr, parts[1])) return l diff --git a/libmproxy/console/flowdetailview.py b/libmproxy/console/flowdetailview.py index 99f2a262..48845a62 100644 --- a/libmproxy/console/flowdetailview.py +++ b/libmproxy/console/flowdetailview.py @@ -1,8 +1,17 @@ from __future__ import absolute_import import urwid -from . import common, signals, searchable +from . import common, searchable from .. import utils + +def maybe_timestamp(base, attr): + if base and getattr(base, attr): + return utils.format_timestamp_with_milli(getattr(base, attr)) + else: + return "active" + pass + + def flowdetails(state, flow): text = [] @@ -81,16 +90,61 @@ def flowdetails(state, flow): parts = [] - parts.append(["Client conn. established", utils.format_timestamp_with_milli(cc.timestamp_start) if (cc and cc.timestamp_start) else "active"]) - parts.append(["Server conn. initiated", utils.format_timestamp_with_milli(sc.timestamp_start) if sc else "active" ]) - parts.append(["Server conn. TCP handshake", utils.format_timestamp_with_milli(sc.timestamp_tcp_setup) if (sc and sc.timestamp_tcp_setup) else "active"]) + parts.append( + [ + "Client conn. established", + maybe_timestamp(cc, "timestamp_start") + ] + ) + parts.append( + [ + "Server conn. initiated", + maybe_timestamp(sc, "timestamp_start") + ] + ) + parts.append( + [ + "Server conn. TCP handshake", + maybe_timestamp(sc, "timestamp_tcp_setup") + ] + ) if sc.ssl_established: - parts.append(["Server conn. SSL handshake", utils.format_timestamp_with_milli(sc.timestamp_ssl_setup) if sc.timestamp_ssl_setup else "active"]) - parts.append(["Client conn. SSL handshake", utils.format_timestamp_with_milli(cc.timestamp_ssl_setup) if (cc and cc.timestamp_ssl_setup) else "active"]) - parts.append(["First request byte", utils.format_timestamp_with_milli(req.timestamp_start)]) - parts.append(["Request complete", utils.format_timestamp_with_milli(req.timestamp_end) if req.timestamp_end else "active"]) - parts.append(["First response byte", utils.format_timestamp_with_milli(resp.timestamp_start) if resp else "active"]) - parts.append(["Response complete", utils.format_timestamp_with_milli(resp.timestamp_end) if (resp and resp.timestamp_end) else "active"]) + parts.append( + [ + "Server conn. SSL handshake", + maybe_timestamp(sc, "timestamp_ssl_setup") + ] + ) + parts.append( + [ + "Client conn. SSL handshake", + maybe_timestamp(cc, "timestamp_ssl_setup") + ] + ) + parts.append( + [ + "First request byte", + maybe_timestamp(req, "timestamp_start") + ] + ) + parts.append( + [ + "Request complete", + maybe_timestamp(req, "timestamp_end") + ] + ) + parts.append( + [ + "First response byte", + maybe_timestamp(resp, "timestamp_start") + ] + ) + parts.append( + [ + "Response complete", + maybe_timestamp(resp, "timestamp_end") + ] + ) # sort operations by timestamp parts = sorted(parts, key=lambda p: p[1]) diff --git a/libmproxy/console/flowview.py b/libmproxy/console/flowview.py index 538f42f0..99844cb7 100644 --- a/libmproxy/console/flowview.py +++ b/libmproxy/console/flowview.py @@ -68,6 +68,7 @@ def _mkhelp(): ("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)"), @@ -434,6 +435,8 @@ class FlowView(tabs.Tabs): signals.flow_change.send(self, flow = self.flow) def keypress(self, size, key): + key = super(self.__class__, self).keypress(size, key) + if key == " ": self.view_next_flow(self.flow) return @@ -446,10 +449,7 @@ class FlowView(tabs.Tabs): else: conn = None - if key == "q": - signals.pop_view_state.send(self) - return None - elif key in ("up", "down", "page up", "page down"): + if key in ("up", "down", "page up", "page down"): # Why doesn't this just work?? self._w.keypress(size, key) elif key == "a": @@ -499,7 +499,7 @@ class FlowView(tabs.Tabs): args = (self.flow,) ) - if not conn and key in "befgmxvz": + if not conn and key in set(list("befgmxvz")): signals.status_message.send( message = "Tab to the request or response", expire = 1 @@ -601,10 +601,7 @@ class FlowView(tabs.Tabs): args = (conn,) ) signals.flow_change.send(self, flow = self.flow) - else: - return super(self.__class__, self).keypress(size, key) - else: - return super(self.__class__, self).keypress(size, key) + return key def encode_callback(self, key, conn): encoding_map = { diff --git a/libmproxy/console/help.py b/libmproxy/console/help.py index 73cd8a50..19f17625 100644 --- a/libmproxy/console/help.py +++ b/libmproxy/console/help.py @@ -89,32 +89,7 @@ class HelpView(urwid.ListBox): common.highlight_key("amf", "f") + [("text", ": AMF (requires PyAMF)")] ), - ("o", "toggle options:"), - (None, - common.highlight_key("anticache", "a") + - [("text", ": prevent cached responses")] - ), - (None, - common.highlight_key("anticomp", "c") + - [("text", ": prevent compressed responses")] - ), - (None, - common.highlight_key("showhost", "h") + - [("text", ": use Host header for URL display")] - ), - (None, - common.highlight_key("killextra", "k") + - [("text", ": kill requests not part of server replay")] - ), - (None, - common.highlight_key("norefresh", "n") + - [("text", ": disable server replay response refresh")] - ), - (None, - common.highlight_key("upstream certs", "u") + - [("text", ": sniff cert info from upstream server")] - ), - + ("o", "options"), ("q", "quit / return to flow list"), ("Q", "quit without confirm prompt"), ("R", "edit replacement patterns"), diff --git a/libmproxy/console/options.py b/libmproxy/console/options.py new file mode 100644 index 00000000..db6cc151 --- /dev/null +++ b/libmproxy/console/options.py @@ -0,0 +1,200 @@ +import urwid + +from . import common, signals + +help_context = None +footer = [ + ('heading_key', "enter/space"), ":toggle ", + ('heading_key', "C"), ":clear all ", +] + + +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" + text = common.highlight_key( + text, + shortcut, + textattr=textattr, + keyattr=keyattr + ) + opt = urwid.Text(text, align="center") + opt = urwid.AttrWrap(opt, textattr) + opt = urwid.Padding(opt, align="center", width=("relative", 20)) + 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 + signals.update_settings.connect(self.sig_update_settings) + + def sig_update_settings(self, sender): + self._modified() + + 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 OptionListBox(urwid.ListBox): + def __init__(self, options): + urwid.ListBox.__init__( + self, + OptionWalker(options) + ) + self.options = options + self.keymap = {} + for i in options: + self.keymap[i.shortcut] = i + + def keypress(self, size, key): + if key == "enter" or key == " ": + self.get_focus()[0].option.activate() + return None + key = common.shortcuts(key) + if key in self.keymap: + self.keymap[key].activate() + self.set_focus(self.options.index(self.keymap[key])) + return None + return super(self.__class__, self).keypress(size, key) + + +_neg = lambda: 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 Options(urwid.WidgetWrap): + def __init__(self, master): + self.master = master + self.lb = OptionListBox( + [ + Option( + "Anti-Cache", + "a", + lambda: master.anticache, + self.toggle_anticache + ), + Option( + "Anti-Compression", + "o", + lambda: master.anticomp, + self.toggle_anticomp + ), + #Option("Header Set Patterns"), + #Option("Ignore Patterns"), + Option( + "Kill Extra", + "E", + lambda: master.killextra, + self.toggle_killextra + ), + #Option("Manage Scripts"), + #Option("Replacement Patterns"), + Option( + "Show Host", + "H", + lambda: master.showhost, + self.toggle_showhost + ), + #Option("Sticky Cookies"), + #Option("Sticky Auth"), + #Option("TCP Proxying"), + Option( + "No Refresh", + "R", + lambda: not master.refresh_server_playback, + self.toggle_refresh_server_playback + ), + Option( + "No Upstream Certs", + "U", + lambda: master.server.config.no_upstream_cert, + self.toggle_upstream_cert + ), + ] + ) + title = urwid.Text("Options") + title = urwid.Padding(title, align="left", width=("relative", 100)) + title = urwid.AttrWrap(title, "heading") + self._w = urwid.Frame( + self.lb, + header = title + ) + self.master.loop.widget.footer.update("") + + def keypress(self, size, key): + if key == "C": + self.clearall() + return None + return super(self.__class__, self).keypress(size, key) + + def clearall(self): + self.master.anticache = False + self.master.anticomp = False + self.master.killextra = False + self.master.showhost = False + self.master.refresh_server_playback = True + self.master.server.config.no_upstream_cert = False + signals.update_settings.send(self) + signals.status_message.send( + message = "All options cleared", + expire = 1 + ) + + def toggle_anticache(self): + self.master.anticache = not self.master.anticache + + def toggle_anticomp(self): + self.master.anticomp = not self.master.anticomp + + def toggle_killextra(self): + self.master.killextra = not self.master.killextra + + def toggle_showhost(self): + self.master.showhost = not self.master.showhost + + def toggle_refresh_server_playback(self): + self.master.refresh_server_playback = not self.master.refresh_server_playback + + def toggle_upstream_cert(self): + self.master.server.config.no_upstream_cert = not self.master.server.config.no_upstream_cert + signals.update_settings.send(self) diff --git a/libmproxy/console/palettes.py b/libmproxy/console/palettes.py index cfb2702c..b55a34aa 100644 --- a/libmproxy/console/palettes.py +++ b/libmproxy/console/palettes.py @@ -17,6 +17,10 @@ class Palette: # Help 'key', 'head', 'text', + # Options + 'option_selected', 'option_active', 'option_active_selected', + 'option_selected_key', + # List and Connections 'method', 'focus', 'code_200', 'code_300', 'code_400', 'code_500', 'code_other', @@ -60,6 +64,12 @@ class LowDark(Palette): head = ('white,bold', 'default'), text = ('light gray', 'default'), + # Options + option_selected = ('light gray', 'dark blue'), + option_selected_key = ('light cyan', 'dark blue'), + option_active = ('light red', 'default'), + option_active_selected = ('light red', 'dark blue'), + # List and Connections method = ('dark cyan', 'default'), focus = ('yellow', 'default'), @@ -100,18 +110,24 @@ class LowLight(Palette): Low-color light background """ low = dict( - title = ('dark magenta,bold', 'light blue'), + title = ('dark magenta', 'default'), # Status bar & heading - heading = ('light gray', 'dark blue'), - heading_key = ('light cyan', 'dark blue'), + heading = ('white', 'black'), + heading_key = ('dark blue', 'black'), heading_inactive = ('black', 'light gray'), # Help - key = ('dark blue,bold', 'default'), - head = ('black,bold', 'default'), + key = ('dark blue', 'default'), + head = ('black', 'default'), text = ('dark gray', 'default'), + # Options + option_selected = ('black', 'light gray'), + option_selected_key = ('dark blue', 'light gray'), + option_active = ('light red', 'default'), + option_active_selected = ('light red', 'light gray'), + # List and Connections method = ('dark cyan', 'default'), focus = ('black', 'default'), @@ -181,6 +197,12 @@ class SolarizedLight(LowLight): key = (sol_blue, 'default',), head = (sol_base00, 'default'), + # Options + option_selected = (sol_base03, sol_base2), + option_selected_key = (sol_blue, sol_base2), + option_active = (sol_orange, 'default'), + option_active_selected = (sol_orange, sol_base2), + # List and Connections method = (sol_cyan, 'default'), focus = (sol_base01, 'default'), @@ -212,16 +234,22 @@ class SolarizedLight(LowLight): class SolarizedDark(LowDark): high = dict( title = (sol_blue, 'default'), - text = (sol_base0, 'default'), + text = (sol_base1, 'default'), # Status bar & heading - heading = (sol_base03, sol_base1), - heading_key = (sol_blue+",bold", sol_base1), + heading = (sol_base2, sol_base01), + heading_key = (sol_blue+",bold", sol_base01), heading_inactive = (sol_base1, sol_base02), # Help key = (sol_blue, 'default',), - head = (sol_base00, 'default'), + head = (sol_base2, 'default'), + + # Options + option_selected = (sol_base03, sol_base00), + option_selected_key = (sol_blue, sol_base00), + option_active = (sol_orange, 'default'), + option_active_selected = (sol_orange, sol_base00), # List and Connections method = (sol_cyan, 'default'), diff --git a/libmproxy/console/searchable.py b/libmproxy/console/searchable.py index 9d66c718..8f63c3f5 100644 --- a/libmproxy/console/searchable.py +++ b/libmproxy/console/searchable.py @@ -36,7 +36,7 @@ class Searchable(urwid.ListBox): if key == "N": self.find_next(True) else: - return key + return super(self.__class__, self).keypress(size, key) def set_search(self, text): self.state.last_search = text diff --git a/libmproxy/console/tabs.py b/libmproxy/console/tabs.py index b8943ad4..2c46e59e 100644 --- a/libmproxy/console/tabs.py +++ b/libmproxy/console/tabs.py @@ -14,11 +14,13 @@ class Tabs(urwid.WidgetWrap): return p def keypress(self, size, key): - if key == "tab": + if key in ["tab", "l"]: self.tab_offset = (self.tab_offset + 1)%(len(self.tabs)) self.show() - else: - return self._w.keypress(size, key) + elif key == "h": + self.tab_offset = (self.tab_offset - 1)%(len(self.tabs)) + self.show() + return self._w.keypress(size, key) def show(self): headers = [] @@ -33,3 +35,4 @@ class Tabs(urwid.WidgetWrap): body = self.tabs[self.tab_offset][1](), header = headers ) + self._w.set_focus("body") diff --git a/libmproxy/console/window.py b/libmproxy/console/window.py index d686f61d..1d22f280 100644 --- a/libmproxy/console/window.py +++ b/libmproxy/console/window.py @@ -1,19 +1,21 @@ import urwid -from . import common, grideditor, signals, contentview +from . import grideditor, signals, contentview + class Window(urwid.Frame): - def __init__(self, master, body, header, footer): + def __init__(self, master, body, header, footer, helpctx): urwid.Frame.__init__(self, body, header=header, footer=footer) self.master = master + self.helpctx = helpctx signals.focus.connect(self.sig_focus) def sig_focus(self, sender, section): self.focus_position = section def keypress(self, size, k): - k = urwid.Frame.keypress(self, self.master.loop.screen_size, k) + k = super(self.__class__, self).keypress(size, k) if k == "?": - self.master.view_help() + self.master.view_help(self.helpctx) elif k == "c": if not self.master.client_playback: signals.status_prompt_path.send( @@ -62,18 +64,12 @@ class Window(urwid.Frame): text = self.master.state.intercept_txt, callback = self.master.set_intercept ) + elif k == "o": + self.master.view_options() elif k == "Q": raise urwid.ExitMainLoop elif k == "q": - signals.status_prompt_onekey.send( - self, - prompt = "Quit", - keys = ( - ("yes", "y"), - ("no", "n"), - ), - callback = self.master.quit, - ) + signals.pop_view_state.send(self) elif k == "M": signals.status_prompt_onekey.send( prompt = "Global default display mode", @@ -113,19 +109,6 @@ class Window(urwid.Frame): ), callback = self.master.stop_server_playback_prompt, ) - elif k == "o": - signals.status_prompt_onekey.send( - prompt = "Options", - keys = ( - ("anticache", "a"), - ("anticomp", "c"), - ("showhost", "h"), - ("killextra", "k"), - ("norefresh", "n"), - ("no-upstream-certs", "u"), - ), - callback = self.master._change_options - ) elif k == "t": signals.status_prompt.send( prompt = "Sticky cookie filter", diff --git a/test/tools/testpatt b/test/tools/testpatt index 5ee1ea02..b41011c0 100755 --- a/test/tools/testpatt +++ b/test/tools/testpatt @@ -1,9 +1,9 @@ #!/bin/bash # Generate a test pattern with pathoc -PATHOD=http://localhost:9999 -pathoc localhost:8080 "get:'$PATHOD/p/200:p0,1:b@2048b':b@2048b" -pathoc localhost:8080 "get:'$PATHOD/p/300:p0,1:b@2048b':b@2048b" -pathoc localhost:8080 "get:'$PATHOD/p/400:p0,1:b@2048b':b@2048b" -pathoc localhost:8080 "get:'$PATHOD/p/500:p0,1:b@2048b':b@2048b" -pathoc localhost:8080 "get:'$PATHOD/p/600:p0,1:b@2048b':b@2048b" +PATHOD=localhost:9999 +pathoc -s -c $PATHOD localhost:8080 "get:'/p/200:p0,1:b@2048b':b@2048b" +pathoc -s -c $PATHOD localhost:8080 "get:'/p/300:p0,1:b@2048b':b@2048b" +pathoc -s -c $PATHOD localhost:8080 "get:'/p/400:p0,1:b@2048b':b@2048b" +pathoc -s -c $PATHOD localhost:8080 "get:'/p/500:p0,1:b@2048b':b@2048b" +pathoc -s -c $PATHOD localhost:8080 "get:'/p/600:p0,1:b@2048b':b@2048b" |