Enable custom options for addons

- Add an options parameter to the start() event. This is to be used by addons
on startup to add custom options.
- Add a running() event that is called once the proxy is up and running.
- With the new paradigm we can't log during master __init__, so add a tiny
termstatus addon to print proxy status to terminal once we're running.
This commit is contained in:
Aldo Cortesi 2017-03-09 13:52:58 +13:00 committed by Aldo Cortesi
parent ee65894d40
commit 0c6663d0d5
32 changed files with 170 additions and 87 deletions

View File

@ -1,11 +1,12 @@
""" """
This script makes it possible to use mitmproxy in scenarios where IP spoofing has been used to redirect This script makes it possible to use mitmproxy in scenarios where IP spoofing
connections to mitmproxy. The way this works is that we rely on either the TLS Server Name Indication (SNI) or the has been used to redirect connections to mitmproxy. The way this works is that
Host header of the HTTP request. we rely on either the TLS Server Name Indication (SNI) or the Host header of the
Of course, this is not foolproof - if an HTTPS connection comes without SNI, we don't HTTP request. Of course, this is not foolproof - if an HTTPS connection comes
know the actual target and cannot construct a certificate that looks valid. without SNI, we don't know the actual target and cannot construct a certificate
Similarly, if there's no Host header or a spoofed Host header, we're out of luck as well. that looks valid. Similarly, if there's no Host header or a spoofed Host header,
Using transparent mode is the better option most of the time. we're out of luck as well. Using transparent mode is the better option most of
the time.
Usage: Usage:
mitmproxy mitmproxy
@ -53,5 +54,5 @@ class Rerouter:
flow.request.port = port flow.request.port = port
def start(): def start(opts):
return Rerouter() return Rerouter()

View File

@ -25,7 +25,7 @@ HAR = {}
SERVERS_SEEN = set() SERVERS_SEEN = set()
def start(): def start(opts):
""" """
Called once on script startup before any other events. Called once on script startup before any other events.
""" """

View File

@ -14,6 +14,6 @@ Usage:
""" """
def start(): def start(opts):
import pydevd import pydevd
pydevd.settrace("localhost", port=5678, stdoutToServer=True, stderrToServer=True) pydevd.settrace("localhost", port=5678, stdoutToServer=True, stderrToServer=True)

View File

@ -112,7 +112,7 @@ class TlsFeedback(TlsLayer):
tls_strategy = None tls_strategy = None
def start(): def start(opts):
global tls_strategy global tls_strategy
if len(sys.argv) == 2: if len(sys.argv) == 2:
tls_strategy = ProbabilisticStrategy(float(sys.argv[1])) tls_strategy = ProbabilisticStrategy(float(sys.argv[1]))

View File

@ -3,5 +3,5 @@ class AddHeader:
flow.response.headers["newheader"] = "foo" flow.response.headers["newheader"] = "foo"
def start(): def start(opts):
return AddHeader() return AddHeader()

View File

@ -20,7 +20,7 @@ class ViewSwapCase(contentviews.View):
view = ViewSwapCase() view = ViewSwapCase()
def start(): def start(opts):
contentviews.add(view) contentviews.add(view)

View File

@ -0,0 +1,10 @@
from mitmproxy import ctx
def start(options):
ctx.log.info("Registering option 'custom'")
options.add_option("custom", str, "default", "A custom option")
def configure(options, updated):
ctx.log.info("custom option value: %s" % options.custom)

View File

@ -17,7 +17,7 @@ class Filter:
print(flow) print(flow)
def start(): def start(opts):
if len(sys.argv) != 2: if len(sys.argv) != 2:
raise ValueError("Usage: -s 'filt.py FILTER'") raise ValueError("Usage: -s 'filt.py FILTER'")
return Filter(sys.argv[1]) return Filter(sys.argv[1])

View File

@ -23,7 +23,7 @@ class Writer:
self.w.add(flow) self.w.add(flow)
def start(): def start(opts):
if len(sys.argv) != 2: if len(sys.argv) != 2:
raise ValueError('Usage: -s "flowriter.py filename"') raise ValueError('Usage: -s "flowriter.py filename"')
return Writer(sys.argv[1]) return Writer(sys.argv[1])

