Merge pull request #2120 from ujjwal96/replace-from-file

Replace from file through replacement editor #1458
This commit is contained in:
Maximilian Hils 2017-03-14 02:23:31 +01:00 committed by GitHub
commit 961747c4bc
7 changed files with 66 additions and 69 deletions

View File

@ -48,14 +48,14 @@ In practice, it's pretty common for the replacement literal to be long and
complex. For instance, it might be an XSS exploit that weighs in at hundreds or complex. For instance, it might be an XSS exploit that weighs in at hundreds or
thousands of characters. To cope with this, there's a variation of the thousands of characters. To cope with this, there's a variation of the
replacement hook specifier that lets you load the replacement text from a file. replacement hook specifier that lets you load the replacement text from a file.
So, you might start **mitmdump** as follows: To specify a file as replacement, prefix the file path with ``@``.
You might start **mitmdump** as follows:
>>> mitmdump --replace-from-file :~q:foo:~/xss-exploit >>> mitmdump --replacements :~q:foo:@~/xss-exploit
This will load the replacement text from the file ``~/xss-exploit``. This will load the replacement text from the file ``~/xss-exploit``.
Both the ``--replace`` and ``--replace-from-file`` flags can be passed multiple The ``--replacements`` flag can be passed multiple times.
times.
Interactively Interactively
@ -66,7 +66,6 @@ replacement hooks using a built-in editor. The context-sensitive help (:kbd:`?`)
complete usage information. complete usage information.
================== ======================= ================== =======================
command-line ``--replace``, command-line ``--replacements``
``--replace-from-file``
mitmproxy shortcut :kbd:`O` then :kbd:`R` mitmproxy shortcut :kbd:`O` then :kbd:`R`
================== ======================= ================== =======================

View File

@ -30,7 +30,6 @@ 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(),

View File

@ -1,3 +1,4 @@
import os
import re import re
from mitmproxy import exceptions from mitmproxy import exceptions
@ -42,7 +43,7 @@ def parse_hook(s):
return patt, a, b return patt, a, b
class _ReplaceBase: class Replace:
def __init__(self): def __init__(self):
self.lst = [] self.lst = []
@ -51,12 +52,12 @@ class _ReplaceBase:
.replacements is a list of tuples (fpat, rex, s): .replacements is a list of tuples (fpat, rex, s):
fpatt: a string specifying a filter pattern. fpatt: a string specifying a filter pattern.
rex: a regular expression, as bytes. rex: a regular expression, as string.
s: the replacement string, as bytes s: the replacement string
""" """
if self.optionName in updated: if "replacements" in updated:
lst = [] lst = []
for rep in getattr(options, self.optionName): for rep in options.replacements:
fpatt, rex, s = parse_hook(rep) fpatt, rex, s = parse_hook(rep)
flt = flowfilter.parse(fpatt) flt = flowfilter.parse(fpatt)
@ -65,11 +66,16 @@ class _ReplaceBase:
"Invalid filter pattern: %s" % fpatt "Invalid filter pattern: %s" % fpatt
) )
try: try:
# We should ideally escape here before trying to compile
re.compile(rex) re.compile(rex)
except re.error as e: except re.error as e:
raise exceptions.OptionsError( raise exceptions.OptionsError(
"Invalid regular expression: %s - %s" % (rex, str(e)) "Invalid regular expression: %s - %s" % (rex, str(e))
) )
if s.startswith("@") and not os.path.isfile(s[1:]):
raise exceptions.OptionsError(
"Invalid file path: {}".format(s[1:])
)
lst.append((rex, s, flt)) lst.append((rex, s, flt))
self.lst = lst self.lst = lst
@ -89,21 +95,13 @@ class _ReplaceBase:
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): def replace(self, obj, rex, s):
if s.startswith("@"):
s = os.path.expanduser(s[1:])
try:
with open(s, "rb") as f:
s = f.read()
except IOError:
ctx.log.warn("Could not read replacement file: %s" % s)
return
obj.replace(rex, s, flags=re.DOTALL) 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)

View File

