More powerful Options scheme

This prepares us for the addon configuration mechanism and gives us a more
flexible way to handle options changes. This changeset should spell the end of
the current anti-pattern in our codebase where we duplicate data out of options
onto the master when mutability is needed. From now on, Options can be the one
source of thruth.

- Change notifications
- Rollback on error
This commit is contained in:
Aldo Cortesi 2016-07-13 18:45:50 +12:00
parent 2624911d75
commit a20f8e9620
6 changed files with 143 additions and 25 deletions

View File

@ -20,6 +20,7 @@ from mitmproxy import controller
from mitmproxy import exceptions from mitmproxy import exceptions
from mitmproxy import flow from mitmproxy import flow
from mitmproxy import script from mitmproxy import script
import mitmproxy.options
from mitmproxy.console import flowlist from mitmproxy.console import flowlist
from mitmproxy.console import flowview from mitmproxy.console import flowview
from mitmproxy.console import grideditor from mitmproxy.console import grideditor
@ -175,7 +176,7 @@ class ConsoleState(flow.State):
self.add_flow_setting(flow, "marked", marked) self.add_flow_setting(flow, "marked", marked)
class Options(object): class Options(mitmproxy.options.Options):
attributes = [ attributes = [
"app", "app",
"app_domain", "app_domain",
@ -210,13 +211,6 @@ class Options(object):
"outfile", "outfile",
] ]
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
for i in self.attributes:
if not hasattr(self, i):
setattr(self, i, None)
class ConsoleMaster(flow.FlowMaster): class ConsoleMaster(flow.FlowMaster):
palette = [] palette = []

View File

@ -11,6 +11,7 @@ from mitmproxy import controller
from mitmproxy import exceptions from mitmproxy import exceptions
from mitmproxy import filt from mitmproxy import filt
from mitmproxy import flow from mitmproxy import flow
from mitmproxy import options
from netlib import human from netlib import human
from netlib import tcp from netlib import tcp
from netlib import strutils from netlib import strutils
@ -20,7 +21,7 @@ class DumpError(Exception):
pass pass
class Options(object): class Options(options.Options):
attributes = [ attributes = [
"app", "app",
"app_host", "app_host",
@ -53,13 +54,6 @@ class Options(object):
"replay_ignore_host" "replay_ignore_host"
] ]
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
for i in self.attributes:
if not hasattr(self, i):
setattr(self, i, None)
class DumpMaster(flow.FlowMaster): class DumpMaster(flow.FlowMaster):

View File

@ -95,3 +95,7 @@ class FlowReadException(ProxyException):
class ControlException(ProxyException): class ControlException(ProxyException):
pass pass
class OptionsError(Exception):
pass

66
mitmproxy/options.py Normal file
View File

@ -0,0 +1,66 @@
from __future__ import absolute_import, print_function, division
import contextlib
import blinker
import pprint
from mitmproxy import exceptions
class Options(object):
"""
.changed is a blinker Signal that triggers whenever options are
updated. If any handler in the chain raises an exceptions.OptionsError
exception, all changes are rolled back, the exception is suppressed,
and the .errored signal is notified.
"""
attributes = []
def __init__(self, **kwargs):
self.__dict__["changed"] = blinker.Signal()
self.__dict__["errored"] = blinker.Signal()
self.__dict__["_opts"] = dict([(i, None) for i in self.attributes])
for k, v in kwargs.items():
self._opts[k] = v
@contextlib.contextmanager
def rollback(self):
old = self._opts.copy()
try:
yield
except exceptions.OptionsError as e:
# Notify error handlers
self.errored.send(self, exc=e)
# Rollback
self.__dict__["_opts"] = old
self.changed.send(self)
def __eq__(self, other):
return self._opts == other._opts
def __copy__(self):
return self.__class__(**self._opts)
def __getattr__(self, attr):
return self._opts[attr]
def __setattr__(self, attr, value):
if attr not in self._opts:
raise KeyError("No such option: %s" % attr)
with self.rollback():
self._opts[attr] = value
self.changed.send(self)
def get(self, k, d=None):
return self._opts.get(k, d)
def update(self, **kwargs):
for k in kwargs:
if k not in self._opts:
raise KeyError("No such option: %s" % k)
with self.rollback():
self._opts.update(kwargs)
self.changed.send(self)
def __repr__(self):
return pprint.pformat(self._opts)

View File

@ -9,6 +9,7 @@ import tornado.ioloop
from mitmproxy import controller from mitmproxy import controller
from mitmproxy import exceptions from mitmproxy import exceptions
from mitmproxy import flow from mitmproxy import flow
from mitmproxy import options
from mitmproxy.web import app from mitmproxy.web import app
from netlib.http import authentication from netlib.http import authentication
@ -88,7 +89,7 @@ class WebState(flow.State):
) )
class Options(object): class Options(options.Options):
attributes = [ attributes = [
"app", "app",
"app_domain", "app_domain",
@ -124,14 +125,6 @@ class Options(object):
"wsingleuser", "wsingleuser",
"whtpasswd", "whtpasswd",
] ]
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
for i in self.attributes:
if not hasattr(self, i):
setattr(self, i, None)
def process_web_options(self, parser): def process_web_options(self, parser):
if self.wsingleuser or self.whtpasswd: if self.wsingleuser or self.whtpasswd:
if self.wsingleuser: if self.wsingleuser:

View File

@ -0,0 +1,67 @@
from __future__ import absolute_import, print_function, division
import copy
from mitmproxy import options
from mitmproxy import exceptions
from netlib.tutils import raises
class TO(options.Options):
attributes = [
"one",
"two"
]
def test_options():
o = TO(two="three")
assert o.one is None
assert o.two == "three"
o.one = "one"
assert o.one == "one"
raises("no such option", setattr, o, "nonexistent", "value")
raises("no such option", o.update, nonexistent = "value")
rec = []
def sub(opts):
rec.append(copy.copy(opts))
o.changed.connect(sub)
o.one = "ninety"
assert len(rec) == 1
assert rec[-1].one == "ninety"
o.update(one="oink")
assert len(rec) == 2
assert rec[-1].one == "oink"
def test_rollback():
o = TO(one="two")
rec = []
def sub(opts):
rec.append(copy.copy(opts))
recerr = []
def errsub(opts, **kwargs):
recerr.append(kwargs)
def err(opts):
if opts.one == "ten":
raise exceptions.OptionsError
o.changed.connect(sub)
o.changed.connect(err)
o.errored.connect(errsub)
o.one = "ten"
assert isinstance(recerr[0]["exc"], exceptions.OptionsError)
assert o.one == "two"
assert len(rec) == 2
assert rec[0].one == "ten"
assert rec[1].one == "two"