View File

@ -7,6 +7,6 @@ If you want to help us out: https://github.com/mitmproxy/mitmproxy/issues/1530 :
from mitmproxy import ctx from mitmproxy import ctx
def start(): def start(opts):
ctx.log.info("This is some informative text.") ctx.log.info("This is some informative text.")
ctx.log.error("This is an error.") ctx.log.error("This is an error.")

View File

@ -23,7 +23,7 @@ class Injector:
flow.response.content = str(html).encode("utf8") flow.response.content = str(html).encode("utf8")
def start(): def start(opts):
if len(sys.argv) != 2: if len(sys.argv) != 2:
raise ValueError('Usage: -s "iframe_injector.py url"') raise ValueError('Usage: -s "iframe_injector.py url"')
return Injector(sys.argv[1]) return Injector(sys.argv[1])

View File

@ -9,7 +9,7 @@ class Replacer:
flow.response.replace(self.src, self.dst) flow.response.replace(self.src, self.dst)
def start(): def start(opts):
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("src", type=str) parser.add_argument("src", type=str)
parser.add_argument("dst", type=str) parser.add_argument("dst", type=str)

View File

@ -14,7 +14,7 @@ def hello_world():
return 'Hello World!' return 'Hello World!'
def start(): def start(opts):
# Host app at the magic domain "proxapp" on port 80. Requests to this # Host app at the magic domain "proxapp" on port 80. Requests to this
# domain and port combination will now be routed to the WSGI app instance. # domain and port combination will now be routed to the WSGI app instance.
return wsgiapp.WSGIApp(app, "proxapp", 80) return wsgiapp.WSGIApp(app, "proxapp", 80)

View File

@ -1,4 +1,5 @@
from mitmproxy import exceptions from mitmproxy import exceptions
from mitmproxy import eventsequence
import pprint import pprint
@ -10,7 +11,7 @@ class AddonManager:
def __init__(self, master): def __init__(self, master):
self.chain = [] self.chain = []
self.master = master self.master = master
master.options.changed.connect(self._options_update) master.options.changed.connect(self.configure_all)
def clear(self): def clear(self):
""" """
@ -29,22 +30,14 @@ class AddonManager:
if name == _get_name(i): if name == _get_name(i):
return i return i
def _options_update(self, options, updated): def configure_all(self, options, updated):
for i in self.chain: self.invoke_all_with_context("configure", options, updated)
with self.master.handlecontext():
self.invoke_with_context(i, "configure", options, updated)
def startup(self, s): def startup(self, s):
""" """
Run startup events on addon. Run startup events on addon.
""" """
self.invoke_with_context(s, "start") self.invoke_with_context(s, "start", self.master.options)
self.invoke_with_context(
s,
"configure",
self.master.options,
self.master.options.keys()
)
def add(self, *addons): def add(self, *addons):
""" """
@ -62,8 +55,7 @@ class AddonManager:
self.invoke_with_context(addon, "done") self.invoke_with_context(addon, "done")
def done(self): def done(self):
for i in self.chain: self.invoke_all_with_context("done")
self.invoke_with_context(i, "done")
def __len__(self): def __len__(self):
return len(self.chain) return len(self.chain)
@ -75,7 +67,14 @@ class AddonManager:
with self.master.handlecontext(): with self.master.handlecontext():
self.invoke(addon, name, *args, **kwargs) self.invoke(addon, name, *args, **kwargs)
def invoke_all_with_context(self, name, *args, **kwargs):
with self.master.handlecontext():
for i in self.chain:
self.invoke(i, name, *args, **kwargs)
def invoke(self, addon, name, *args, **kwargs): def invoke(self, addon, name, *args, **kwargs):
if name not in eventsequence.Events: # prama: no cover
raise NotImplementedError("Unknown event")
func = getattr(addon, name, None) func = getattr(addon, name, None)
if func: if func:
if not callable(func): if not callable(func):

View File

