aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAldo Cortesi <aldo@corte.si>2016-10-29 12:19:19 +1300
committerGitHub <noreply@github.com>2016-10-29 12:19:19 +1300
commita3131ac34330537b205ba7332f752c1b1ea93278 (patch)
treedea3958bc9c306bc5b6c6cb849c34ae8a0cb39a1
parent9be34baa403802953e09d4962b755d50af91a503 (diff)
parent005c22445b013fdbce06569966cd86a48201e837 (diff)
downloadmitmproxy-a3131ac34330537b205ba7332f752c1b1ea93278.tar.gz
mitmproxy-a3131ac34330537b205ba7332f752c1b1ea93278.tar.bz2
mitmproxy-a3131ac34330537b205ba7332f752c1b1ea93278.zip
Merge pull request #1683 from cortesi/view
addons.View
-rw-r--r--mitmproxy/addons/view.py269
-rw-r--r--setup.py1
-rw-r--r--test/mitmproxy/addons/test_view.py275
3 files changed, 545 insertions, 0 deletions
diff --git a/mitmproxy/addons/view.py b/mitmproxy/addons/view.py
new file mode 100644
index 00000000..8c0567a5
--- /dev/null
+++ b/mitmproxy/addons/view.py
@@ -0,0 +1,269 @@
+"""
+The View:
+
+- Keeps track of a store of flows
+- Maintains a filtered, ordered view onto that list of flows
+- Exposes a number of signals so the view can be monitored
+- Tracks focus within the view
+- Exposes a settings store for flows that automatically expires if the flow is
+ removed from the store.
+"""
+import collections
+import typing
+import datetime
+
+import blinker
+import sortedcontainers
+
+import mitmproxy.flow
+from mitmproxy import flowfilter
+
+
+def key_request_start(f: mitmproxy.flow.Flow) -> datetime.datetime:
+ return f.request.timestamp_start or 0
+
+
+def key_request_method(f: mitmproxy.flow.Flow) -> str:
+ return f.request.method
+
+
+matchall = flowfilter.parse(".")
+
+
+class View(collections.Sequence):
+ def __init__(self):
+ super().__init__()
+ self._store = {}
+ self.filter = matchall
+ self.order_key = key_request_start
+ self.order_reversed = False
+ self._view = sortedcontainers.SortedListWithKey(key = self.order_key)
+
+ # These signals broadcast events that affect the view. That is, an
+ # update to a flow in the store but not in the view does not trigger a
+ # signal. All signals are called after the view has been updated.
+ self.sig_update = blinker.Signal()
+ self.sig_add = blinker.Signal()
+ self.sig_remove = blinker.Signal()
+ # Signals that the view should be refreshed completely
+ self.sig_refresh = blinker.Signal()
+
+ self.focus = Focus(self)
+ self.settings = Settings(self)
+
+ def _rev(self, idx: int) -> int:
+ """
+ Reverses an index, if needed
+ """
+ if self.order_reversed:
+ if idx < 0:
+ idx = -idx - 1
+ else:
+ idx = len(self._view) - idx - 1
+ if idx < 0:
+ raise IndexError
+ return idx
+
+ def __len__(self):
+ return len(self._view)
+
+ def __getitem__(self, offset) -> mitmproxy.flow.Flow:
+ return self._view[self._rev(offset)]
+
+ # Reflect some methods to the efficient underlying implementation
+
+ def bisect(self, f: mitmproxy.flow.Flow) -> int:
+ v = self._view.bisect(f)
+ # Bisect returns an item to the RIGHT of the existing entries.
+ if v == 0:
+ return v
+ return self._rev(v - 1) + 1
+
+ def index(self, f: mitmproxy.flow.Flow) -> int:
+ return self._rev(self._view.index(f))
+
+ # API
+
+ def toggle_reversed(self):
+ self.order_reversed = not self.order_reversed
+ self.sig_refresh.send(self)
+
+ def set_order(self, order_key: typing.Callable):
+ """
+ Sets the current view order.
+ """
+ self.order_key = order_key
+ newview = sortedcontainers.SortedListWithKey(key=order_key)
+ newview.update(self._view)
+ self._view = newview
+
+ def set_filter(self, flt: typing.Optional[flowfilter.TFilter]):
+ """
+ Sets the current view filter.
+ """
+ self.filter = flt or matchall
+ self._view.clear()
+ for i in self._store.values():
+ if self.filter(i):
+ self._view.add(i)
+ self.sig_refresh.send(self)
+
+ def clear(self):
+ """
+ Clears both the state and view.
+ """
+ self._state.clear()
+ self._view.clear()
+ self.sig_refresh.send(self)
+
+ def add(self, f: mitmproxy.flow.Flow):
+ """
+ Adds a flow to the state. If the flow already exists, it is
+ ignored.
+ """
+ if f.id not in self._store:
+ self._store[f.id] = f
+ if self.filter(f):
+ self._view.add(f)
+ self.sig_add.send(self, flow=f)
+
+ def remove(self, f: mitmproxy.flow.Flow):
+ """
+ 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)
+
+ def update(self, f: mitmproxy.flow.Flow):
+ """
+ Updates a flow. If the flow is not in the state, it's ignored.
+ """
+ if f.id in self._store:
+ if self.filter(f):
+ if f not in self._view:
+ self._view.add(f)
+ self.sig_add.send(self, flow=f)
+ else:
+ self.sig_update.send(self, flow=f)
+ else:
+ try:
+ self._view.remove(f)
+ self.sig_remove.send(self, flow=f)
+ except ValueError:
+ # The value was not in the view
+ pass
+
+ # Event handlers
+
+ def request(self, f):
+ self.add(f)
+
+ def intercept(self, f):
+ self.update(f)
+
+ def resume(self, f):
+ self.update(f)
+
+ def error(self, f):
+ self.update(f)
+
+ def response(self, f):
+ self.update(f)
+
+
+class Focus:
+ """
+ Tracks a focus element within a View.
+ """
+ def __init__(self, v: View) -> None:
+ self.view = v
+ self._flow = None
+ if len(self.view):
+ self.flow = self.view[0]
+ v.sig_add.connect(self._sig_add)
+ v.sig_remove.connect(self._sig_remove)
+ v.sig_refresh.connect(self._sig_refresh)
+
+ @property
+ def flow(self) -> typing.Optional[mitmproxy.flow.Flow]:
+ return self._flow
+
+ @flow.setter
+ def flow(self, f: 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
+
+ @property
+ def index(self) -> typing.Optional[int]:
+ if self.flow:
+ return self.view.index(self.flow)
+
+ def next(self):
+ """
+ Sets the focus to the next flow.
+ """
+ if self.flow:
+ idx = min(self.index + 1, len(self.view) - 1)
+ self.flow = self.view[idx]
+
+ def prev(self):
+ """
+ Sets the focus to the previous flow.
+ """
+ if self.flow:
+ idx = max(self.index - 1, 0)
+ self.flow = self.view[idx]
+
+ def _nearest(self, f, v):
+ return min(v.bisect(f), len(v) - 1)
+
+ def _sig_remove(self, view, flow):
+ if len(view) == 0:
+ self.flow = None
+ elif flow is self.flow:
+ self.flow = view[self._nearest(self.flow, view)]
+
+ def _sig_refresh(self, view):
+ if len(view) == 0:
+ self.flow = None
+ elif self.flow is None:
+ self.flow = view[0]
+ elif self.flow not in view:
+ self.flow = view[self._nearest(self.flow, view)]
+
+ def _sig_add(self, view, flow):
+ # We only have to act if we don't have a focus element
+ if not self.flow:
+ self.flow = flow
+
+
+class Settings(collections.Mapping):
+ def __init__(self, view: View) -> None:
+ self.view = view
+ self.values = {}
+ view.sig_remove.connect(self._sig_remove)
+ view.sig_refresh.connect(self._sig_refresh)
+
+ def __iter__(self) -> typing.Iterable:
+ return iter(self.values)
+
+ def __len__(self) -> int:
+ return len(self.values)
+
+ def __getitem__(self, f: mitmproxy.flow.Flow) -> dict:
+ if f.id not in self.view._store:
+ raise KeyError
+ return self.values.setdefault(f.id, {})
+
+ def _sig_remove(self, view, flow):
+ if flow.id in self.values:
+ del self.values[flow.id]
+
+ def _sig_refresh(self, view):
+ for fid in self.values.keys():
+ if fid not in view._store:
+ del self.values[fid]
diff --git a/setup.py b/setup.py
index 70ff8b5d..fd291973 100644
--- a/setup.py
+++ b/setup.py
@@ -83,6 +83,7 @@ setup(
"urwid>=1.3.1, <1.4",
"watchdog>=0.8.3, <0.9",
"brotlipy>=0.5.1, <0.7",
+ "sortedcontainers>=1.5.4, <1.6",
],
extras_require={
':sys_platform == "win32"': [
diff --git a/test/mitmproxy/addons/test_view.py b/test/mitmproxy/addons/test_view.py
new file mode 100644
index 00000000..15cf534e
--- /dev/null
+++ b/test/mitmproxy/addons/test_view.py
@@ -0,0 +1,275 @@
+from mitmproxy.addons import view
+from mitmproxy import flowfilter
+
+from .. import tutils
+
+
+def test_simple():
+ v = view.View()
+ f = tutils.tflow()
+ f.request.timestamp_start = 1
+ v.request(f)
+ assert list(v) == [f]
+ v.request(f)
+ assert list(v) == [f]
+ assert len(v._store) == 1
+
+ f2 = tutils.tflow()
+ f2.request.timestamp_start = 3
+ v.request(f2)
+ assert list(v) == [f, f2]
+ v.request(f2)
+ assert list(v) == [f, f2]
+ assert len(v._store) == 2
+
+ f3 = tutils.tflow()
+ f3.request.timestamp_start = 2
+ v.request(f3)
+ assert list(v) == [f, f3, f2]
+ v.request(f3)
+ assert list(v) == [f, f3, f2]
+ assert len(v._store) == 3
+
+
+def tft(*, method="get", start=0):
+ f = tutils.tflow()
+ f.request.method = method
+ f.request.timestamp_start = start
+ return f
+
+
+def test_filter():
+ v = view.View()
+ f = flowfilter.parse("~m get")
+ v.request(tft(method="get"))
+ v.request(tft(method="put"))
+ v.request(tft(method="get"))
+ v.request(tft(method="put"))
+ assert(len(v)) == 4
+ v.set_filter(f)
+ assert [i.request.method for i in v] == ["GET", "GET"]
+ assert len(v._store) == 4
+ v.set_filter(None)
+
+
+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.toggle_reversed()
+ assert [i.request.method for i in v] == ["PUT", "PUT", "GET", "GET"]
+
+ v.set_order(view.key_request_start)
+ assert [i.request.timestamp_start for i in v] == [4, 3, 2, 1]
+
+ v.toggle_reversed()
+ assert [i.request.timestamp_start for i in v] == [1, 2, 3, 4]
+
+
+def test_reversed():
+ v = view.View()
+ v.request(tft(start=1))
+ v.request(tft(start=2))
+ v.request(tft(start=3))
+ v.toggle_reversed()
+
+ assert v[0].request.timestamp_start == 3
+ assert v[-1].request.timestamp_start == 1
+ assert v[2].request.timestamp_start == 1
+ tutils.raises(IndexError, v.__getitem__, 5)
+ tutils.raises(IndexError, v.__getitem__, -5)
+
+ assert v.bisect(v[0]) == 1
+ assert v.bisect(v[2]) == 3
+
+
+def test_update():
+ v = view.View()
+ flt = flowfilter.parse("~m get")
+ v.set_filter(flt)
+
+ f = tft(method="get")
+ v.request(f)
+ assert f in v
+
+ f.request.method = "put"
+ v.update(f)
+ assert f not in v
+
+ f.request.method = "get"
+ v.update(f)
+ assert f in v
+
+ v.update(f)
+ assert f in v
+
+
+class Record:
+ def __init__(self):
+ self.calls = []
+
+ def __bool__(self):
+ return bool(self.calls)
+
+ def __repr__(self):
+ return repr(self.calls)
+
+ def __call__(self, *args, **kwargs):
+ self.calls.append((args, kwargs))
+
+
+def test_signals():
+ v = view.View()
+ rec_add = Record()
+ rec_update = Record()
+ rec_remove = Record()
+ rec_refresh = Record()
+
+ def clearrec():
+ rec_add.calls = []
+ rec_update.calls = []
+ rec_remove.calls = []
+ rec_refresh.calls = []
+
+ v.sig_add.connect(rec_add)
+ v.sig_update.connect(rec_update)
+ v.sig_remove.connect(rec_remove)
+ v.sig_refresh.connect(rec_refresh)
+
+ assert not any([rec_add, rec_update, rec_remove, rec_refresh])
+
+ # Simple add
+ v.add(tft())
+ assert rec_add
+ assert not any([rec_update, rec_remove, rec_refresh])
+
+ # Filter change triggers refresh
+ clearrec()
+ v.set_filter(flowfilter.parse("~m put"))
+ assert rec_refresh
+ assert not any([rec_update, rec_add, rec_remove])
+
+ v.set_filter(flowfilter.parse("~m get"))
+
+ # An update that results in a flow being added to the view
+ clearrec()
+ v[0].request.method = "PUT"
+ v.update(v[0])
+ assert rec_remove
+ assert not any([rec_update, rec_refresh, rec_add])
+
+ # An update that does not affect the view just sends update
+ v.set_filter(flowfilter.parse("~m put"))
+ clearrec()
+ v.update(v[0])
+ assert rec_update
+ assert not any([rec_remove, rec_refresh, rec_add])
+
+ # An update for a flow in state but not view does not do anything
+ f = v[0]
+ v.set_filter(flowfilter.parse("~m get"))
+ assert not len(v)
+ clearrec()
+ v.update(f)
+ assert not any([rec_add, rec_update, rec_remove, rec_refresh])
+
+
+def test_focus():
+ # Special case - initialising with a view that already contains data
+ v = view.View()
+ v.add(tft())
+ f = view.Focus(v)
+ assert f.index is 0
+ assert f.flow is v[0]
+
+ # Start empty
+ v = view.View()
+ f = view.Focus(v)
+ assert f.index is None
+ assert f.flow is None
+
+ v.add(tft(start=1))
+ assert f.index == 0
+ assert f.flow is v[0]
+
+ v.add(tft(start=0))
+ assert f.index == 1
+ assert f.flow is v[1]
+
+ v.add(tft(start=2))
+ assert f.index == 1
+ assert f.flow is v[1]
+
+ v.remove(v[1])
+ assert f.index == 1
+ assert f.flow is v[1]
+
+ v.remove(v[1])
+ assert f.index == 0
+ assert f.flow is v[0]
+
+ v.remove(v[0])
+ assert f.index is None
+ assert f.flow is None
+
+ v.add(tft(method="get", start=0))
+ v.add(tft(method="get", start=1))
+ v.add(tft(method="put", start=2))
+ v.add(tft(method="get", start=3))
+
+ f.flow = v[2]
+ assert f.flow.request.method == "PUT"
+
+ filt = flowfilter.parse("~m get")
+ v.set_filter(filt)
+ assert f.index == 2
+
+ filt = flowfilter.parse("~m oink")
+ v.set_filter(filt)
+ assert f.index is None
+
+
+def test_focus_nextprev():
+ v = view.View()
+ # Nops on an empty view
+ v.focus.next()
+ v.focus.prev()
+
+ # Nops on a single-flow view
+ v.add(tft(start=0))
+ assert v.focus.flow == v[0]
+ v.focus.next()
+ assert v.focus.flow == v[0]
+ v.focus.prev()
+ assert v.focus.flow == v[0]
+
+ v.add(tft(start=1))
+ v.focus.next()
+ assert v.focus.flow == v[1]
+ v.focus.next()
+ assert v.focus.flow == v[1]
+ v.focus.prev()
+ assert v.focus.flow == v[0]
+ v.focus.prev()
+ assert v.focus.flow == v[0]
+
+
+def test_settings():
+ v = view.View()
+ f = tft()
+
+ 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
+ v.remove(f)
+ tutils.raises(KeyError, v.settings.__getitem__, f)
+ assert not v.settings.keys()