diff options
-rw-r--r-- | mitmproxy/addonmanager.py | 27 | ||||
-rw-r--r-- | mitmproxy/addons/script.py | 32 | ||||
-rw-r--r-- | mitmproxy/ctx.py | 6 | ||||
-rw-r--r-- | mitmproxy/options.py | 79 | ||||
-rw-r--r-- | mitmproxy/optmanager.py | 3 | ||||
-rw-r--r-- | test/helper_tools/typehints_for_options.py | 34 | ||||
-rw-r--r-- | test/mitmproxy/addons/test_script.py | 60 | ||||
-rw-r--r-- | test/mitmproxy/test_addonmanager.py | 15 | ||||
-rw-r--r-- | test/mitmproxy/test_optmanager.py | 4 |
9 files changed, 202 insertions, 58 deletions
diff --git a/mitmproxy/addonmanager.py b/mitmproxy/addonmanager.py index ec82d650..857ed530 100644 --- a/mitmproxy/addonmanager.py +++ b/mitmproxy/addonmanager.py @@ -1,3 +1,4 @@ +import types import typing import traceback import contextlib @@ -85,7 +86,18 @@ class Loader: choices: typing.Optional[typing.Sequence[str]] = None ) -> None: if name in self.master.options: - ctx.log.warn("Over-riding existing option %s" % name) + existing = self.master.options._options[name] + same_signature = ( + existing.name == name and + existing.typespec == typespec and + existing.default == default and + existing.help == help and + existing.choices == choices + ) + if same_signature: + return + else: + ctx.log.warn("Over-riding existing option %s" % name) self.master.options.add_option( name, typespec, @@ -229,11 +241,18 @@ class AddonManager: for a in traverse([addon]): func = getattr(a, name, None) if func: - if not callable(func): + if callable(func): + func(*args, **kwargs) + elif isinstance(func, types.ModuleType): + # we gracefully exclude module imports with the same name as hooks. + # For example, a user may have "from mitmproxy import log" in an addon, + # which has the same name as the "log" hook. In this particular case, + # we end up in an error loop because we "log" this error. + pass + else: raise exceptions.AddonManagerError( - "Addon handler %s not callable" % name + "Addon handler {} ({}) not callable".format(name, a) ) - func(*args, **kwargs) def trigger(self, name, *args, **kwargs): """ diff --git a/mitmproxy/addons/script.py b/mitmproxy/addons/script.py index 3e60fe67..58e8cdcd 100644 --- a/mitmproxy/addons/script.py +++ b/mitmproxy/addons/script.py @@ -3,6 +3,7 @@ import importlib.util import importlib.machinery import time import sys +import types import typing from mitmproxy import addonmanager @@ -13,28 +14,23 @@ from mitmproxy import eventsequence from mitmproxy import ctx -def load_script(actx, path): - if not os.path.exists(path): - ctx.log.info("No such file: %s" % path) - return - +def load_script(path: str) -> types.ModuleType: fullname = "__mitmproxy_script__.{}".format( os.path.splitext(os.path.basename(path))[0] ) # the fullname is not unique among scripts, so if there already is an existing script with said # fullname, remove it. sys.modules.pop(fullname, None) + oldpath = sys.path + sys.path.insert(0, os.path.dirname(path)) try: - oldpath = sys.path - sys.path.insert(0, os.path.dirname(path)) - with addonmanager.safecall(): - loader = importlib.machinery.SourceFileLoader(fullname, path) - spec = importlib.util.spec_from_loader(fullname, loader=loader) - m = importlib.util.module_from_spec(spec) - loader.exec_module(m) - if not getattr(m, "name", None): - m.name = path - return m + loader = importlib.machinery.SourceFileLoader(fullname, path) + spec = importlib.util.spec_from_loader(fullname, loader=loader) + m = importlib.util.module_from_spec(spec) + loader.exec_module(m) + if not getattr(m, "name", None): + m.name = path # type: ignore + return m finally: sys.path[:] = oldpath @@ -65,7 +61,7 @@ class Script: try: mtime = os.stat(self.fullpath).st_mtime except FileNotFoundError: - scripts = ctx.options.scripts + scripts = list(ctx.options.scripts) scripts.remove(self.path) ctx.options.update(scripts=scripts) return @@ -74,7 +70,9 @@ class Script: ctx.log.info("Loading script: %s" % self.path) if self.ns: ctx.master.addons.remove(self.ns) - self.ns = load_script(ctx, self.fullpath) + self.ns = None + with addonmanager.safecall(): + self.ns = load_script(self.fullpath) if self.ns: # We're already running, so we have to explicitly register and # configure the addon diff --git a/mitmproxy/ctx.py b/mitmproxy/ctx.py index 954edcb1..2ad1faec 100644 --- a/mitmproxy/ctx.py +++ b/mitmproxy/ctx.py @@ -2,6 +2,6 @@ import mitmproxy.master # noqa import mitmproxy.log # noqa import mitmproxy.options # noqa -master = None # type: "mitmproxy.master.Master" -log = None # type: "mitmproxy.log.Log" -options = None # type: "mitmproxy.options.Options" +master = None # type: mitmproxy.master.Master +log = None # type: mitmproxy.log.Log +options = None # type: mitmproxy.options.Options diff --git a/mitmproxy/options.py b/mitmproxy/options.py index 7df7736d..20151c19 100644 --- a/mitmproxy/options.py +++ b/mitmproxy/options.py @@ -48,6 +48,85 @@ LISTEN_PORT = 8080 class Options(optmanager.OptManager): + + if False: + # This provides type hints for IDEs (e.g. PyCharm) and mypy. + # Autogenerated using test/helper_tools/typehints_for_options.py + add_upstream_certs_to_client_chain = None # type: bool + allow_remote = None # type: bool + anticache = None # type: bool + anticomp = None # type: bool + body_size_limit = None # type: Optional[str] + cadir = None # type: str + certs = None # type: Sequence[str] + ciphers_client = None # type: Optional[str] + ciphers_server = None # type: Optional[str] + client_certs = None # type: Optional[str] + client_replay = None # type: Sequence[str] + console_focus_follow = None # type: bool + console_layout = None # type: str + console_layout_headers = None # type: bool + console_mouse = None # type: bool + console_order = None # type: str + console_order_reversed = None # type: bool + console_palette = None # type: str + console_palette_transparent = None # type: bool + default_contentview = None # type: str + flow_detail = None # type: int + http2 = None # type: bool + http2_priority = None # type: bool + ignore_hosts = None # type: Sequence[str] + intercept = None # type: Optional[str] + intercept_active = None # type: bool + keep_host_header = None # type: bool + keepserving = None # type: bool + listen_host = None # type: str + listen_port = None # type: int + mode = None # type: str + onboarding = None # type: bool + onboarding_host = None # type: str + onboarding_port = None # type: int + proxyauth = None # type: Optional[str] + rawtcp = None # type: bool + refresh_server_playback = None # type: bool + replacements = None # type: Sequence[str] + replay_kill_extra = None # type: bool + rfile = None # type: Optional[str] + save_stream_file = None # type: Optional[str] + save_stream_filter = None # type: Optional[str] + scripts = None # type: Sequence[str] + server = None # type: bool + server_replay = None # type: Sequence[str] + server_replay_ignore_content = None # type: bool + server_replay_ignore_host = None # type: bool + server_replay_ignore_params = None # type: Sequence[str] + server_replay_ignore_payload_params = None # type: Sequence[str] + server_replay_nopop = None # type: bool + server_replay_use_headers = None # type: Sequence[str] + setheaders = None # type: Sequence[str] + showhost = None # type: bool + spoof_source_address = None # type: bool + ssl_insecure = None # type: bool + ssl_verify_upstream_trusted_ca = None # type: Optional[str] + ssl_verify_upstream_trusted_cadir = None # type: Optional[str] + ssl_version_client = None # type: str + ssl_version_server = None # type: str + stickyauth = None # type: Optional[str] + stickycookie = None # type: Optional[str] + stream_large_bodies = None # type: Optional[str] + stream_websockets = None # type: bool + tcp_hosts = None # type: Sequence[str] + upstream_auth = None # type: Optional[str] + upstream_bind_address = None # type: str + upstream_cert = None # type: bool + verbosity = None # type: str + view_filter = None # type: Optional[str] + web_debug = None # type: bool + web_iface = None # type: str + web_open_browser = None # type: bool + web_port = None # type: int + websocket = None # type: bool + def __init__(self, **kwargs) -> None: super().__init__() self.add_option( diff --git a/mitmproxy/optmanager.py b/mitmproxy/optmanager.py index c28ec685..84c8d2ea 100644 --- a/mitmproxy/optmanager.py +++ b/mitmproxy/optmanager.py @@ -170,6 +170,9 @@ class OptManager: def keys(self): return set(self._options.keys()) + def items(self): + return self._options.items() + def __contains__(self, k): return k in self._options diff --git a/test/helper_tools/typehints_for_options.py b/test/helper_tools/typehints_for_options.py new file mode 100644 index 00000000..8c7d006c --- /dev/null +++ b/test/helper_tools/typehints_for_options.py @@ -0,0 +1,34 @@ +import typing +from unittest import mock + +from mitmproxy import proxy, options +from mitmproxy.tools import dump, console, web + + +def print_typehints(opts): + for name, option in sorted(opts.items()): + print( + # For Python 3.6, we can just use "{}: {}". + "{} = None # type: {}".format( + name, + { + int: "int", + str: "str", + bool: "bool", + typing.Optional[str]: "Optional[str]", + typing.Sequence[str]: "Sequence[str]" + }[option.typespec] + ) + ) + + +if __name__ == "__main__": + opts = options.Options() + server = proxy.server.DummyServer(None) + + # initialize with all three tools here to capture tool-specific options defined in addons. + dump.DumpMaster(opts, server) + with mock.patch("sys.stdout.isatty", lambda: True): + console.master.ConsoleMaster(opts, server) + web.master.WebMaster(opts, server) + print_typehints(opts) diff --git a/test/mitmproxy/addons/test_script.py b/test/mitmproxy/addons/test_script.py index b7e6c82a..64fd9505 100644 --- a/test/mitmproxy/addons/test_script.py +++ b/test/mitmproxy/addons/test_script.py @@ -1,6 +1,5 @@ import traceback import sys -import time import os import pytest @@ -14,20 +13,17 @@ from mitmproxy.addons import script def test_load_script(): - with taddons.context() as tctx: - ns = script.load_script( - tctx.ctx(), - tutils.test_data.path( - "mitmproxy/data/addonscripts/recorder/recorder.py" - ) + ns = script.load_script( + tutils.test_data.path( + "mitmproxy/data/addonscripts/recorder/recorder.py" ) - assert ns.addons + ) + assert ns.addons - ns = script.load_script( - tctx.ctx(), + with pytest.raises(FileNotFoundError): + script.load_script( "nonexistent" ) - assert not ns def test_load_fullname(): @@ -36,22 +32,19 @@ def test_load_fullname(): This only succeeds if they get assigned different basenames. """ - with taddons.context() as tctx: - ns = script.load_script( - tctx.ctx(), - tutils.test_data.path( - "mitmproxy/data/addonscripts/addon.py" - ) + ns = script.load_script( + tutils.test_data.path( + "mitmproxy/data/addonscripts/addon.py" ) - assert ns.addons - ns2 = script.load_script( - tctx.ctx(), - tutils.test_data.path( - "mitmproxy/data/addonscripts/same_filename/addon.py" - ) + ) + assert ns.addons + ns2 = script.load_script( + tutils.test_data.path( + "mitmproxy/data/addonscripts/same_filename/addon.py" ) - assert ns.name != ns2.name - assert not hasattr(ns2, "addons") + ) + assert ns.name != ns2.name + assert not hasattr(ns2, "addons") def test_script_print_stdout(): @@ -59,7 +52,6 @@ def test_script_print_stdout(): with mock.patch('mitmproxy.ctx.log.warn') as mock_warn: with addonmanager.safecall(): ns = script.load_script( - tctx.ctx(), tutils.test_data.path( "mitmproxy/data/addonscripts/print.py" ) @@ -103,11 +95,13 @@ class TestScript: sc = script.Script(str(f)) tctx.configure(sc) sc.tick() - for _ in range(3): - sc.last_load, sc.last_mtime = 0, 0 - sc.tick() - time.sleep(0.1) - tctx.master.has_log("Loading") + assert tctx.master.has_log("Loading") + tctx.master.clear() + assert not tctx.master.has_log("Loading") + + sc.last_load, sc.last_mtime = 0, 0 + sc.tick() + assert tctx.master.has_log("Loading") def test_exception(self): with taddons.context() as tctx: @@ -121,8 +115,8 @@ class TestScript: f = tflow.tflow(resp=True) tctx.master.addons.trigger("request", f) - tctx.master.has_log("ValueError: Error!") - tctx.master.has_log("error.py") + assert tctx.master.has_log("ValueError: Error!") + assert tctx.master.has_log("error.py") def test_addon(self): with taddons.context() as tctx: diff --git a/test/mitmproxy/test_addonmanager.py b/test/mitmproxy/test_addonmanager.py index 3ac74375..accf48e0 100644 --- a/test/mitmproxy/test_addonmanager.py +++ b/test/mitmproxy/test_addonmanager.py @@ -90,7 +90,15 @@ def test_loader(): with taddons.context() as tctx: l = addonmanager.Loader(tctx.master) l.add_option("custom_option", bool, False, "help") + assert "custom_option" in l.master.options + + # calling this again with the same signature is a no-op. l.add_option("custom_option", bool, False, "help") + assert not tctx.master.has_log("Over-riding existing option") + + # a different signature should emit a warning though. + l.add_option("custom_option", bool, True, "help") + assert tctx.master.has_log("Over-riding existing option") def cmd(a: str) -> str: return "foo" @@ -114,7 +122,12 @@ def test_simple(): a.add(TAddon("one")) a.trigger("done") a.trigger("tick") - tctx.master.has_log("not callable") + assert tctx.master.has_log("not callable") + + tctx.master.clear() + a.get("one").tick = addons + a.trigger("tick") + assert not tctx.master.has_log("not callable") a.remove(a.get("one")) assert not a.get("one") diff --git a/test/mitmproxy/test_optmanager.py b/test/mitmproxy/test_optmanager.py index 7b4ffb8b..fe72e6bb 100644 --- a/test/mitmproxy/test_optmanager.py +++ b/test/mitmproxy/test_optmanager.py @@ -229,6 +229,10 @@ def test_simple(): assert "one" in TO() +def test_items(): + assert TO().items() + + def test_serialize(): o = TD2() o.three = "set" |