@ -170,22 +170,23 @@ class Script:
def load_script(self): def load_script(self):
self.ns = load_script(self.path, self.args) self.ns = load_script(self.path, self.args)
ret = self.run("start") ret = self.run("start", self.last_options)
if ret: if ret:
self.ns = ret self.ns = ret
self.run("start") self.run("start", self.last_options)
def tick(self): def tick(self):
if self.should_reload.is_set(): if self.should_reload.is_set():
self.should_reload.clear() self.should_reload.clear()
ctx.log.info("Reloading script: %s" % self.name) ctx.log.info("Reloading script: %s" % self.name)
self.ns = load_script(self.path, self.args) self.ns = load_script(self.path, self.args)
self.start() self.start(self.last_options)
self.configure(self.last_options, self.last_options.keys()) self.configure(self.last_options, self.last_options.keys())
else: else:
self.run("tick") self.run("tick")
def start(self): def start(self, opts):
self.last_options = opts
self.load_script() self.load_script()
def configure(self, options, updated): def configure(self, options, updated):
@ -209,6 +210,12 @@ class ScriptLoader:
""" """
An addon that manages loading scripts from options. An addon that manages loading scripts from options.
""" """
def __init__(self):
self.is_running = False
def running(self):
self.is_running = True
def run_once(self, command, flows): def run_once(self, command, flows):
try: try:
sc = Script(command) sc = Script(command)
@ -267,3 +274,10 @@ class ScriptLoader:
for s in newscripts: for s in newscripts:
ctx.master.addons.startup(s) ctx.master.addons.startup(s)
if self.is_running:
# If we're already running, we configure and tell the addon
# we're up and running.
ctx.master.addons.invoke_with_context(
s, "configure", options, options.keys()
)
ctx.master.addons.invoke_with_context(s, "running")

View File

@ -0,0 +1,23 @@
from mitmproxy import ctx
"""
A tiny addon to print the proxy status to terminal. Eventually this could
also print some stats on exit.
"""
class TermStatus:
def __init__(self):
self.server = False
def configure(self, options, updated):
if "server" in updated:
self.server = options.server
def running(self):
if self.server:
ctx.log.info(
"Proxy server listening at http://{}:{}".format(
*ctx.master.server.address,
)
)

View File

@ -33,6 +33,7 @@ Events = frozenset([
"done", "done",
"log", "log",
"start", "start",
"running",
"tick", "tick",
]) ])

View File

@ -76,12 +76,16 @@ class Master:
def run(self): def run(self):
self.start() self.start()
running = False
try: try:
while not self.should_exit.is_set(): while not self.should_exit.is_set():
# Don't choose a very small timeout in Python 2: # Don't choose a very small timeout in Python 2:
# https://github.com/mitmproxy/mitmproxy/issues/443 # https://github.com/mitmproxy/mitmproxy/issues/443
# TODO: Lower the timeout value if we move to Python 3. # TODO: Lower the timeout value if we move to Python 3.
self.tick(0.1) self.tick(0.1)
if not running:
running = True
self.addons.invoke_all_with_context("running")
finally: finally:
self.shutdown() self.shutdown()

View File

@ -324,7 +324,15 @@ class OptManager:
options=options options=options
) )
def set(self, spec): def set(self, *spec):
vals = {}
for i in spec:
vals.update(self._setspec(i))
self.update(**vals)
def _setspec(self, spec):
d = {}
parts = spec.split("=", maxsplit=1) parts = spec.split("=", maxsplit=1)
if len(parts) == 1: if len(parts) == 1:
optname, optval = parts[0], None optname, optval = parts[0], None
@ -333,14 +341,14 @@ class OptManager:
o = self._options[optname] o = self._options[optname]
if o.typespec in (str, typing.Optional[str]): if o.typespec in (str, typing.Optional[str]):
setattr(self, optname, optval) d[optname] = optval
elif o.typespec in (int, typing.Optional[int]): elif o.typespec in (int, typing.Optional[int]):
if optval: if optval:
try: try:
optval = int(optval) optval = int(optval)
except ValueError: except ValueError:
raise exceptions.OptionsError("Not an integer: %s" % optval) raise exceptions.OptionsError("Not an integer: %s" % optval)
setattr(self, optname, optval) d[optname] = optval
elif o.typespec == bool: elif o.typespec == bool:
if not optval or optval == "true": if not optval or optval == "true":
v = True v = True
@ -350,18 +358,15 @@ class OptManager:
raise exceptions.OptionsError( raise exceptions.OptionsError(
"Boolean must be \"true\", \"false\", or have the value " "omitted (a synonym for \"true\")." "Boolean must be \"true\", \"false\", or have the value " "omitted (a synonym for \"true\")."
) )
setattr(self, optname, v) d[optname] = v
elif o.typespec == typing.Sequence[str]: elif o.typespec == typing.Sequence[str]:
if not optval: if not optval:
setattr(self, optname, []) d[optname] = []
else: else:
setattr( d[optname] = getattr(self, optname) + [optval]
self,
optname,
getattr(self, optname) + [optval]
)
else: # pragma: no cover else: # pragma: no cover
raise NotImplementedError("Unsupported option type: %s", o.typespec) raise NotImplementedError("Unsupported option type: %s", o.typespec)
return d
def make_parser(self, parser, optname, metavar=None, short=None): def make_parser(self, parser, optname, metavar=None, short=None):
o = self._options[optname] o = self._options[optname]

