aboutsummaryrefslogtreecommitdiffstats
path: root/mitmproxy/addonmanager.py
diff options
context:
space:
mode:
authorAldo Cortesi <aldo@nullcube.com>2017-04-25 19:06:24 +1200
committerAldo Cortesi <aldo@nullcube.com>2017-04-25 22:13:44 +1200
commite6eeab60946e61047ed858422badbda189a6f9e8 (patch)
treec0bc4775cd24824a3c2ff7df73c3b6078874eb70 /mitmproxy/addonmanager.py
parent90c425bd14087a984afd92eec2c18e63707e4ffa (diff)
downloadmitmproxy-e6eeab60946e61047ed858422badbda189a6f9e8.tar.gz
mitmproxy-e6eeab60946e61047ed858422badbda189a6f9e8.tar.bz2
mitmproxy-e6eeab60946e61047ed858422badbda189a6f9e8.zip
Revamp how addons work
- Addons now nest, which means that addons can manage addons. This has a number of salutary effects - the scripts addon no longer has to poke into the global addons list, we no longer have to replace/remove/boot-outof parent addons when we load scripts, and this paves the way for making our top-level tools into addons themselves. - All addon calls are now wrapped in a safe execution environment where exceptions are caught, and output to stdout/stderr are intercepted and turned into logs. - We no longer support script arguments in sys.argv - creating an option properly is the only way to pass arguments. This means that all scripts are always directly controllable from interctive tooling, and that arguments are type-checked. For now, I've disabled testing of the har dump example - it needs to be moved to the new argument handling, and become a class addon. I'll address that in a separate patch.
Diffstat (limited to 'mitmproxy/addonmanager.py')
-rw-r--r--mitmproxy/addonmanager.py160
1 files changed, 119 insertions, 41 deletions
diff --git a/mitmproxy/addonmanager.py b/mitmproxy/addonmanager.py
index 241b3cde..b6d7adb6 100644
--- a/mitmproxy/addonmanager.py
+++ b/mitmproxy/addonmanager.py
@@ -1,4 +1,7 @@
import typing
+import traceback
+import contextlib
+import sys
from mitmproxy import exceptions
from mitmproxy import eventsequence
@@ -11,13 +14,66 @@ def _get_name(itm):
return getattr(itm, "name", itm.__class__.__name__.lower())
+def cut_traceback(tb, func_name):
+ """
+ Cut off a traceback at the function with the given name.
+ The func_name's frame is excluded.
+
+ Args:
+ tb: traceback object, as returned by sys.exc_info()[2]
+ func_name: function name
+
+ Returns:
+ Reduced traceback.
+ """
+ tb_orig = tb
+ for _, _, fname, _ in traceback.extract_tb(tb):
+ tb = tb.tb_next
+ if fname == func_name:
+ break
+ return tb or tb_orig
+
+
+class StreamLog:
+ """
+ A class for redirecting output using contextlib.
+ """
+ def __init__(self, log):
+ self.log = log
+
+ def write(self, buf):
+ if buf.strip():
+ self.log(buf)
+
+ def flush(self): # pragma: no cover
+ # Click uses flush sometimes, so we dummy it up
+ pass
+
+
+@contextlib.contextmanager
+def safecall():
+ stdout_replacement = StreamLog(ctx.log.warn)
+ try:
+ with contextlib.redirect_stdout(stdout_replacement):
+ yield
+ except exceptions.AddonHalt:
+ raise
+ except Exception as e:
+ etype, value, tb = sys.exc_info()
+ tb = cut_traceback(tb, "invoke_addon").tb_next
+ ctx.log.error(
+ "Addon error: %s" % "".join(
+ traceback.format_exception(etype, value, tb)
+ )
+ )
+
+
class Loader:
"""
A loader object is passed to the load() event when addons start up.
"""
def __init__(self, master):
self.master = master
- self.boot_into_addon = None
def add_option(
self,
@@ -35,25 +91,33 @@ class Loader:
choices
)
- def boot_into(self, addon):
- self.boot_into_addon = addon
- func = getattr(addon, "load", None)
- if func:
- func(self)
+
+def traverse(chain):
+ """
+ Recursively traverse an addon chain.
+ """
+ for a in chain:
+ yield a
+ if hasattr(a, "addons"):
+ yield from traverse(a.addons)
class AddonManager:
def __init__(self, master):
+ self.lookup = {}
self.chain = []
self.master = master
- master.options.changed.connect(self.configure_all)
+ master.options.changed.connect(self._configure_all)
+
+ def _configure_all(self, options, updated):
+ self.trigger("configure", options, updated)
def clear(self):
"""
Remove all addons.
"""
- self.done()
- self.chain = []
+ for i in self.chain:
+ self.remove(i)
def get(self, name):
"""
@@ -61,36 +125,52 @@ class AddonManager:
attribute on the instance, or the lower case class name if that
does not exist.
"""
- for i in self.chain:
- if name == _get_name(i):
- return i
+ return self.lookup.get(name, None)
- def configure_all(self, options, updated):
- self.trigger("configure", options, updated)
+ def register(self, addon):
+ """
+ Register an addon and all its sub-addons with the manager without
+ adding it to the chain. This should be used by addons that
+ dynamically manage addons. Must be called within a current context.
+ """
+ for a in traverse([addon]):
+ name = _get_name(a)
+ if name in self.lookup:
+ raise exceptions.AddonError(
+ "An addon called '%s' already exists." % name
+ )
+ l = Loader(self.master)
+ self.invoke_addon(addon, "load", l)
+ for a in traverse([addon]):
+ name = _get_name(a)
+ self.lookup[name] = a
+ return addon
def add(self, *addons):
"""
- Add addons to the end of the chain, and run their startup events.
+ Add addons to the end of the chain, and run their load event.
+ If any addon has sub-addons, they are registered.
"""
with self.master.handlecontext():
for i in addons:
- l = Loader(self.master)
- self.invoke_addon(i, "load", l)
- if l.boot_into_addon:
- self.chain.append(l.boot_into_addon)
- else:
- self.chain.append(i)
+ self.chain.append(self.register(i))
def remove(self, addon):
"""
- Remove an addon from the chain, and run its done events.
+ Remove an addon and all its sub-addons.
+
+ If the addon is not in the chain - that is, if it's managed by a
+ parent addon - it's the parent's responsibility to remove it from
+ its own addons attribute.
"""
- self.chain = [i for i in self.chain if i is not addon]
+ for a in traverse([addon]):
+ n = _get_name(a)
+ if n not in self.lookup:
+ raise exceptions.AddonError("No such addon: %s" % n)
+ self.chain = [i for i in self.chain if i is not a]
+ del self.lookup[_get_name(a)]
with self.master.handlecontext():
- self.invoke_addon(addon, "done")
-
- def done(self):
- self.trigger("done")
+ self.invoke_addon(a, "done")
def __len__(self):
return len(self.chain)
@@ -126,22 +206,19 @@ class AddonManager:
def invoke_addon(self, addon, name, *args, **kwargs):
"""
- Invoke an event on an addon. This method must run within an
- established handler context.
+ Invoke an event on an addon and all its children. This method must
+ run within an established handler context.
"""
- if not ctx.master:
- raise exceptions.AddonError(
- "invoke_addon called without a handler context."
- )
if name not in eventsequence.Events:
name = "event_" + name
- func = getattr(addon, name, None)
- if func:
- if not callable(func):
- raise exceptions.AddonError(
- "Addon handler %s not callable" % name
- )
- func(*args, **kwargs)
+ for a in traverse([addon]):
+ func = getattr(a, name, None)
+ if func:
+ if not callable(func):
+ raise exceptions.AddonError(
+ "Addon handler %s not callable" % name
+ )
+ func(*args, **kwargs)
def trigger(self, name, *args, **kwargs):
"""
@@ -150,6 +227,7 @@ class AddonManager:
with self.master.handlecontext():
for i in self.chain:
try:
- self.invoke_addon(i, name, *args, **kwargs)
+ with safecall():
+ self.invoke_addon(i, name, *args, **kwargs)
except exceptions.AddonHalt:
return