aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--mitmproxy/addonmanager.py27
-rw-r--r--mitmproxy/addons/script.py32
-rw-r--r--mitmproxy/ctx.py6
-rw-r--r--mitmproxy/options.py79
-rw-r--r--mitmproxy/optmanager.py3
-rw-r--r--test/helper_tools/typehints_for_options.py34
-rw-r--r--test/mitmproxy/addons/test_script.py60
-rw-r--r--test/mitmproxy/test_addonmanager.py15
-rw-r--r--test/mitmproxy/test_optmanager.py4
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"