View File

@ -3,7 +3,7 @@ from mitmproxy import exceptions
from mitmproxy import addons from mitmproxy import addons
from mitmproxy import options from mitmproxy import options
from mitmproxy import master from mitmproxy import master
from mitmproxy.addons import dumper, termlog from mitmproxy.addons import dumper, termlog, termstatus
class DumpMaster(master.Master): class DumpMaster(master.Master):
@ -18,17 +18,11 @@ class DumpMaster(master.Master):
master.Master.__init__(self, options, server) master.Master.__init__(self, options, server)
self.has_errored = False self.has_errored = False
if with_termlog: if with_termlog:
self.addons.add(termlog.TermLog()) self.addons.add(termlog.TermLog(), termstatus.TermStatus())
self.addons.add(*addons.default_addons()) self.addons.add(*addons.default_addons())
if with_dumper: if with_dumper:
self.addons.add(dumper.Dumper()) self.addons.add(dumper.Dumper())
if self.options.server:
self.add_log(
"Proxy server listening at http://{}:{}".format(server.address[0], server.address[1]),
"info"
)
if options.rfile: if options.rfile:
try: try:
self.load_flows_file(options.rfile) self.load_flows_file(options.rfile)

View File

@ -45,9 +45,6 @@ def process_options(parser, opts, args):
if args.quiet: if args.quiet:
args.flow_detail = 0 args.flow_detail = 0
for i in args.setoptions:
opts.set(i)
adict = {} adict = {}
for n in dir(args): for n in dir(args):
if n in opts: if n in opts:
@ -77,6 +74,8 @@ def run(MasterKlass, args): # pragma: no cover
opts.load_paths(args.conf) opts.load_paths(args.conf)
server = process_options(parser, opts, args) server = process_options(parser, opts, args)
master = MasterKlass(opts, server) master = MasterKlass(opts, server)
master.addons.configure_all(opts, opts.keys())
opts.set(*args.setoptions)
def cleankill(*args, **kwargs): def cleankill(*args, **kwargs):
master.shutdown() master.shutdown()

View File

