mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-23 08:11:00 +00:00
Revamp replacement hooks
- Replacement specifiers can be either strings or tuples. This lets us cope gracefully with command-line parsing (and posible quick interactive specification) without having to special-case replacement hooks, or have knowledge of hook specification leak outside the addon. We can also now use the same command-line spec format in config files. - Split replacement and replacement from file into separate addons and options. Files are now read on each replacement, so you can edit replacement files in place without restart. - Modernise the test suite to use addon test helpers. TODO: editing and displaying replace-from-file in console app
This commit is contained in:
parent
02f51d043d
commit
4cc75a9560
@ -28,6 +28,7 @@ def default_addons():
|
|||||||
onboarding.Onboarding(),
|
onboarding.Onboarding(),
|
||||||
proxyauth.ProxyAuth(),
|
proxyauth.ProxyAuth(),
|
||||||
replace.Replace(),
|
replace.Replace(),
|
||||||
|
replace.ReplaceFile(),
|
||||||
script.ScriptLoader(),
|
script.ScriptLoader(),
|
||||||
serverplayback.ServerPlayback(),
|
serverplayback.ServerPlayback(),
|
||||||
setheaders.SetHeaders(),
|
setheaders.SetHeaders(),
|
||||||
|
@ -2,9 +2,47 @@ import re
|
|||||||
|
|
||||||
from mitmproxy import exceptions
|
from mitmproxy import exceptions
|
||||||
from mitmproxy import flowfilter
|
from mitmproxy import flowfilter
|
||||||
|
from mitmproxy import ctx
|
||||||
|
|
||||||
|
|
||||||
class Replace:
|
def parse_hook(s):
|
||||||
|
"""
|
||||||
|
Returns a (pattern, regex, replacement) tuple.
|
||||||
|
|
||||||
|
The general form for a replacement hook is as follows:
|
||||||
|
|
||||||
|
/patt/regex/replacement
|
||||||
|
|
||||||
|
The first character specifies the separator. Example:
|
||||||
|
|
||||||
|
:~q:foo:bar
|
||||||
|
|
||||||
|
If only two clauses are specified, the pattern is set to match
|
||||||
|
universally (i.e. ".*"). Example:
|
||||||
|
|
||||||
|
/foo/bar/
|
||||||
|
|
||||||
|
Clauses are parsed from left to right. Extra separators are taken to be
|
||||||
|
part of the final clause. For instance, the replacement clause below is
|
||||||
|
"foo/bar/":
|
||||||
|
|
||||||
|
/one/two/foo/bar/
|
||||||
|
"""
|
||||||
|
sep, rem = s[0], s[1:]
|
||||||
|
parts = rem.split(sep, 2)
|
||||||
|
if len(parts) == 2:
|
||||||
|
patt = ".*"
|
||||||
|
a, b = parts
|
||||||
|
elif len(parts) == 3:
|
||||||
|
patt, a, b = parts
|
||||||
|
else:
|
||||||
|
raise exceptions.OptionsError(
|
||||||
|
"Invalid replacement specifier: %s" % s
|
||||||
|
)
|
||||||
|
return patt, a, b
|
||||||
|
|
||||||
|
|
||||||
|
class _ReplaceBase:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.lst = []
|
self.lst = []
|
||||||
|
|
||||||
@ -16,9 +54,14 @@ class Replace:
|
|||||||
rex: a regular expression, as bytes.
|
rex: a regular expression, as bytes.
|
||||||
s: the replacement string, as bytes
|
s: the replacement string, as bytes
|
||||||
"""
|
"""
|
||||||
if "replacements" in updated:
|
if self.optionName in updated:
|
||||||
lst = []
|
lst = []
|
||||||
for fpatt, rex, s in options.replacements:
|
for rep in getattr(options, self.optionName):
|
||||||
|
if isinstance(rep, str):
|
||||||
|
fpatt, rex, s = parse_hook(rep)
|
||||||
|
else:
|
||||||
|
fpatt, rex, s = rep
|
||||||
|
|
||||||
flt = flowfilter.parse(fpatt)
|
flt = flowfilter.parse(fpatt)
|
||||||
if not flt:
|
if not flt:
|
||||||
raise exceptions.OptionsError(
|
raise exceptions.OptionsError(
|
||||||
@ -37,9 +80,9 @@ class Replace:
|
|||||||
for rex, s, flt in self.lst:
|
for rex, s, flt in self.lst:
|
||||||
if flt(f):
|
if flt(f):
|
||||||
if f.response:
|
if f.response:
|
||||||
f.response.replace(rex, s, flags=re.DOTALL)
|
self.replace(f.response, rex, s)
|
||||||
else:
|
else:
|
||||||
f.request.replace(rex, s, flags=re.DOTALL)
|
self.replace(f.request, rex, s)
|
||||||
|
|
||||||
def request(self, flow):
|
def request(self, flow):
|
||||||
if not flow.reply.has_message:
|
if not flow.reply.has_message:
|
||||||
@ -48,3 +91,22 @@ class Replace:
|
|||||||
def response(self, flow):
|
def response(self, flow):
|
||||||
if not flow.reply.has_message:
|
if not flow.reply.has_message:
|
||||||
self.execute(flow)
|
self.execute(flow)
|
||||||
|
|
||||||
|
|
||||||
|
class Replace(_ReplaceBase):
|
||||||
|
optionName = "replacements"
|
||||||
|
|
||||||
|
def replace(self, obj, rex, s):
|
||||||
|
obj.replace(rex, s, flags=re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
|
class ReplaceFile(_ReplaceBase):
|
||||||
|
optionName = "replacement_files"
|
||||||
|
|
||||||
|
def replace(self, obj, rex, s):
|
||||||
|
try:
|
||||||
|
v = open(s, "rb").read()
|
||||||
|
except IOError as e:
|
||||||
|
ctx.log.warn("Could not read replacement file: %s" % s)
|
||||||
|
return
|
||||||
|
obj.replace(rex, v, flags=re.DOTALL)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import Tuple, Optional, Sequence
|
from typing import Tuple, Optional, Sequence, Union
|
||||||
|
|
||||||
from mitmproxy import optmanager
|
from mitmproxy import optmanager
|
||||||
|
|
||||||
@ -38,7 +38,8 @@ class Options(optmanager.OptManager):
|
|||||||
rfile: Optional[str] = None,
|
rfile: Optional[str] = None,
|
||||||
scripts: Sequence[str] = [],
|
scripts: Sequence[str] = [],
|
||||||
showhost: bool = False,
|
showhost: bool = False,
|
||||||
replacements: Sequence[Tuple[str, str, str]] = [],
|
replacements: Sequence[Union[Tuple[str, str, str], str]] = [],
|
||||||
|
replacement_files: Sequence[Union[Tuple[str, str, str], str]] = [],
|
||||||
server_replay_use_headers: Sequence[str] = [],
|
server_replay_use_headers: Sequence[str] = [],
|
||||||
setheaders: Sequence[Tuple[str, str, str]] = [],
|
setheaders: Sequence[Tuple[str, str, str]] = [],
|
||||||
server_replay: Sequence[str] = [],
|
server_replay: Sequence[str] = [],
|
||||||
@ -124,6 +125,7 @@ class Options(optmanager.OptManager):
|
|||||||
self.scripts = scripts
|
self.scripts = scripts
|
||||||
self.showhost = showhost
|
self.showhost = showhost
|
||||||
self.replacements = replacements
|
self.replacements = replacements
|
||||||
|
self.replacement_files = replacement_files
|
||||||
self.server_replay_use_headers = server_replay_use_headers
|
self.server_replay_use_headers = server_replay_use_headers
|
||||||
self.setheaders = setheaders
|
self.setheaders = setheaders
|
||||||
self.server_replay = server_replay
|
self.server_replay = server_replay
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import re
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from mitmproxy import exceptions
|
from mitmproxy import exceptions
|
||||||
@ -41,40 +40,6 @@ def _parse_hook(s):
|
|||||||
return patt, a, b
|
return patt, a, b
|
||||||
|
|
||||||
|
|
||||||
def parse_replace_hook(s):
|
|
||||||
"""
|
|
||||||
Returns a (pattern, regex, replacement) tuple.
|
|
||||||
|
|
||||||
The general form for a replacement hook is as follows:
|
|
||||||
|
|
||||||
/patt/regex/replacement
|
|
||||||
|
|
||||||
The first character specifies the separator. Example:
|
|
||||||
|
|
||||||
:~q:foo:bar
|
|
||||||
|
|
||||||
If only two clauses are specified, the pattern is set to match
|
|
||||||
universally (i.e. ".*"). Example:
|
|
||||||
|
|
||||||
/foo/bar/
|
|
||||||
|
|
||||||
Clauses are parsed from left to right. Extra separators are taken to be
|
|
||||||
part of the final clause. For instance, the replacement clause below is
|
|
||||||
"foo/bar/":
|
|
||||||
|
|
||||||
/one/two/foo/bar/
|
|
||||||
|
|
||||||
Checks that pattern and regex are both well-formed. Raises
|
|
||||||
ParseException on error.
|
|
||||||
"""
|
|
||||||
patt, regex, replacement = _parse_hook(s)
|
|
||||||
try:
|
|
||||||
re.compile(regex)
|
|
||||||
except re.error as e:
|
|
||||||
raise ParseException("Malformed replacement regex: %s" % str(e))
|
|
||||||
return patt, regex, replacement
|
|
||||||
|
|
||||||
|
|
||||||
def parse_setheader(s):
|
def parse_setheader(s):
|
||||||
"""
|
"""
|
||||||
Returns a (pattern, header, value) tuple.
|
Returns a (pattern, header, value) tuple.
|
||||||
@ -116,26 +81,6 @@ def get_common_options(args):
|
|||||||
if stream_large_bodies:
|
if stream_large_bodies:
|
||||||
stream_large_bodies = human.parse_size(stream_large_bodies)
|
stream_large_bodies = human.parse_size(stream_large_bodies)
|
||||||
|
|
||||||
reps = []
|
|
||||||
for i in args.replace or []:
|
|
||||||
try:
|
|
||||||
p = parse_replace_hook(i)
|
|
||||||
except ParseException as e:
|
|
||||||
raise exceptions.OptionsError(e)
|
|
||||||
reps.append(p)
|
|
||||||
for i in args.replace_file or []:
|
|
||||||
try:
|
|
||||||
patt, rex, path = parse_replace_hook(i)
|
|
||||||
except ParseException as e:
|
|
||||||
raise exceptions.OptionsError(e)
|
|
||||||
try:
|
|
||||||
v = open(path, "rb").read()
|
|
||||||
except IOError as e:
|
|
||||||
raise exceptions.OptionsError(
|
|
||||||
"Could not read replace file: %s" % path
|
|
||||||
)
|
|
||||||
reps.append((patt, rex, v))
|
|
||||||
|
|
||||||
setheaders = []
|
setheaders = []
|
||||||
for i in args.setheader or []:
|
for i in args.setheader or []:
|
||||||
try:
|
try:
|
||||||
@ -224,7 +169,8 @@ def get_common_options(args):
|
|||||||
refresh_server_playback=not args.norefresh,
|
refresh_server_playback=not args.norefresh,
|
||||||
server_replay_use_headers=args.server_replay_use_headers,
|
server_replay_use_headers=args.server_replay_use_headers,
|
||||||
rfile=args.rfile,
|
rfile=args.rfile,
|
||||||
replacements=reps,
|
replacements=args.replacements,
|
||||||
|
replacement_files=args.replacement_files,
|
||||||
setheaders=setheaders,
|
setheaders=setheaders,
|
||||||
server_replay=args.server_replay,
|
server_replay=args.server_replay,
|
||||||
scripts=args.scripts,
|
scripts=args.scripts,
|
||||||
@ -676,13 +622,13 @@ def replacements(parser):
|
|||||||
)
|
)
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--replace",
|
"--replace",
|
||||||
action="append", type=str, dest="replace",
|
action="append", type=str, dest="replacements",
|
||||||
metavar="PATTERN",
|
metavar="PATTERN",
|
||||||
help="Replacement pattern."
|
help="Replacement pattern."
|
||||||
)
|
)
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--replace-from-file",
|
"--replace-from-file",
|
||||||
action="append", type=str, dest="replace_file",
|
action="append", type=str, dest="replacement_files",
|
||||||
metavar="PATH",
|
metavar="PATH",
|
||||||
help="""
|
help="""
|
||||||
Replacement pattern, where the replacement clause is a path to a
|
Replacement pattern, where the replacement clause is a path to a
|
||||||
|
@ -6,6 +6,8 @@ from mitmproxy.tools.console import grideditor
|
|||||||
from mitmproxy.tools.console import select
|
from mitmproxy.tools.console import select
|
||||||
from mitmproxy.tools.console import signals
|
from mitmproxy.tools.console import signals
|
||||||
|
|
||||||
|
from mitmproxy.addons import replace
|
||||||
|
|
||||||
footer = [
|
footer = [
|
||||||
('heading_key', "enter/space"), ":toggle ",
|
('heading_key', "enter/space"), ":toggle ",
|
||||||
('heading_key', "C"), ":clear all ",
|
('heading_key', "C"), ":clear all ",
|
||||||
@ -215,10 +217,16 @@ class Options(urwid.WidgetWrap):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def replacepatterns(self):
|
def replacepatterns(self):
|
||||||
|
data = []
|
||||||
|
for d in self.master.options.replacements:
|
||||||
|
if isinstance(d, str):
|
||||||
|
data.append(replace.parse_hook(d))
|
||||||
|
else:
|
||||||
|
data.append(d)
|
||||||
self.master.view_grideditor(
|
self.master.view_grideditor(
|
||||||
grideditor.ReplaceEditor(
|
grideditor.ReplaceEditor(
|
||||||
self.master,
|
self.master,
|
||||||
self.master.options.replacements,
|
data,
|
||||||
self.master.options.setter("replacements")
|
self.master.options.setter("replacements")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -1,56 +1,58 @@
|
|||||||
|
import os.path
|
||||||
from mitmproxy.test import tflow
|
from mitmproxy.test import tflow
|
||||||
from mitmproxy.test import tutils
|
from mitmproxy.test import tutils
|
||||||
|
|
||||||
from .. import tservers
|
from .. import tservers
|
||||||
from mitmproxy.addons import replace
|
from mitmproxy.addons import replace
|
||||||
from mitmproxy import master
|
from mitmproxy.test import taddons
|
||||||
from mitmproxy import options
|
|
||||||
from mitmproxy import proxy
|
|
||||||
|
|
||||||
|
|
||||||
class TestReplace:
|
class TestReplace:
|
||||||
|
def test_parse_hook(self):
|
||||||
|
x = replace.parse_hook("/foo/bar/voing")
|
||||||
|
assert x == ("foo", "bar", "voing")
|
||||||
|
x = replace.parse_hook("/foo/bar/vo/ing/")
|
||||||
|
assert x == ("foo", "bar", "vo/ing/")
|
||||||
|
x = replace.parse_hook("/bar/voing")
|
||||||
|
assert x == (".*", "bar", "voing")
|
||||||
|
tutils.raises("invalid replacement", replace.parse_hook, "/")
|
||||||
|
|
||||||
def test_configure(self):
|
def test_configure(self):
|
||||||
r = replace.Replace()
|
r = replace.Replace()
|
||||||
updated = set(["replacements"])
|
with taddons.context() as tctx:
|
||||||
r.configure(options.Options(
|
tctx.configure(r, replacements=[("one", "two", "three")])
|
||||||
replacements=[("one", "two", "three")]
|
|
||||||
), updated)
|
|
||||||
tutils.raises(
|
tutils.raises(
|
||||||
"invalid filter pattern",
|
"invalid filter pattern",
|
||||||
r.configure,
|
tctx.configure,
|
||||||
options.Options(
|
r,
|
||||||
replacements=[("~b", "two", "three")]
|
replacements=[("~b", "two", "three")]
|
||||||
),
|
|
||||||
updated
|
|
||||||
)
|
)
|
||||||
tutils.raises(
|
tutils.raises(
|
||||||
"invalid regular expression",
|
"invalid regular expression",
|
||||||
r.configure,
|
tctx.configure,
|
||||||
options.Options(
|
r,
|
||||||
replacements=[("foo", "+", "three")]
|
replacements=[("foo", "+", "three")]
|
||||||
),
|
|
||||||
updated
|
|
||||||
)
|
)
|
||||||
|
tctx.configure(r, replacements=["/a/b/c/"])
|
||||||
|
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
o = options.Options(
|
r = replace.Replace()
|
||||||
|
with taddons.context() as tctx:
|
||||||
|
tctx.configure(
|
||||||
|
r,
|
||||||
replacements = [
|
replacements = [
|
||||||
("~q", "foo", "bar"),
|
("~q", "foo", "bar"),
|
||||||
("~s", "foo", "bar"),
|
("~s", "foo", "bar"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
m = master.Master(o, proxy.DummyServer())
|
|
||||||
sa = replace.Replace()
|
|
||||||
m.addons.add(sa)
|
|
||||||
|
|
||||||
f = tflow.tflow()
|
f = tflow.tflow()
|
||||||
f.request.content = b"foo"
|
f.request.content = b"foo"
|
||||||
m.request(f)
|
r.request(f)
|
||||||
assert f.request.content == b"bar"
|
assert f.request.content == b"bar"
|
||||||
|
|
||||||
f = tflow.tflow(resp=True)
|
f = tflow.tflow(resp=True)
|
||||||
f.response.content = b"foo"
|
f.response.content = b"foo"
|
||||||
m.response(f)
|
r.response(f)
|
||||||
assert f.response.content == b"bar"
|
assert f.response.content == b"bar"
|
||||||
|
|
||||||
|
|
||||||
@ -72,3 +74,36 @@ class TestUpstreamProxy(tservers.HTTPUpstreamProxyTest):
|
|||||||
req = p.request("get:'%s/p/418:b\"foo\"'" % self.server.urlbase)
|
req = p.request("get:'%s/p/418:b\"foo\"'" % self.server.urlbase)
|
||||||
assert req.content == b"ORLY"
|
assert req.content == b"ORLY"
|
||||||
assert req.status_code == 418
|
assert req.status_code == 418
|
||||||
|
|
||||||
|
|
||||||
|
class TestReplaceFile:
|
||||||
|
def test_simple(self):
|
||||||
|
r = replace.ReplaceFile()
|
||||||
|
with tutils.tmpdir() as td:
|
||||||
|
rp = os.path.join(td, "replacement")
|
||||||
|
with open(rp, "w") as f:
|
||||||
|
f.write("bar")
|
||||||
|
with taddons.context() as tctx:
|
||||||
|
tctx.configure(
|
||||||
|
r,
|
||||||
|
replacement_files = [
|
||||||
|
("~q", "foo", rp),
|
||||||
|
("~s", "foo", rp),
|
||||||
|
("~b nonexistent", "nonexistent", "nonexistent"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
f = tflow.tflow()
|
||||||
|
f.request.content = b"foo"
|
||||||
|
r.request(f)
|
||||||
|
assert f.request.content == b"bar"
|
||||||
|
|
||||||
|
f = tflow.tflow(resp=True)
|
||||||
|
f.response.content = b"foo"
|
||||||
|
r.response(f)
|
||||||
|
assert f.response.content == b"bar"
|
||||||
|
|
||||||
|
f = tflow.tflow()
|
||||||
|
f.request.content = b"nonexistent"
|
||||||
|
assert not tctx.master.event_log
|
||||||
|
r.request(f)
|
||||||
|
assert tctx.master.event_log
|
||||||
|
@ -3,38 +3,6 @@ from mitmproxy.tools import cmdline
|
|||||||
from mitmproxy.test import tutils
|
from mitmproxy.test import tutils
|
||||||
|
|
||||||
|
|
||||||
def test_parse_replace_hook():
|
|
||||||
x = cmdline.parse_replace_hook("/foo/bar/voing")
|
|
||||||
assert x == ("foo", "bar", "voing")
|
|
||||||
|
|
||||||
x = cmdline.parse_replace_hook("/foo/bar/vo/ing/")
|
|
||||||
assert x == ("foo", "bar", "vo/ing/")
|
|
||||||
|
|
||||||
x = cmdline.parse_replace_hook("/bar/voing")
|
|
||||||
assert x == (".*", "bar", "voing")
|
|
||||||
|
|
||||||
tutils.raises(
|
|
||||||
cmdline.ParseException,
|
|
||||||
cmdline.parse_replace_hook,
|
|
||||||
"/foo"
|
|
||||||
)
|
|
||||||
tutils.raises(
|
|
||||||
"replacement regex",
|
|
||||||
cmdline.parse_replace_hook,
|
|
||||||
"patt/[/rep"
|
|
||||||
)
|
|
||||||
tutils.raises(
|
|
||||||
"filter pattern",
|
|
||||||
cmdline.parse_replace_hook,
|
|
||||||
"/~/foo/rep"
|
|
||||||
)
|
|
||||||
tutils.raises(
|
|
||||||
"empty clause",
|
|
||||||
cmdline.parse_replace_hook,
|
|
||||||
"//"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_setheaders():
|
def test_parse_setheaders():
|
||||||
x = cmdline.parse_setheader("/foo/bar/voing")
|
x = cmdline.parse_setheader("/foo/bar/voing")
|
||||||
assert x == ("foo", "bar", "voing")
|
assert x == ("foo", "bar", "voing")
|
||||||
@ -65,38 +33,6 @@ def test_common():
|
|||||||
)
|
)
|
||||||
opts.setheader = []
|
opts.setheader = []
|
||||||
|
|
||||||
opts.replace = ["/foo/bar/voing"]
|
|
||||||
v = cmdline.get_common_options(opts)
|
|
||||||
assert v["replacements"] == [("foo", "bar", "voing")]
|
|
||||||
|
|
||||||
opts.replace = ["//"]
|
|
||||||
tutils.raises(
|
|
||||||
"empty clause",
|
|
||||||
cmdline.get_common_options,
|
|
||||||
opts
|
|
||||||
)
|
|
||||||
|
|
||||||
opts.replace = []
|
|
||||||
opts.replace_file = [("/foo/bar/nonexistent")]
|
|
||||||
tutils.raises(
|
|
||||||
"could not read replace file",
|
|
||||||
cmdline.get_common_options,
|
|
||||||
opts
|
|
||||||
)
|
|
||||||
|
|
||||||
opts.replace_file = [("/~/bar/nonexistent")]
|
|
||||||
tutils.raises(
|
|
||||||
"filter pattern",
|
|
||||||
cmdline.get_common_options,
|
|
||||||
opts
|
|
||||||
)
|
|
||||||
|
|
||||||
p = tutils.test_data.path("mitmproxy/data/replace")
|
|
||||||
opts.replace_file = [("/foo/bar/%s" % p)]
|
|
||||||
v = cmdline.get_common_options(opts)["replacements"]
|
|
||||||
assert len(v) == 1
|
|
||||||
assert v[0][2].strip() == b"replacecontents"
|
|
||||||
|
|
||||||
|
|
||||||
def test_mitmproxy():
|
def test_mitmproxy():
|
||||||
ap = cmdline.mitmproxy()
|
ap = cmdline.mitmproxy()
|
||||||
|
Loading…
Reference in New Issue
Block a user