Merge pull request #4847 from mhils/flowfilter

Flowfilter Improvements
This commit is contained in:
Maximilian Hils 2021-10-09 18:38:56 +02:00 committed by GitHub
commit aad92c9d5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 799 additions and 487 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

@ -115,8 +115,6 @@ class OrderKeySize(_OrderKey):
raise NotImplementedError() raise NotImplementedError()
matchall = flowfilter.parse("~http | ~tcp")
orders = [ orders = [
("t", "time"), ("t", "time"),
("m", "method"), ("m", "method"),
@ -129,7 +127,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,15 +324,14 @@ 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]):
self.filter = flt or matchall self.filter = flt or flowfilter.match_all
self._refilter() self._refilter()
# View Updates # View Updates
@ -454,9 +451,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 +545,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 ClassVar, 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:
... # pragma: no cover
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

@ -58,6 +58,7 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
"type": flow.type, "type": flow.type,
"modified": flow.modified(), "modified": flow.modified(),
"marked": emoji.get(flow.marked, "🔴") if flow.marked else "", "marked": emoji.get(flow.marked, "🔴") if flow.marked else "",
"comment": flow.comment,
} }
if flow.client_conn: if flow.client_conn:

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,12 +13,10 @@ 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)
if not flow_filter:
raise ValueError(f"Invalid filter pattern: {patt}")
return flow_filter, subject, replacement return flow_filter, subject, replacement
else: else:
raise ValueError("Invalid number of parameters (2 or 3 are expected)") raise ValueError("Invalid number of parameters (2 or 3 are expected)")

View File

@ -43,8 +43,9 @@ 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="~~")
tctx.configure(rf, readfile_filter="")
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_read(self, tmpdir, data, corrupt_data): async def test_read(self, tmpdir, data, corrupt_data):

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,14 @@ 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, match="Empty filter"):
flowfilter.parse("")
with pytest.raises(ValueError, match="Invalid filter"):
flowfilter.parse("~b")
with pytest.raises(ValueError, match="Invalid filter"):
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")
@ -568,6 +572,8 @@ class TestMatchingDummyFlow:
f = self.flow() f = self.flow()
f.server_conn = tflow.tserver_conn() f.server_conn = tflow.tserver_conn()
assert self.q("~all", f)
assert not self.q("~a", f) assert not self.q("~a", f)
assert not self.q("~b whatever", f) assert not self.q("~b whatever", f)

View File

@ -39,15 +39,18 @@ def console(monkeypatch):
return m return m
@pytest.mark.asyncio
def test_integration(tdata, console): def test_integration(tdata, console):
console.type(f":view.flows.load {tdata.path('mitmproxy/data/dumpfile-7.mitm')}<enter>") console.type(f":view.flows.load {tdata.path('mitmproxy/data/dumpfile-7.mitm')}<enter>")
console.type("<enter><tab><tab>") console.type("<enter><tab><tab>")
console.type("<space><tab><tab>") # view second flow console.type("<space><tab><tab>") # view second flow
@pytest.mark.asyncio
def test_options_home_end(console): def test_options_home_end(console):
console.type("O<home><end>") console.type("O<home><end>")
@pytest.mark.asyncio
def test_keybindings_home_end(console): def test_keybindings_home_end(console):
console.type("K<home><end>") console.type("K<home><end>")

View File

@ -48,6 +48,7 @@ def test_generate_tflow_js(tdata):
] ]
tf_http.request.trailers = Headers(trailer="qvalue") tf_http.request.trailers = Headers(trailer="qvalue")
tf_http.response.trailers = Headers(trailer="qvalue") tf_http.response.trailers = Headers(trailer="qvalue")
tf_http.comment = "I'm a comment!"
tf_tcp = tflow.ttcpflow(err=True) tf_tcp = tflow.ttcpflow(err=True)
tf_tcp.id = "2ea7012b-21b5-4f8f-98cd-d49819954001" tf_tcp.id = "2ea7012b-21b5-4f8f-98cd-d49819954001"

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