@ -120,13 +120,6 @@ class Options(optmanager.OptManager):
the separator can be any character. the separator can be any character.
""" """
) )
self.add_option(
"replacement_files", Sequence[str], [],
"""
Replacement pattern, where the replacement clause is a path to a
file.
"""
)
self.add_option( self.add_option(
"server_replay_use_headers", Sequence[str], [], "server_replay_use_headers", Sequence[str], [],
"Request headers to be considered during replay." "Request headers to be considered during replay."

View File

@ -93,7 +93,6 @@ def common_options(parser, opts):
# Replacements # Replacements
group = parser.add_argument_group("Replacements") group = parser.add_argument_group("Replacements")
opts.make_parser(group, "replacements", metavar="PATTERN", short="R") opts.make_parser(group, "replacements", metavar="PATTERN", short="R")
opts.make_parser(group, "replacement_files", metavar="PATTERN")
# Set headers # Set headers
group = parser.add_argument_group("Set Headers") group = parser.add_argument_group("Set Headers")

View File

@ -1,5 +1,8 @@
import os
import re import re
import urwid import urwid
from mitmproxy import exceptions from mitmproxy import exceptions
from mitmproxy import flowfilter from mitmproxy import flowfilter
from mitmproxy.addons import script from mitmproxy.addons import script
@ -87,6 +90,9 @@ class ReplaceEditor(base.GridEditor):
re.compile(val) re.compile(val)
except re.error: except re.error:
return "Invalid regular expression." return "Invalid regular expression."
elif col == 2:
if val.startswith("@") and not os.path.isfile(os.path.expanduser(val[1:])):
return "Invalid file path"
return False return False

View File

@ -1,6 +1,5 @@
import pytest import pytest
from .. import tservers
from mitmproxy.addons import replace from mitmproxy.addons import replace
from mitmproxy.test import taddons from mitmproxy.test import taddons
from mitmproxy.test import tflow from mitmproxy.test import tflow
@ -32,7 +31,7 @@ class TestReplace:
with taddons.context() as tctx: with taddons.context() as tctx:
tctx.configure( tctx.configure(
r, r,
replacements = [ replacements=[
"/~q/foo/bar", "/~q/foo/bar",
"/~s/foo/bar", "/~s/foo/bar",
] ]
@ -47,53 +46,57 @@ class TestReplace:
r.response(f) r.response(f)
assert f.response.content == b"bar" assert f.response.content == b"bar"
class TestUpstreamProxy(tservers.HTTPUpstreamProxyTest):
ssl = False
def test_order(self): def test_order(self):
sa = replace.Replace() r = replace.Replace()
self.proxy.tmaster.addons.add(sa) with taddons.context() as tctx:
tctx.configure(
self.proxy.tmaster.options.replacements = [ r,
"/~q/foo/bar", replacements=[
"/~q/bar/baz", "/foo/bar",
"/~q/foo/oh noes!", "/bar/baz",
"/~s/baz/ORLY" "/foo/oh noes!",
] "/bar/oh noes!",
p = self.pathoc() ]
with p.connect(): )
req = p.request("get:'%s/p/418:b\"foo\"'" % self.server.urlbase) f = tflow.tflow()
assert req.content == b"ORLY" f.request.content = b"foo"
assert req.status_code == 418 r.request(f)
assert f.request.content == b"baz"
class TestReplaceFile: class TestReplaceFile:
def test_simple(self, tmpdir): def test_simple(self, tmpdir):
r = replace.ReplaceFile() r = replace.Replace()
rp = tmpdir.join("replacement")
rp.write("bar")
with taddons.context() as tctx: with taddons.context() as tctx:
tmpfile = tmpdir.join("replacement")
tmpfile.write("bar")
tctx.configure( tctx.configure(
r, r,
replacement_files = [ replacements=["/~q/foo/@" + str(tmpfile)]
"/~q/foo/" + str(rp),
"/~s/foo/" + str(rp),
"/~b nonexistent/nonexistent/nonexistent",
]
) )
f = tflow.tflow() f = tflow.tflow()
f.request.content = b"foo" f.request.content = b"foo"
r.request(f) r.request(f)
assert f.request.content == b"bar" assert f.request.content == b"bar"
f = tflow.tflow(resp=True) def test_nonexistent(self, tmpdir):
f.response.content = b"foo" r = replace.Replace()
r.response(f) with taddons.context() as tctx:
assert f.response.content == b"bar" with pytest.raises(Exception, match="Invalid file path"):
tctx.configure(
r,
replacements=["/~q/foo/@nonexistent"]
)
tmpfile = tmpdir.join("replacement")
tmpfile.write("bar")
tctx.configure(
r,
replacements=["/~q/foo/@" + str(tmpfile)]
)
tmpfile.remove()
f = tflow.tflow() f = tflow.tflow()
f.request.content = b"nonexistent" f.request.content = b"foo"
assert not tctx.master.event_log assert not tctx.master.event_log
r.request(f) r.request(f)
assert tctx.master.event_log assert tctx.master.event_log