diff options
-rw-r--r-- | mitmproxy/addons/view.py | 150 | ||||
-rw-r--r-- | mitmproxy/master.py | 8 | ||||
-rw-r--r-- | mitmproxy/tools/cmdline.py | 4 | ||||
-rw-r--r-- | mitmproxy/tools/console/flowlist.py | 33 | ||||
-rw-r--r-- | mitmproxy/tools/console/master.py | 7 | ||||
-rw-r--r-- | mitmproxy/tools/console/statusbar.py | 2 | ||||
-rw-r--r-- | mitmproxy/tools/main.py | 2 | ||||
-rw-r--r-- | test/mitmproxy/addons/test_view.py | 58 |
8 files changed, 171 insertions, 93 deletions
diff --git a/mitmproxy/addons/view.py b/mitmproxy/addons/view.py index b4ba2315..c09ff454 100644 --- a/mitmproxy/addons/view.py +++ b/mitmproxy/addons/view.py @@ -19,39 +19,83 @@ import mitmproxy.flow from mitmproxy import flowfilter from mitmproxy import exceptions +# The underlying sorted list implementation expects the sort key to be stable +# for the lifetime of the object. However, if we sort by size, for instance, +# the sort order changes as the flow progresses through its lifecycle. We +# address this through two means: +# +# - Let order keys cache the sort value by flow ID. +# +# - Add a facility to refresh items in the list by removing and re-adding them +# when they are updated. + + +class _OrderKey: + def __init__(self, view): + self.view = view -def key_request_start(f: mitmproxy.flow.Flow) -> datetime.datetime: - return f.request.timestamp_start or 0 + def generate(self, f: mitmproxy.flow.Flow) -> typing.Any: + pass + def refresh(self, f): + k = self._key() + old = self.view.settings[f][k] + new = self.generate(f) + if old != new: + self.view._view.remove(f) + self.view.settings[f][k] = new + self.view._view.add(f) + self.view.sig_refresh.send(self.view) -def key_request_method(f: mitmproxy.flow.Flow) -> str: - return f.request.method + def _key(self): + return "_order_%s" % id(self) + def __call__(self, f): + k = self._key() + s = self.view.settings[f] + if k in s: + return s[k] + val = self.generate(f) + s[k] = val + return val -def key_request_url(f: mitmproxy.flow.Flow) -> str: - return f.request.url +class OrderRequestStart(_OrderKey): + def generate(self, f: mitmproxy.flow.Flow) -> datetime.datetime: + return f.request.timestamp_start or 0 -def key_size(f: mitmproxy.flow.Flow) -> int: - s = 0 - if f.request.raw_content: - s += len(f.request.raw_content) - if f.response and f.response.raw_content: - s += len(f.response.raw_content) - return s +class OrderRequestMethod(_OrderKey): + def generate(self, f: mitmproxy.flow.Flow) -> datetime.datetime: + return f.request.method -orders = [ - ("t", "time", key_request_start), - ("m", "method", key_request_method), - ("u", "url", key_request_url), - ("z", "size", key_size), -] + +class OrderRequestURL(_OrderKey): + def generate(self, f: mitmproxy.flow.Flow) -> datetime.datetime: + return f.request.url + + +class OrderKeySize(_OrderKey): + def generate(self, f: mitmproxy.flow.Flow) -> datetime.datetime: + s = 0 + if f.request.raw_content: + s += len(f.request.raw_content) + if f.response and f.response.raw_content: + s += len(f.response.raw_content) + return s matchall = flowfilter.parse(".") +orders = [ + ("t", "time"), + ("m", "method"), + ("u", "url"), + ("z", "size"), +] + + class View(collections.Sequence): def __init__(self): super().__init__() @@ -59,8 +103,18 @@ class View(collections.Sequence): self.filter = matchall # Should we show only marked flows? self.show_marked = False - self.order_key = key_request_start + + self.default_order = OrderRequestStart(self) + self.orders = dict( + time = self.default_order, + method = OrderRequestMethod(self), + url = OrderRequestURL(self), + size = OrderKeySize(self), + ) + self.order_key = self.default_order self.order_reversed = False + self.focus_follow = False + self._view = sortedcontainers.SortedListWithKey(key = self.order_key) # These signals broadcast events that affect the view. That is, an @@ -115,13 +169,23 @@ class View(collections.Sequence): def index(self, f: mitmproxy.flow.Flow) -> int: return self._rev(self._view.index(f)) + def __contains__(self, f: mitmproxy.flow.Flow) -> bool: + return self._view.__contains__(f) + + def _order_key_name(self): + return "_order_%s" % id(self.order_key) + + def _base_add(self, f): + self.settings[f][self._order_key_name()] = self.order_key(f) + self._view.add(f) + def _refilter(self): self._view.clear() for i in self._store.values(): if self.show_marked and not i.marked: continue if self.filter(i): - self._view.add(i) + self._base_add(i) self.sig_refresh.send(self) # API @@ -155,9 +219,10 @@ class View(collections.Sequence): """ self._store.clear() self._view.clear() + self.settings.clear() self.sig_refresh.send(self) - def add(self, f: mitmproxy.flow.Flow): + def add(self, f: mitmproxy.flow.Flow) -> bool: """ Adds a flow to the state. If the flow already exists, it is ignored. @@ -165,7 +230,9 @@ class View(collections.Sequence): if f.id not in self._store: self._store[f.id] = f if self.filter(f): - self._view.add(f) + self._base_add(f) + if self.focus_follow: + self.focus.flow = f self.sig_add.send(self, flow=f) def remove(self, f: mitmproxy.flow.Flow): @@ -173,10 +240,10 @@ class View(collections.Sequence): Removes the flow from the underlying store and the view. """ if f.id in self._store: - del self._store[f.id] if f in self._view: self._view.remove(f) self.sig_remove.send(self, flow=f) + del self._store[f.id] def update(self, f: mitmproxy.flow.Flow): """ @@ -185,9 +252,16 @@ class View(collections.Sequence): if f.id in self._store: if self.filter(f): if f not in self._view: - self._view.add(f) + self._base_add(f) + if self.focus_follow: + self.focus.flow = f self.sig_add.send(self, flow=f) else: + # This is a tad complicated. The sortedcontainers + # implementation assumes that the order key is stable. If + # it changes mid-way Very Bad Things happen. We detect when + # this happens, and re-fresh the item. + self.order_key.refresh(f) self.sig_update.send(self, flow=f) else: try: @@ -210,32 +284,31 @@ class View(collections.Sequence): self.set_filter(filt) if "order" in updated: if opts.order is None: - self.set_order(key_request_start) + self.set_order(self.default_order) else: - for _, name, func in orders: - if name == opts.order: - self.set_order(func) - break - else: + if opts.order not in self.orders: raise exceptions.OptionsError( "Unknown flow order: %s" % opts.order ) + self.set_order(self.orders[opts.order]) if "order_reversed" in updated: self.set_reversed(opts.order_reversed) + if "focus_follow" in updated: + self.focus_follow = opts.focus_follow def request(self, f): self.add(f) - def intercept(self, f): + def error(self, f): self.update(f) - def resume(self, f): + def response(self, f): self.update(f) - def error(self, f): + def intercept(self, f): self.update(f) - def response(self, f): + def resume(self, f): self.update(f) @@ -258,7 +331,7 @@ class Focus: return self._flow @flow.setter - def flow(self, f: mitmproxy.flow.Flow): + def flow(self, f: typing.Optional[mitmproxy.flow.Flow]): if f is not None and f not in self.view: raise ValueError("Attempt to set focus to flow not in view") self._flow = f @@ -305,6 +378,9 @@ class Settings(collections.Mapping): view.sig_remove.connect(self._sig_remove) view.sig_refresh.connect(self._sig_refresh) + def clear(self): + self.values.clear() + def __iter__(self) -> typing.Iterable: return iter(self.values) @@ -321,6 +397,6 @@ class Settings(collections.Mapping): del self.values[flow.id] def _sig_refresh(self, view): - for fid in self.values.keys(): + for fid in list(self.values.keys()): if fid not in view._store: del self.values[fid] diff --git a/mitmproxy/master.py b/mitmproxy/master.py index 31ce17a3..ffbfb0cb 100644 --- a/mitmproxy/master.py +++ b/mitmproxy/master.py @@ -92,10 +92,14 @@ class Master: try: mtype, obj = self.event_queue.get(timeout=timeout) if mtype not in events.Events: - raise exceptions.ControlException("Unknown event %s" % repr(mtype)) + raise exceptions.ControlException( + "Unknown event %s" % repr(mtype) + ) handle_func = getattr(self, mtype) if not callable(handle_func): - raise exceptions.ControlException("Handler %s not callable" % mtype) + raise exceptions.ControlException( + "Handler %s not callable" % mtype + ) if not handle_func.__dict__.get("__handler"): raise exceptions.ControlException( "Handler function %s is not decorated with controller.handler" % ( diff --git a/mitmproxy/tools/cmdline.py b/mitmproxy/tools/cmdline.py index cf6e1d35..e4b29d0f 100644 --- a/mitmproxy/tools/cmdline.py +++ b/mitmproxy/tools/cmdline.py @@ -784,8 +784,8 @@ def mitmproxy(): ) parser.add_argument( "--follow", - action="store_true", dest="follow", - help="Follow flow list." + action="store_true", dest="focus_follow", + help="Focus follows new flows." ) parser.add_argument( "--order", diff --git a/mitmproxy/tools/console/flowlist.py b/mitmproxy/tools/console/flowlist.py index a0a3dc94..76545893 100644 --- a/mitmproxy/tools/console/flowlist.py +++ b/mitmproxy/tools/console/flowlist.py @@ -175,18 +175,6 @@ class FlowItem(urwid.WidgetWrap): signals.flowlist_change.send(self) elif key == "M": self.master.view.toggle_marked() - elif key == "o": - orders = [(i[1], i[0]) for i in view.orders] - lookup = dict([(i[0], i[1]) for i in view.orders]) - - def change_order(k): - self.master.options.order = lookup[k] - - signals.status_prompt_onekey.send( - prompt = "Order", - keys = orders, - callback = change_order - ) elif key == "r": try: self.master.replay_request(self.flow) @@ -220,9 +208,6 @@ class FlowItem(urwid.WidgetWrap): for f in self.master.view: f.marked = False signals.flowlist_change.send(self) - elif key == "v": - val = not self.master.options.order_reversed - self.master.options.order_reversed = val elif key == "V": if not self.flow.modified(): signals.status_message.send(message="Flow not modified.") @@ -389,8 +374,24 @@ class FlowListBox(urwid.ListBox): keys = common.METHOD_OPTIONS, callback = self.get_method ) + elif key == "o": + orders = [(i[1], i[0]) for i in view.orders] + lookup = dict([(i[0], i[1]) for i in view.orders]) + + def change_order(k): + self.master.options.order = lookup[k] + + signals.status_prompt_onekey.send( + prompt = "Order", + keys = orders, + callback = change_order + ) elif key == "F": - self.master.toggle_follow_flows() + o = self.master.options + o.focus_follow = not o.focus_follow + elif key == "v": + val = not self.master.options.order_reversed + self.master.options.order_reversed = val elif key == "W": if self.master.options.outfile: self.master.options.outfile = None diff --git a/mitmproxy/tools/console/master.py b/mitmproxy/tools/console/master.py index 932dc151..4c5e8c8c 100644 --- a/mitmproxy/tools/console/master.py +++ b/mitmproxy/tools/console/master.py @@ -44,25 +44,23 @@ class Options(mitmproxy.options.Options): self, *, # all args are keyword-only. eventlog: bool = False, - follow: bool = False, + focus_follow: bool = False, intercept: Optional[str] = None, filter: Optional[str] = None, palette: Optional[str] = None, palette_transparent: bool = False, no_mouse: bool = False, - follow_focus: bool = False, order: Optional[str] = None, order_reversed: bool = False, **kwargs ): self.eventlog = eventlog - self.follow = follow + self.focus_follow = focus_follow self.intercept = intercept self.filter = filter self.palette = palette self.palette_transparent = palette_transparent self.no_mouse = no_mouse - self.follow_focus = follow_focus self.order = order self.order_reversed = order_reversed super().__init__(**kwargs) @@ -83,7 +81,6 @@ class ConsoleMaster(master.Master): self.palette_transparent = options.palette_transparent self.logbuffer = urwid.SimpleListWalker([]) - self.follow = options.follow self.view_stack = [] diff --git a/mitmproxy/tools/console/statusbar.py b/mitmproxy/tools/console/statusbar.py index b358f711..e292cbd7 100644 --- a/mitmproxy/tools/console/statusbar.py +++ b/mitmproxy/tools/console/statusbar.py @@ -199,7 +199,7 @@ class StatusBar(urwid.WidgetWrap): opts.append("killextra") if self.master.options.no_upstream_cert: opts.append("no-upstream-cert") - if self.master.options.follow_focus: + if self.master.options.focus_follow: opts.append("following") if self.master.options.stream_large_bodies: opts.append( diff --git a/mitmproxy/tools/main.py b/mitmproxy/tools/main.py index 772841f5..08e5f2ca 100644 --- a/mitmproxy/tools/main.py +++ b/mitmproxy/tools/main.py @@ -69,7 +69,7 @@ def mitmproxy(args=None): # pragma: no cover console_options.palette = args.palette console_options.palette_transparent = args.palette_transparent console_options.eventlog = args.eventlog - console_options.follow = args.follow + console_options.focus_follow = args.focus_follow console_options.intercept = args.intercept console_options.filter = args.filter console_options.no_mouse = args.no_mouse diff --git a/test/mitmproxy/addons/test_view.py b/test/mitmproxy/addons/test_view.py index 750e8469..63df8307 100644 --- a/test/mitmproxy/addons/test_view.py +++ b/test/mitmproxy/addons/test_view.py @@ -7,6 +7,20 @@ from mitmproxy.test import taddons from .. import tutils +class Options(options.Options): + def __init__( + self, *, + filter=None, + order=None, + order_reversed=False, + **kwargs + ): + self.filter = filter + self.order = order + self.order_reversed = order_reversed + super().__init__(**kwargs) + + def test_simple(): v = view.View() f = tflow.tflow() @@ -69,22 +83,23 @@ def test_filter(): def test_order(): v = view.View() - v.request(tft(method="get", start=1)) - v.request(tft(method="put", start=2)) - v.request(tft(method="get", start=3)) - v.request(tft(method="put", start=4)) - assert [i.request.timestamp_start for i in v] == [1, 2, 3, 4] - - v.set_order(view.key_request_method) - assert [i.request.method for i in v] == ["GET", "GET", "PUT", "PUT"] - v.set_reversed(True) - assert [i.request.method for i in v] == ["PUT", "PUT", "GET", "GET"] + with taddons.context(options=Options()) as tctx: + v.request(tft(method="get", start=1)) + v.request(tft(method="put", start=2)) + v.request(tft(method="get", start=3)) + v.request(tft(method="put", start=4)) + assert [i.request.timestamp_start for i in v] == [1, 2, 3, 4] - v.set_order(view.key_request_start) - assert [i.request.timestamp_start for i in v] == [4, 3, 2, 1] + tctx.configure(v, order="method") + assert [i.request.method for i in v] == ["GET", "GET", "PUT", "PUT"] + v.set_reversed(True) + assert [i.request.method for i in v] == ["PUT", "PUT", "GET", "GET"] + + tctx.configure(v, order="time") + assert [i.request.timestamp_start for i in v] == [4, 3, 2, 1] - v.set_reversed(False) - assert [i.request.timestamp_start for i in v] == [1, 2, 3, 4] + v.set_reversed(False) + assert [i.request.timestamp_start for i in v] == [1, 2, 3, 4] def test_reversed(): @@ -256,7 +271,6 @@ def test_settings(): tutils.raises(KeyError, v.settings.__getitem__, f) v.add(f) - assert v.settings[f] == {} v.settings[f]["foo"] = "bar" assert v.settings[f]["foo"] == "bar" assert len(list(v.settings)) == 1 @@ -265,20 +279,6 @@ def test_settings(): assert not v.settings.keys() -class Options(options.Options): - def __init__( - self, *, - filter=None, - order=None, - order_reversed=False, - **kwargs - ): - self.filter = filter - self.order = order - self.order_reversed = order_reversed - super().__init__(**kwargs) - - def test_configure(): v = view.View() with taddons.context(options=Options()) as tctx: |