@ -116,9 +116,7 @@ class TestScript:
) )
) )
sc.load_script() sc.load_script()
assert sc.ns.call_log == [ assert sc.ns.call_log[0][0:2] == ("solo", "start")
("solo", "start", (), {}),
]
sc.ns.call_log = [] sc.ns.call_log = []
f = tflow.tflow(resp=True) f = tflow.tflow(resp=True)
@ -146,7 +144,7 @@ class TestScript:
sc = script.Script( sc = script.Script(
tutils.test_data.path("mitmproxy/data/addonscripts/error.py") tutils.test_data.path("mitmproxy/data/addonscripts/error.py")
) )
sc.start() sc.start(tctx.options)
f = tflow.tflow(resp=True) f = tflow.tflow(resp=True)
sc.request(f) sc.request(f)
assert tctx.master.event_log[0][0] == "error" assert tctx.master.event_log[0][0] == "error"
@ -162,7 +160,7 @@ class TestScript:
"mitmproxy/data/addonscripts/addon.py" "mitmproxy/data/addonscripts/addon.py"
) )
) )
sc.start() sc.start(tctx.options)
tctx.configure(sc) tctx.configure(sc)
assert sc.ns.event_log == [ assert sc.ns.event_log == [
'scriptstart', 'addonstart', 'addonconfigure' 'scriptstart', 'addonstart', 'addonconfigure'
@ -225,24 +223,31 @@ class TestScriptLoader:
assert len(m.addons) == 1 assert len(m.addons) == 1
def test_dupes(self): def test_dupes(self):
o = options.Options(scripts=["one", "one"])
m = master.Master(o, proxy.DummyServer())
sc = script.ScriptLoader() sc = script.ScriptLoader()
with taddons.context() as tctx:
tctx.master.addons.add(sc)
with pytest.raises(exceptions.OptionsError): with pytest.raises(exceptions.OptionsError):
m.addons.add(o, sc) tctx.configure(
sc,
scripts = ["one", "one"]
)
def test_nonexistent(self): def test_nonexistent(self):
o = options.Options(scripts=["nonexistent"])
m = master.Master(o, proxy.DummyServer())
sc = script.ScriptLoader() sc = script.ScriptLoader()
with taddons.context() as tctx:
tctx.master.addons.add(sc)
with pytest.raises(exceptions.OptionsError): with pytest.raises(exceptions.OptionsError):
m.addons.add(o, sc) tctx.configure(
sc,
scripts = ["nonexistent"]
)
def test_order(self): def test_order(self):
rec = tutils.test_data.path("mitmproxy/data/addonscripts/recorder.py") rec = tutils.test_data.path("mitmproxy/data/addonscripts/recorder.py")
sc = script.ScriptLoader() sc = script.ScriptLoader()
with taddons.context() as tctx: with taddons.context() as tctx:
tctx.master.addons.add(sc) tctx.master.addons.add(sc)
sc.running()
tctx.configure( tctx.configure(
sc, sc,
scripts = [ scripts = [
@ -253,9 +258,17 @@ class TestScriptLoader:
) )
debug = [(i[0], i[1]) for i in tctx.master.event_log if i[0] == "debug"] debug = [(i[0], i[1]) for i in tctx.master.event_log if i[0] == "debug"]
assert debug == [ assert debug == [
('debug', 'a start'), ('debug', 'a configure'), ('debug', 'a start'),
('debug', 'b start'), ('debug', 'b configure'), ('debug', 'a configure'),
('debug', 'c start'), ('debug', 'c configure') ('debug', 'a running'),
('debug', 'b start'),
('debug', 'b configure'),
('debug', 'b running'),
('debug', 'c start'),
('debug', 'c configure'),
('debug', 'c running'),
] ]
tctx.master.event_log = [] tctx.master.event_log = []
tctx.configure( tctx.configure(
@ -284,4 +297,5 @@ class TestScriptLoader:
('debug', 'b done'), ('debug', 'b done'),
('debug', 'x start'), ('debug', 'x start'),
('debug', 'x configure'), ('debug', 'x configure'),
('debug', 'x running'),
] ]

View File

@ -0,0 +1,12 @@
from mitmproxy.addons import termstatus
from mitmproxy.test import taddons
def test_configure():
ts = termstatus.TermStatus()
with taddons.context() as ctx:
ts.running()
assert not ctx.master.event_log
ctx.configure(ts, server=True)
ts.running()
assert ctx.master.event_log

View File

@ -6,7 +6,7 @@ class Addon:
def event_log(self): def event_log(self):
return event_log return event_log
def start(self): def start(self, opts):
event_log.append("addonstart") event_log.append("addonstart")
def configure(self, options, updated): def configure(self, options, updated):
@ -17,6 +17,6 @@ def configure(options, updated):
event_log.append("addonconfigure") event_log.append("addonconfigure")
def start(): def start(opts):
event_log.append("scriptstart") event_log.append("scriptstart")
return Addon() return Addon()

View File

@ -9,5 +9,5 @@ class ConcurrentClass:
time.sleep(0.1) time.sleep(0.1)
def start(): def start(opts):
return ConcurrentClass() return ConcurrentClass()

View File

@ -2,5 +2,5 @@ from mitmproxy.script import concurrent
@concurrent @concurrent
def start(): def start(opts):
pass pass

View File

@ -22,5 +22,5 @@ class CallLogger:
raise AttributeError raise AttributeError
def start(): def start(opts):
return CallLogger(*sys.argv[1:]) return CallLogger(*sys.argv[1:])

View File

@ -302,6 +302,9 @@ class TestHTTP(tservers.HTTPProxyTest, CommonMixin):
class TestHTTPAuth(tservers.HTTPProxyTest): class TestHTTPAuth(tservers.HTTPProxyTest):
def test_auth(self): def test_auth(self):
self.master.addons.add(proxyauth.ProxyAuth()) self.master.addons.add(proxyauth.ProxyAuth())
self.master.addons.configure_all(
self.master.options, self.master.options.keys()
)
self.master.options.proxyauth = "test:test" self.master.options.proxyauth = "test:test"
assert self.pathod("202").status_code == 407 assert self.pathod("202").status_code == 407
p = self.pathoc() p = self.pathoc()

View File

@ -24,7 +24,7 @@ class TestConcurrent(tservers.MasterTest):
"mitmproxy/data/addonscripts/concurrent_decorator.py" "mitmproxy/data/addonscripts/concurrent_decorator.py"
) )
) )
sc.start() sc.start(tctx.options)
f1, f2 = tflow.tflow(), tflow.tflow() f1, f2 = tflow.tflow(), tflow.tflow()
tctx.cycle(sc, f1) tctx.cycle(sc, f1)
@ -42,7 +42,7 @@ class TestConcurrent(tservers.MasterTest):
"mitmproxy/data/addonscripts/concurrent_decorator_err.py" "mitmproxy/data/addonscripts/concurrent_decorator_err.py"
) )
) )
sc.start() sc.start(tctx.options)
assert "decorator not supported" in tctx.master.event_log[0][1] assert "decorator not supported" in tctx.master.event_log[0][1]
def test_concurrent_class(self): def test_concurrent_class(self):
@ -52,7 +52,7 @@ class TestConcurrent(tservers.MasterTest):
"mitmproxy/data/addonscripts/concurrent_decorator_class.py" "mitmproxy/data/addonscripts/concurrent_decorator_class.py"
) )
) )
sc.start() sc.start(tctx.options)
f1, f2 = tflow.tflow(), tflow.tflow() f1, f2 = tflow.tflow(), tflow.tflow()
tctx.cycle(sc, f1) tctx.cycle(sc, f1)