@ -64,6 +64,9 @@
&.selected { &.selected {
background-color: #e0ebf5 !important; background-color: #e0ebf5 !important;
} }
&.selected.highlighted {
background-color: #7bbefc !important;
}
&.highlighted { &.highlighted {
background-color: #ffeb99; background-color: #ffeb99;

View File

@ -22,6 +22,7 @@ export function THTTPFlow(): Required<HTTPFlow> {
"tls_established": true, "tls_established": true,
"tls_version": "TLSv1.2" "tls_version": "TLSv1.2"
}, },
"comment": "I'm a comment!",
"error": { "error": {
"msg": "error", "msg": "error",
"timestamp": 946681207.0 "timestamp": 946681207.0
@ -180,6 +181,7 @@ export function TTCPFlow(): Required<TCPFlow> {
"tls_established": true, "tls_established": true,
"tls_version": "TLSv1.2" "tls_version": "TLSv1.2"
}, },
"comment": "",
"error": { "error": {
"msg": "error", "msg": "error",
"timestamp": 946681207.0 "timestamp": 946681207.0

View File

@ -83,7 +83,7 @@ describe('updateUrlFromStore', () => {
}, },
store = mockStore(state) store = mockStore(state)
updateUrlFromStore(store) updateUrlFromStore(store)
expect(history.replaceState).toBeCalledWith(undefined, '', '/#/flows/123/request?s=~u foo') expect(history.replaceState).toBeCalledWith(undefined, '', '/#/flows/123/request?s=~u%20foo')
}) })
}) })

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,4 @@
// PEG.js filter rules - see https://pegjs.org/ // PEG.js filter rules - see https://pegjs.org/
{ {
function or(first, second) { function or(first, second) {
// Add explicit function names to ease debugging. // Add explicit function names to ease debugging.
@ -9,6 +8,7 @@ function or(first, second) {
orFilter.desc = first.desc + " or " + second.desc; orFilter.desc = first.desc + " or " + second.desc;
return orFilter; return orFilter;
} }
function and(first, second) { function and(first, second) {
function andFilter() { function andFilter() {
return first.apply(this, arguments) && second.apply(this, arguments); return first.apply(this, arguments) && second.apply(this, arguments);
@ -16,6 +16,7 @@ function and(first, second) {
andFilter.desc = first.desc + " and " + second.desc; andFilter.desc = first.desc + " and " + second.desc;
return andFilter; return andFilter;
} }
function not(expr) { function not(expr) {
function notFilter() { function notFilter() {
return !expr.apply(this, arguments); return !expr.apply(this, arguments);
@ -23,6 +24,7 @@ function not(expr) {
notFilter.desc = "not " + expr.desc; notFilter.desc = "not " + expr.desc;
return notFilter; return notFilter;
} }
function binding(expr) { function binding(expr) {
function bindingFilter() { function bindingFilter() {
return expr.apply(this, arguments); return expr.apply(this, arguments);
@ -30,22 +32,20 @@ function binding(expr) {
bindingFilter.desc = "(" + expr.desc + ")"; bindingFilter.desc = "(" + expr.desc + ")";
return bindingFilter; return bindingFilter;
} }
function trueFilter(flow) {
// ~all
function allFilter(flow) {
return true; return true;
} }
trueFilter.desc = "true"; allFilter.desc = "all flows";
function falseFilter(flow) {
return false;
}
falseFilter.desc = "false";
// ~a
var ASSET_TYPES = [ var ASSET_TYPES = [
new RegExp("text/javascript"), new RegExp("text/javascript"),
new RegExp("application/x-javascript"), new RegExp("application/x-javascript"),
new RegExp("application/javascript"), new RegExp("application/javascript"),
new RegExp("text/css"), new RegExp("text/css"),
new RegExp("image/.*"), new RegExp("image/.*")
new RegExp("application/x-shockwave-flash")
]; ];
function assetFilter(flow) { function assetFilter(flow) {
if (flow.response) { if (flow.response) {
@ -60,13 +60,8 @@ function assetFilter(flow) {
return false; return false;
} }
assetFilter.desc = "is asset"; assetFilter.desc = "is asset";
function responseCode(code){
function responseCodeFilter(flow){ // ~b
return flow.response && flow.response.status_code === code;
}
responseCodeFilter.desc = "resp. code is " + code;
return responseCodeFilter;
}
function body(regex){ function body(regex){
regex = new RegExp(regex, "i"); regex = new RegExp(regex, "i");
function bodyFilter(flow){ function bodyFilter(flow){
@ -75,6 +70,8 @@ function body(regex){
bodyFilter.desc = "body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10"; bodyFilter.desc = "body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10";
return bodyFilter; return bodyFilter;
} }
// ~bq
function requestBody(regex){ function requestBody(regex){
regex = new RegExp(regex, "i"); regex = new RegExp(regex, "i");
function requestBodyFilter(flow){ function requestBodyFilter(flow){
@ -83,6 +80,8 @@ function requestBody(regex){
requestBodyFilter.desc = "body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10"; requestBodyFilter.desc = "body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10";
return requestBodyFilter; return requestBodyFilter;
} }
// ~bs
function responseBody(regex){ function responseBody(regex){
regex = new RegExp(regex, "i"); regex = new RegExp(regex, "i");
function responseBodyFilter(flow){ function responseBodyFilter(flow){
@ -91,6 +90,27 @@ function responseBody(regex){
responseBodyFilter.desc = "body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10"; responseBodyFilter.desc = "body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10";
return responseBodyFilter; return responseBodyFilter;
} }
// ~c
function responseCode(code){
function responseCodeFilter(flow){
return flow.response && flow.response.status_code === code;
}
responseCodeFilter.desc = "resp. code is " + code;
return responseCodeFilter;
}
// ~comment
function comment(regex){
regex = new RegExp(regex, "i");
function commentFilter(flow){
return regex.test(flow.comment)
}
commentFilter.desc = "comment matches " + regex;
return commentFilter;
}
// ~d
function domain(regex){ function domain(regex){
regex = new RegExp(regex, "i"); regex = new RegExp(regex, "i");
function domainFilter(flow){ function domainFilter(flow){
@ -99,6 +119,8 @@ function domain(regex){
domainFilter.desc = "domain matches " + regex; domainFilter.desc = "domain matches " + regex;
return domainFilter; return domainFilter;
} }
// ~dst
function destination(regex){ function destination(regex){
regex = new RegExp(regex, "i"); regex = new RegExp(regex, "i");
function destinationFilter(flow){ function destinationFilter(flow){
@ -109,10 +131,14 @@ function destination(regex){
destinationFilter.desc = "destination address matches " + regex; destinationFilter.desc = "destination address matches " + regex;
return destinationFilter; return destinationFilter;
} }
// ~e
function errorFilter(flow){ function errorFilter(flow){
return !!flow.error; return !!flow.error;
} }
errorFilter.desc = "has error"; errorFilter.desc = "has error";
// ~h
function header(regex){ function header(regex){
regex = new RegExp(regex, "i"); regex = new RegExp(regex, "i");
function headerFilter(flow){ function headerFilter(flow){
@ -125,6 +151,8 @@ function header(regex){
headerFilter.desc = "header matches " + regex; headerFilter.desc = "header matches " + regex;
return headerFilter; return headerFilter;
} }
// ~hq
function requestHeader(regex){ function requestHeader(regex){
regex = new RegExp(regex, "i"); regex = new RegExp(regex, "i");
function requestHeaderFilter(flow){ function requestHeaderFilter(flow){
@ -133,6 +161,8 @@ function requestHeader(regex){
requestHeaderFilter.desc = "req. header matches " + regex; requestHeaderFilter.desc = "req. header matches " + regex;
return requestHeaderFilter; return requestHeaderFilter;
} }
// ~hs
function responseHeader(regex){ function responseHeader(regex){
regex = new RegExp(regex, "i"); regex = new RegExp(regex, "i");
function responseHeaderFilter(flow){ function responseHeaderFilter(flow){
@ -141,10 +171,30 @@ function responseHeader(regex){
responseHeaderFilter.desc = "resp. header matches " + regex; responseHeaderFilter.desc = "resp. header matches " + regex;
return responseHeaderFilter; return responseHeaderFilter;
} }
// ~http
function httpFilter(flow){ function httpFilter(flow){
return flow.type === "http"; return flow.type === "http";
} }
httpFilter.desc = "is an HTTP Flow"; httpFilter.desc = "is an HTTP Flow";
// ~marked
function markedFilter(flow){
return flow.marked;
}
markedFilter.desc = "is marked";
// ~marker
function marker(regex){
regex = new RegExp(regex, "i");
function markerFilter(flow){
return regex.test(flow.marked)
}
markerFilter.desc = "marker matches " + regex;
return markerFilter;
}
// ~m
function method(regex){ function method(regex){
regex = new RegExp(regex, "i"); regex = new RegExp(regex, "i");
function methodFilter(flow){ function methodFilter(flow){
@ -153,18 +203,32 @@ function method(regex){
methodFilter.desc = "method matches " + regex; methodFilter.desc = "method matches " + regex;
return methodFilter; return methodFilter;
} }
function markedFilter(flow){
return flow.marked; // ~q
}
markedFilter.desc = "is marked";
function noResponseFilter(flow){ function noResponseFilter(flow){
return flow.request && !flow.response; return flow.request && !flow.response;
} }
noResponseFilter.desc = "has no response"; noResponseFilter.desc = "has no response";
function responseFilter(flow){
return !!flow.response; // ~replayq
function clientReplayFilter(flow){
return flow.is_replay === "request";
} }
responseFilter.desc = "has response"; clientReplayFilter.desc = "request has been replayed";
// ~replays
function serverReplayFilter(flow){
return flow.is_replay === "response";
}
serverReplayFilter.desc = "response has been replayed";
// ~replay
function replayFilter(flow){
return !!flow.is_replay;
}
replayFilter.desc = "flow has been replayed";
// ~src
function source(regex){ function source(regex){
regex = new RegExp(regex, "i"); regex = new RegExp(regex, "i");
function sourceFilter(flow){ function sourceFilter(flow){
@ -175,6 +239,40 @@ function source(regex){
sourceFilter.desc = "source address matches " + regex; sourceFilter.desc = "source address matches " + regex;
return sourceFilter; return sourceFilter;
} }
// ~s
function responseFilter(flow){
return !!flow.response;
}
responseFilter.desc = "has response";
// ~tcp
function tcpFilter(flow){
return flow.type === "tcp";
}
tcpFilter.desc = "is a TCP Flow";
// ~tq
function requestContentType(regex){
regex = new RegExp(regex, "i");
function requestContentTypeFilter(flow){
return flow.request && regex.test(flowutils.RequestUtils.getContentType(flow.request));
}
requestContentTypeFilter.desc = "req. content type matches " + regex;
return requestContentTypeFilter;
}
// ~ts
function responseContentType(regex){
regex = new RegExp(regex, "i");
function responseContentTypeFilter(flow){
return flow.response && regex.test(flowutils.ResponseUtils.getContentType(flow.response));
}
responseContentTypeFilter.desc = "resp. content type matches " + regex;
return responseContentTypeFilter;
}
// ~t
function contentType(regex){ function contentType(regex){
regex = new RegExp(regex, "i"); regex = new RegExp(regex, "i");
function contentTypeFilter(flow){ function contentTypeFilter(flow){
@ -187,26 +285,8 @@ function contentType(regex){
contentTypeFilter.desc = "content type matches " + regex; contentTypeFilter.desc = "content type matches " + regex;
return contentTypeFilter; return contentTypeFilter;
} }
function tcpFilter(flow){
return flow.type === "tcp"; // ~u
}
tcpFilter.desc = "is a TCP Flow";
function requestContentType(regex){
regex = new RegExp(regex, "i");
function requestContentTypeFilter(flow){
return flow.request && regex.test(flowutils.RequestUtils.getContentType(flow.request));
}
requestContentTypeFilter.desc = "req. content type matches " + regex;
return requestContentTypeFilter;
}
function responseContentType(regex){
regex = new RegExp(regex, "i");
function responseContentTypeFilter(flow){
return flow.response && regex.test(flowutils.ResponseUtils.getContentType(flow.response));
}
responseContentTypeFilter.desc = "resp. content type matches " + regex;
return responseContentTypeFilter;
}
function url(regex){ function url(regex){
regex = new RegExp(regex, "i"); regex = new RegExp(regex, "i");
function urlFilter(flow){ function urlFilter(flow){
@ -215,6 +295,8 @@ function url(regex){
urlFilter.desc = "url matches " + regex; urlFilter.desc = "url matches " + regex;
return urlFilter; return urlFilter;
} }
// ~websocket
function websocketFilter(flow){ function websocketFilter(flow){
return flow.type === "websocket"; return flow.type === "websocket";
} }
@ -250,19 +332,18 @@ BindingExpr
{ return binding(expr); } { return binding(expr); }
/ Expr / Expr
/* All the filters except "~s" and "~src" are arranged in the ascending order as /* All the filters are generally arranged in the order as they are described
given in the docs(https://mitmproxy.org/docs/latest/concepts-filters/). on https://docs.mitmproxy.org/dev/concepts-filters/, with the exception of
"~s" and "~src" are so arranged as "~s" caused problems in the evaluation of single-letter filters, which are moved to the bottom so that they evaluate properly */
"~src". */
Expr Expr
= "true" { return trueFilter; } = "~all" { return allFilter; }
/ "false" { return falseFilter; }
/ "~a" { return assetFilter; } / "~a" { return assetFilter; }
/ "~b" ws+ s:StringLiteral { return body(s); } / "~b" ws+ s:StringLiteral { return body(s); }
/ "~bq" ws+ s:StringLiteral { return requestBody(s); } / "~bq" ws+ s:StringLiteral { return requestBody(s); }
/ "~bs" ws+ s:StringLiteral { return responseBody(s); } / "~bs" ws+ s:StringLiteral { return responseBody(s); }
/ "~c" ws+ s:IntegerLiteral { return responseCode(s); } / "~c" ws+ s:IntegerLiteral { return responseCode(s); }
/ "~comment" ws+ s:StringLiteral { return comment(s); }
/ "~d" ws+ s:StringLiteral { return domain(s); } / "~d" ws+ s:StringLiteral { return domain(s); }
/ "~dst" ws+ s:StringLiteral { return destination(s); } / "~dst" ws+ s:StringLiteral { return destination(s); }
/ "~e" { return errorFilter; } / "~e" { return errorFilter; }
@ -270,15 +351,19 @@ Expr
/ "~hq" ws+ s:StringLiteral { return requestHeader(s); } / "~hq" ws+ s:StringLiteral { return requestHeader(s); }
/ "~hs" ws+ s:StringLiteral { return responseHeader(s); } / "~hs" ws+ s:StringLiteral { return responseHeader(s); }
/ "~http" { return httpFilter; } / "~http" { return httpFilter; }
/ "~m" ws+ s:StringLiteral { return method(s); }
/ "~marked" { return markedFilter; } / "~marked" { return markedFilter; }
/ "~marker" ws+ s:StringLiteral { return marker(s); }
/ "~m" ws+ s:StringLiteral { return method(s); }
/ "~q" { return noResponseFilter; } / "~q" { return noResponseFilter; }
/ "~replayq" { return clientReplayFilter; }
/ "~replays" { return serverReplayFilter; }
/ "~replay" { return replayFilter; }
/ "~src" ws+ s:StringLiteral { return source(s); } / "~src" ws+ s:StringLiteral { return source(s); }
/ "~s" { return responseFilter; } / "~s" { return responseFilter; }
/ "~t" ws+ s:StringLiteral { return contentType(s); }
/ "~tcp" { return tcpFilter; } / "~tcp" { return tcpFilter; }
/ "~tq" ws+ s:StringLiteral { return requestContentType(s); } / "~tq" ws+ s:StringLiteral { return requestContentType(s); }
/ "~ts" ws+ s:StringLiteral { return responseContentType(s); } / "~ts" ws+ s:StringLiteral { return responseContentType(s); }
/ "~t" ws+ s:StringLiteral { return contentType(s); }
/ "~u" ws+ s:StringLiteral { return url(s); } / "~u" ws+ s:StringLiteral { return url(s); }
/ "~websocket" { return websocketFilter; } / "~websocket" { return websocketFilter; }
/ s:StringLiteral { return url(s); } / s:StringLiteral { return url(s); }

View File

@ -8,6 +8,7 @@ interface _Flow {
type: string type: string
modified: boolean modified: boolean
marked: string marked: string
comment: string
client_conn: Client client_conn: Client
server_conn?: Server server_conn?: Server
error?: Error error?: Error

View File

@ -33,7 +33,8 @@ export function updateStoreFromUrl(store) {
query query
.split("&") .split("&")
.forEach((x) => { .forEach((x) => {
const [key, value] = x.split("=", 2) let [key, value] = x.split("=", 2)
value = decodeURIComponent(value)
switch (key) { switch (key) {
case Query.SEARCH: case Query.SEARCH:
store.dispatch(setFilter(value)) store.dispatch(setFilter(value))
@ -66,7 +67,7 @@ export function updateUrlFromStore(store) {
} }
const queryStr = Object.keys(query) const queryStr = Object.keys(query)
.filter(k => query[k]) .filter(k => query[k])
.map(k => `${k}=${query[k]}`) .map(k => `${k}=${encodeURIComponent(query[k])}`)
.join("&") .join("&")
let url let url