Merge pull request #1683 from cortesi/view

addons.View
This commit is contained in:
Aldo Cortesi 2016-10-29 12:19:19 +13:00 committed by GitHub
commit a3131ac343
3 changed files with 545 additions and 0 deletions

269
mitmproxy/addons/view.py Normal file
View File

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

View File

@ -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"': [

View File

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