improve flowfilter api: raise on invalid input, add ~all

This commit is contained in:
Maximilian Hils 2021-10-08 17:43:52 +02:00
parent adfccb90a5
commit c43a2ef8dc
17 changed files with 82 additions and 76 deletions

View File

@ -10,7 +10,8 @@ class Filter:
self.filter: flowfilter.TFilter = None self.filter: flowfilter.TFilter = None
def configure(self, updated): def configure(self, updated):
self.filter = flowfilter.parse(ctx.options.flowfilter) if "flowfilter" in updated:
self.filter = flowfilter.parse(ctx.options.flowfilter)
def load(self, l): def load(self, l):
l.add_option( l.add_option(

View File

@ -28,8 +28,6 @@ def parse_spec(option: str) -> BlockSpec:
except ValueError: except ValueError:
raise ValueError(f"Invalid HTTP status code: {status}") raise ValueError(f"Invalid HTTP status code: {status}")
flow_filter = flowfilter.parse(flow_patt) flow_filter = flowfilter.parse(flow_patt)
if not flow_filter:
raise ValueError(f"Invalid filter pattern: {flow_patt}")
if not RESPONSES.get(status_code): if not RESPONSES.get(status_code):
raise ValueError(f"Invalid HTTP status code: {status}") raise ValueError(f"Invalid HTTP status code: {status}")

View File

@ -59,11 +59,10 @@ class Dumper:
def configure(self, updated): def configure(self, updated):
if "dumper_filter" in updated: if "dumper_filter" in updated:
if ctx.options.dumper_filter: if ctx.options.dumper_filter:
self.filter = flowfilter.parse(ctx.options.dumper_filter) try:
if not self.filter: self.filter = flowfilter.parse(ctx.options.dumper_filter)
raise exceptions.OptionsError( except ValueError as e:
"Invalid filter expression: %s" % ctx.options.dumper_filter raise exceptions.OptionsError(str(e)) from e
)
else: else:
self.filter = None self.filter = None

View File

@ -21,9 +21,10 @@ class Intercept:
def configure(self, updated): def configure(self, updated):
if "intercept" in updated: if "intercept" in updated:
if ctx.options.intercept: if ctx.options.intercept:
self.filt = flowfilter.parse(ctx.options.intercept) try:
if not self.filt: self.filt = flowfilter.parse(ctx.options.intercept)
raise exceptions.OptionsError(f"Invalid interception filter: {ctx.options.intercept}") except ValueError as e:
raise exceptions.OptionsError(str(e)) from e
ctx.options.intercept_active = True ctx.options.intercept_active = True
else: else:
self.filt = None self.filt = None

View File

@ -30,14 +30,13 @@ class ReadFile:
def configure(self, updated): def configure(self, updated):
if "readfile_filter" in updated: if "readfile_filter" in updated:
filt = None
if ctx.options.readfile_filter: if ctx.options.readfile_filter:
filt = flowfilter.parse(ctx.options.readfile_filter) try:
if not filt: self.filter = flowfilter.parse(ctx.options.readfile_filter)
raise exceptions.OptionsError( except ValueError as e:
"Invalid readfile filter: %s" % ctx.options.readfile_filter raise exceptions.OptionsError(str(e)) from e
) else:
self.filter = filt self.filter = None
async def load_flows(self, fo: typing.IO[bytes]) -> int: async def load_flows(self, fo: typing.IO[bytes]) -> int:
cnt = 0 cnt = 0

View File

@ -48,11 +48,10 @@ class Save:
# We're already streaming - stop the previous stream and restart # We're already streaming - stop the previous stream and restart
if "save_stream_filter" in updated: if "save_stream_filter" in updated:
if ctx.options.save_stream_filter: if ctx.options.save_stream_filter:
self.filt = flowfilter.parse(ctx.options.save_stream_filter) try:
if not self.filt: self.filt = flowfilter.parse(ctx.options.save_stream_filter)
raise exceptions.OptionsError( except ValueError as e:
"Invalid filter specification: %s" % ctx.options.save_stream_filter raise exceptions.OptionsError(str(e)) from e
)
else: else:
self.filt = None self.filt = None
if "save_stream_file" in updated or "save_stream_filter" in updated: if "save_stream_file" in updated or "save_stream_filter" in updated:

View File

@ -19,12 +19,10 @@ class StickyAuth:
def configure(self, updated): def configure(self, updated):
if "stickyauth" in updated: if "stickyauth" in updated:
if ctx.options.stickyauth: if ctx.options.stickyauth:
flt = flowfilter.parse(ctx.options.stickyauth) try:
if not flt: self.flt = flowfilter.parse(ctx.options.stickyauth)
raise exceptions.OptionsError( except ValueError as e:
"stickyauth: invalid filter expression: %s" % ctx.options.stickyauth raise exceptions.OptionsError(str(e)) from e
)
self.flt = flt
else: else:
self.flt = None self.flt = None

View File

@ -43,12 +43,10 @@ class StickyCookie:
def configure(self, updated): def configure(self, updated):
if "stickycookie" in updated: if "stickycookie" in updated:
if ctx.options.stickycookie: if ctx.options.stickycookie:
flt = flowfilter.parse(ctx.options.stickycookie) try:
if not flt: self.flt = flowfilter.parse(ctx.options.stickycookie)
raise exceptions.OptionsError( except ValueError as e:
"stickycookie: invalid filter expression: %s" % ctx.options.stickycookie raise exceptions.OptionsError(str(e)) from e
)
self.flt = flt
else: else:
self.flt = None self.flt = None

View File

@ -114,9 +114,6 @@ class OrderKeySize(_OrderKey):
else: else:
raise NotImplementedError() raise NotImplementedError()
matchall = flowfilter.parse("~http | ~tcp")
orders = [ orders = [
("t", "time"), ("t", "time"),
("m", "method"), ("m", "method"),
@ -129,7 +126,7 @@ class View(collections.abc.Sequence):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._store = collections.OrderedDict() self._store = collections.OrderedDict()
self.filter = matchall self.filter = flowfilter.match_all
# Should we show only marked flows? # Should we show only marked flows?
self.show_marked = False self.show_marked = False
@ -326,11 +323,10 @@ class View(collections.abc.Sequence):
""" """
filt = None filt = None
if filter_expr: if filter_expr:
filt = flowfilter.parse(filter_expr) try:
if not filt: filt = flowfilter.parse(filter_expr)
raise exceptions.CommandError( except ValueError as e:
"Invalid interception filter: %s" % filter_expr raise exceptions.CommandError(str(e)) from e
)
self.set_filter(filt) self.set_filter(filt)
def set_filter(self, flt: typing.Optional[flowfilter.TFilter]): def set_filter(self, flt: typing.Optional[flowfilter.TFilter]):
@ -454,9 +450,10 @@ class View(collections.abc.Sequence):
ids = flow_spec[1:].split(",") ids = flow_spec[1:].split(",")
return [i for i in self._store.values() if i.id in ids] return [i for i in self._store.values() if i.id in ids]
else: else:
filt = flowfilter.parse(flow_spec) try:
if not filt: filt = flowfilter.parse(flow_spec)
raise exceptions.CommandError("Invalid flow filter: %s" % flow_spec) except ValueError as e:
raise exceptions.CommandError(str(e)) from e
return [i for i in self._store.values() if filt(i)] return [i for i in self._store.values() if filt(i)]
@command.command("view.flows.create") @command.command("view.flows.create")
@ -547,11 +544,10 @@ class View(collections.abc.Sequence):
if "view_filter" in updated: if "view_filter" in updated:
filt = None filt = None
if ctx.options.view_filter: if ctx.options.view_filter:
filt = flowfilter.parse(ctx.options.view_filter) try:
if not filt: filt = flowfilter.parse(ctx.options.view_filter)
raise exceptions.OptionsError( except ValueError as e:
"Invalid interception filter: %s" % ctx.options.view_filter raise exceptions.OptionsError(str(e)) from e
)
self.set_filter(filt) self.set_filter(filt)
if "view_order" in updated: if "view_order" in updated:
if ctx.options.view_order not in self.orders: if ctx.options.view_order not in self.orders:

View File

@ -35,7 +35,7 @@
import functools import functools
import re import re
import sys import sys
from typing import Callable, ClassVar, Optional, Sequence, Type from typing import Callable, ClassVar, Optional, Sequence, Type, Protocol, Union
import pyparsing as pp import pyparsing as pp
from mitmproxy import flow, http, tcp from mitmproxy import flow, http, tcp
@ -135,6 +135,14 @@ class FResp(_Action):
return bool(f.response) return bool(f.response)
class FAll(_Action):
code = "all"
help = "Match all flows"
def __call__(self, f: flow.Flow):
return True
class _Rex(_Action): class _Rex(_Action):
flags = 0 flags = 0
is_binary = True is_binary = True
@ -504,6 +512,7 @@ filter_unary: Sequence[Type[_Action]] = [
FResp, FResp,
FTCP, FTCP,
FWebSocket, FWebSocket,
FAll,
] ]
filter_rex: Sequence[Type[_Rex]] = [ filter_rex: Sequence[Type[_Rex]] = [
FBod, FBod,
@ -583,21 +592,31 @@ def _make():
bnf = _make() bnf = _make()
TFilter = Callable[[flow.Flow], bool]
def parse(s: str) -> Optional[TFilter]: class TFilter(Protocol):
pattern: str
def __call__(self, f: flow.Flow) -> bool:
...
def parse(s: str) -> TFilter:
"""
Parse a filter expression and return the compiled filter function.
If the filter syntax is invalid, `ValueError` is raised.
"""
if not s:
raise ValueError("Empty filter expression")
try: try:
flt = bnf.parseString(s, parseAll=True)[0] flt = bnf.parseString(s, parseAll=True)[0]
flt.pattern = s flt.pattern = s
return flt return flt
except pp.ParseException: except (pp.ParseException, ValueError) as e:
return None raise ValueError(f"Invalid filter expression: {s!r}") from e
except ValueError:
return None
def match(flt, flow): def match(flt: Union[str, TFilter], flow: flow.Flow) -> bool:
""" """
Matches a flow against a compiled filter expression. Matches a flow against a compiled filter expression.
Returns True if matched, False if not. Returns True if matched, False if not.
@ -607,13 +626,15 @@ def match(flt, flow):
""" """
if isinstance(flt, str): if isinstance(flt, str):
flt = parse(flt) flt = parse(flt)
if not flt:
raise ValueError("Invalid filter expression.")
if flt: if flt:
return flt(flow) return flt(flow)
return True return True
match_all: TFilter = parse("~all")
"""A filter function that matches all flows"""
help = [] help = []
for a in filter_unary: for a in filter_unary:
help.append( help.append(

View File

@ -2,10 +2,6 @@ import typing
from mitmproxy import flowfilter from mitmproxy import flowfilter
def _match_all(flow) -> bool:
return True
def parse_spec(option: str) -> typing.Tuple[flowfilter.TFilter, str, str]: def parse_spec(option: str) -> typing.Tuple[flowfilter.TFilter, str, str]:
""" """
Parse strings in the following format: Parse strings in the following format:
@ -17,7 +13,7 @@ def parse_spec(option: str) -> typing.Tuple[flowfilter.TFilter, str, str]:
parts = rem.split(sep, 2) parts = rem.split(sep, 2)
if len(parts) == 2: if len(parts) == 2:
subject, replacement = parts subject, replacement = parts
return _match_all, subject, replacement return flowfilter.match_all, subject, replacement
elif len(parts) == 3: elif len(parts) == 3:
patt, subject, replacement = parts patt, subject, replacement = parts
flow_filter = flowfilter.parse(patt) flow_filter = flowfilter.parse(patt)

View File

@ -43,7 +43,7 @@ class TestReadFile:
rf = readfile.ReadFile() rf = readfile.ReadFile()
with taddons.context(rf) as tctx: with taddons.context(rf) as tctx:
tctx.configure(rf, readfile_filter="~q") tctx.configure(rf, readfile_filter="~q")
with pytest.raises(Exception, match="Invalid readfile filter"): with pytest.raises(Exception, match="Invalid filter expression"):
tctx.configure(rf, readfile_filter="~~") tctx.configure(rf, readfile_filter="~~")
@pytest.mark.asyncio @pytest.mark.asyncio

View File

@ -16,7 +16,7 @@ class TestStickyCookie:
def test_config(self): def test_config(self):
sc = stickycookie.StickyCookie() sc = stickycookie.StickyCookie()
with taddons.context(sc) as tctx: with taddons.context(sc) as tctx:
with pytest.raises(Exception, match="invalid filter"): with pytest.raises(Exception, match="Invalid filter expression"):
tctx.configure(sc, stickycookie="~b") tctx.configure(sc, stickycookie="~b")
tctx.configure(sc, stickycookie="foo") tctx.configure(sc, stickycookie="foo")

View File

@ -268,7 +268,7 @@ def test_resolve():
assert m(tctx.command(v.resolve, "@unmarked")) == ["PUT", "GET", "PUT"] assert m(tctx.command(v.resolve, "@unmarked")) == ["PUT", "GET", "PUT"]
assert m(tctx.command(v.resolve, "@all")) == ["GET", "PUT", "GET", "PUT"] assert m(tctx.command(v.resolve, "@all")) == ["GET", "PUT", "GET", "PUT"]
with pytest.raises(exceptions.CommandError, match="Invalid flow filter"): with pytest.raises(exceptions.CommandError, match="Invalid filter expression"):
tctx.command(v.resolve, "~") tctx.command(v.resolve, "~")
@ -608,7 +608,7 @@ def test_configure():
v = view.View() v = view.View()
with taddons.context(v) as tctx: with taddons.context(v) as tctx:
tctx.configure(v, view_filter="~q") tctx.configure(v, view_filter="~q")
with pytest.raises(Exception, match="Invalid interception filter"): with pytest.raises(Exception, match="Invalid filter expression"):
tctx.configure(v, view_filter="~~") tctx.configure(v, view_filter="~~")
tctx.configure(v, view_order="method") tctx.configure(v, view_order="method")

View File

@ -13,10 +13,12 @@ class TestParsing:
assert c.getvalue() assert c.getvalue()
def test_parse_err(self): def test_parse_err(self):
assert flowfilter.parse("~h [") is None with pytest.raises(ValueError):
flowfilter.parse("~b")
with pytest.raises(ValueError):
flowfilter.parse("~h [")
def test_simple(self): def test_simple(self):
assert not flowfilter.parse("~b")
assert flowfilter.parse("~q") assert flowfilter.parse("~q")
assert flowfilter.parse("~c 10") assert flowfilter.parse("~c 10")
assert flowfilter.parse("~m foobar") assert flowfilter.parse("~m foobar")

View File

@ -16,5 +16,5 @@ def test_parse_spec():
with pytest.raises(ValueError, match="Invalid number of parameters"): with pytest.raises(ValueError, match="Invalid number of parameters"):
parse_spec("/") parse_spec("/")
with pytest.raises(ValueError, match="Invalid filter pattern"): with pytest.raises(ValueError, match="Invalid filter expression"):
parse_spec("/~b/one/two") parse_spec("/~b/one/two")

View File

@ -14,7 +14,6 @@ export interface OptionsState {
ciphers_server: string | undefined ciphers_server: string | undefined
client_certs: string | undefined client_certs: string | undefined
client_replay: string[] client_replay: string[]
client_replay_concurrency: number
command_history: boolean command_history: boolean
confdir: string confdir: string
connection_strategy: string connection_strategy: string
@ -100,7 +99,6 @@ export const defaultState: OptionsState = {
ciphers_server: undefined, ciphers_server: undefined,
client_certs: undefined, client_certs: undefined,
client_replay: [], client_replay: [],
client_replay_concurrency: 1,
command_history: true, command_history: true,
confdir: "~/.mitmproxy", confdir: "~/.mitmproxy",
connection_strategy: "eager", connection_strategy: "eager",