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
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):
l.add_option(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,10 +2,6 @@ import typing
from mitmproxy import flowfilter
def _match_all(flow) -> bool:
return True
def parse_spec(option: str) -> typing.Tuple[flowfilter.TFilter, str, str]:
"""
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)
if len(parts) == 2:
subject, replacement = parts
return _match_all, subject, replacement
return flowfilter.match_all, subject, replacement
elif len(parts) == 3:
patt, subject, replacement = parts
flow_filter = flowfilter.parse(patt)
if not flow_filter:
raise ValueError(f"Invalid filter pattern: {patt}")
return flow_filter, subject, replacement
else:
raise ValueError("Invalid number of parameters (2 or 3 are expected)")

View File

@ -43,8 +43,9 @@ class TestReadFile:
rf = readfile.ReadFile()
with taddons.context(rf) as tctx:
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
async def test_read(self, tmpdir, data, corrupt_data):

View File

@ -16,7 +16,7 @@ class TestStickyCookie:
def test_config(self):
sc = stickycookie.StickyCookie()
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="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, "@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, "~")
@ -608,7 +608,7 @@ def test_configure():
v = view.View()
with taddons.context(v) as tctx:
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_order="method")

View File

@ -13,10 +13,14 @@ class TestParsing:
assert c.getvalue()
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):
assert not flowfilter.parse("~b")
assert flowfilter.parse("~q")
assert flowfilter.parse("~c 10")
assert flowfilter.parse("~m foobar")
@ -568,6 +572,8 @@ class TestMatchingDummyFlow:
f = self.flow()
f.server_conn = tflow.tserver_conn()
assert self.q("~all", f)
assert not self.q("~a", f)
assert not self.q("~b whatever", f)

View File

@ -39,15 +39,18 @@ def console(monkeypatch):
return m
@pytest.mark.asyncio
def test_integration(tdata, console):
console.type(f":view.flows.load {tdata.path('mitmproxy/data/dumpfile-7.mitm')}<enter>")
console.type("<enter><tab><tab>")
console.type("<space><tab><tab>") # view second flow
@pytest.mark.asyncio
def test_options_home_end(console):
console.type("O<home><end>")
@pytest.mark.asyncio
def test_keybindings_home_end(console):
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.response.trailers = Headers(trailer="qvalue")
tf_http.comment = "I'm a comment!"
tf_tcp = tflow.ttcpflow(err=True)
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"):
parse_spec("/")
with pytest.raises(ValueError, match="Invalid filter pattern"):
with pytest.raises(ValueError, match="Invalid filter expression"):
parse_spec("/~b/one/two")

View File

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

View File

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

View File

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

View File

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

View File

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