mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-26 18:18:25 +00:00
commit
a3131ac343
269
mitmproxy/addons/view.py
Normal file
269
mitmproxy/addons/view.py
Normal 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]
|
1
setup.py
1
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"': [
|
||||
|
275
test/mitmproxy/addons/test_view.py
Normal file
275
test/mitmproxy/addons/test_view.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user