View File

@ -10,12 +10,12 @@ from mitmproxy import proxy
class TAddon: class TAddon:
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
self.noop_member = True self.tick = True
def __repr__(self): def __repr__(self):
return "Addon(%s)" % self.name return "Addon(%s)" % self.name
def noop(self): def done(self):
pass pass
@ -30,6 +30,6 @@ def test_simple():
assert not a.chain assert not a.chain
a.add(TAddon("one")) a.add(TAddon("one"))
a("noop") a("done")
with pytest.raises(exceptions.AddonError): with pytest.raises(exceptions.AddonError):
a("noop_member") a("tick")

View File

@ -28,7 +28,9 @@ class TestMaster(tservers.MasterTest):
if "verbosity" not in opts: if "verbosity" not in opts:
opts["verbosity"] = 1 opts["verbosity"] = 1
o = options.Options(**opts) o = options.Options(**opts)
return console.master.ConsoleMaster(o, proxy.DummyServer()) m = console.master.ConsoleMaster(o, proxy.DummyServer())
m.addons.configure_all(o, o.keys())
return m
def test_basic(self): def test_basic(self):
m = self.mkmaster() m = self.mkmaster()

View File

@ -79,6 +79,8 @@ class TestMaster(master.Master):
self.state = TestState() self.state = TestState()
self.addons.add(self.state) self.addons.add(self.state)
self.addons.add(*addons) self.addons.add(*addons)
self.addons.configure_all(self.options, self.options.keys())
self.addons.invoke_all_with_context("running")
def clear_log(self): def clear_log(self):
self.tlog = [] self.tlog = []