mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-23 00:01:36 +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",
|
"urwid>=1.3.1, <1.4",
|
||||||
"watchdog>=0.8.3, <0.9",
|
"watchdog>=0.8.3, <0.9",
|
||||||
"brotlipy>=0.5.1, <0.7",
|
"brotlipy>=0.5.1, <0.7",
|
||||||
|
"sortedcontainers>=1.5.4, <1.6",
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
':sys_platform == "win32"': [
|
':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