Revamp options

- Options are now explicitly initialized with an add_option method
- We have one canonical Options class - ditch dump.Options
This commit is contained in:
Aldo Cortesi 2017-03-05 14:55:46 +13:00
parent e0644398b6
commit 67381ae550
14 changed files with 276 additions and 391 deletions

View File

@ -21,186 +21,94 @@ DEFAULT_CLIENT_CIPHERS = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA
class Options(optmanager.OptManager): class Options(optmanager.OptManager):
def __init__( def __init__(self, **kwargs) -> None:
self,
*, # all args are keyword-only.
onboarding: bool = True,
onboarding_host: str = APP_HOST,
onboarding_port: int = APP_PORT,
anticache: bool = False,
anticomp: bool = False,
client_replay: Sequence[str] = [],
replay_kill_extra: bool = False,
keepserving: bool = True,
no_server: bool = False,
server_replay_nopop: bool = False,
refresh_server_playback: bool = True,
rfile: Optional[str] = None,
scripts: Sequence[str] = [],
showhost: bool = False,
replacements: Sequence[Union[Tuple[str, str, str], str]] = [],
replacement_files: Sequence[Union[Tuple[str, str, str], str]] = [],
server_replay_use_headers: Sequence[str] = [],
setheaders: Sequence[Union[Tuple[str, str, str], str]] = [],
server_replay: Sequence[str] = [],
stickycookie: Optional[str] = None,
stickyauth: Optional[str] = None,
stream_large_bodies: Optional[int] = None,
verbosity: int = 2,
default_contentview: str = "auto",
streamfile: Optional[str] = None,
streamfile_append: bool = False,
server_replay_ignore_content: bool = False,
server_replay_ignore_params: Sequence[str] = [],
server_replay_ignore_payload_params: Sequence[str] = [],
server_replay_ignore_host: bool = False,
# Proxy options
auth_nonanonymous: bool = False,
auth_singleuser: Optional[str] = None,
auth_htpasswd: Optional[str] = None,
add_upstream_certs_to_client_chain: bool = False,
body_size_limit: Optional[int] = None,
cadir: str = CA_DIR,
certs: Sequence[Tuple[str, str]] = [],
ciphers_client: str=DEFAULT_CLIENT_CIPHERS,
ciphers_server: Optional[str]=None,
clientcerts: Optional[str] = None,
ignore_hosts: Sequence[str] = [],
listen_host: str = "",
listen_port: int = LISTEN_PORT,
upstream_bind_address: str = "",
mode: str = "regular",
no_upstream_cert: bool = False,
keep_host_header: bool = False,
http2: bool = True,
http2_priority: bool = False,
websocket: bool = True,
rawtcp: bool = False,
spoof_source_address: bool = False,
upstream_server: Optional[str] = None,
upstream_auth: Optional[str] = None,
ssl_version_client: str = "secure",
ssl_version_server: str = "secure",
ssl_insecure: bool = False,
ssl_verify_upstream_trusted_cadir: Optional[str] = None,
ssl_verify_upstream_trusted_ca: Optional[str] = None,
tcp_hosts: Sequence[str] = [],
intercept: Optional[str] = None,
# Console options
console_eventlog: bool = False,
console_focus_follow: bool = False,
console_palette: Optional[str] = "dark",
console_palette_transparent: bool = False,
console_no_mouse: bool = False,
console_order: Optional[str] = None,
console_order_reversed: bool = False,
filter: Optional[str] = None,
# Web options
web_open_browser: bool = True,
web_debug: bool = False,
web_port: int = 8081,
web_iface: str = "127.0.0.1",
# Dump options
filtstr: Optional[str] = None,
flow_detail: int = 1
) -> None:
# We could replace all assignments with clever metaprogramming,
# but type hints are a much more valueable asset.
self.onboarding = onboarding
self.onboarding_host = onboarding_host
self.onboarding_port = onboarding_port
self.anticache = anticache
self.anticomp = anticomp
self.client_replay = client_replay
self.keepserving = keepserving
self.replay_kill_extra = replay_kill_extra
self.no_server = no_server
self.server_replay_nopop = server_replay_nopop
self.refresh_server_playback = refresh_server_playback
self.rfile = rfile
self.scripts = scripts
self.showhost = showhost
self.replacements = replacements
self.replacement_files = replacement_files
self.server_replay_use_headers = server_replay_use_headers
self.setheaders = setheaders
self.server_replay = server_replay
self.stickycookie = stickycookie
self.stickyauth = stickyauth
self.stream_large_bodies = stream_large_bodies
self.verbosity = verbosity
self.default_contentview = default_contentview
self.streamfile = streamfile
self.streamfile_append = streamfile_append
self.server_replay_ignore_content = server_replay_ignore_content
self.server_replay_ignore_params = server_replay_ignore_params
self.server_replay_ignore_payload_params = server_replay_ignore_payload_params
self.server_replay_ignore_host = server_replay_ignore_host
# Proxy options
self.auth_nonanonymous = auth_nonanonymous
self.auth_singleuser = auth_singleuser
self.auth_htpasswd = auth_htpasswd
self.add_upstream_certs_to_client_chain = add_upstream_certs_to_client_chain
self.body_size_limit = body_size_limit
self.cadir = cadir
self.certs = certs
self.ciphers_client = ciphers_client
self.ciphers_server = ciphers_server
self.clientcerts = clientcerts
self.ignore_hosts = ignore_hosts
self.listen_host = listen_host
self.listen_port = listen_port
self.upstream_bind_address = upstream_bind_address
self.mode = mode
self.no_upstream_cert = no_upstream_cert
self.keep_host_header = keep_host_header
self.http2 = http2
self.http2_priority = http2_priority
self.websocket = websocket
self.rawtcp = rawtcp
self.spoof_source_address = spoof_source_address
self.upstream_server = upstream_server
self.upstream_auth = upstream_auth
self.ssl_version_client = ssl_version_client
self.ssl_version_server = ssl_version_server
self.ssl_insecure = ssl_insecure
self.ssl_verify_upstream_trusted_cadir = ssl_verify_upstream_trusted_cadir
self.ssl_verify_upstream_trusted_ca = ssl_verify_upstream_trusted_ca
self.tcp_hosts = tcp_hosts
self.intercept = intercept
# Console options
self.console_eventlog = console_eventlog
self.console_focus_follow = console_focus_follow
self.console_palette = console_palette
self.console_palette_transparent = console_palette_transparent
self.console_no_mouse = console_no_mouse
self.console_order = console_order
self.console_order_reversed = console_order_reversed
self.filter = filter
# Web options
self.web_open_browser = web_open_browser
self.web_debug = web_debug
self.web_port = web_port
self.web_iface = web_iface
# Dump options
self.filtstr = filtstr
self.flow_detail = flow_detail
super().__init__() super().__init__()
self.add_option("onboarding", True, bool)
self.add_option("onboarding_host", APP_HOST, str)
self.add_option("onboarding_port", APP_PORT, int)
self.add_option("anticache", False, bool)
self.add_option("anticomp", False, bool)
self.add_option("client_replay", [], Sequence[str])
self.add_option("replay_kill_extra", False, bool)
self.add_option("keepserving", True, bool)
self.add_option("no_server", False, bool)
self.add_option("server_replay_nopop", False, bool)
self.add_option("refresh_server_playback", True, bool)
self.add_option("rfile", None, Optional[str])
self.add_option("scripts", [], Sequence[str])
self.add_option("showhost", False, bool)
self.add_option("replacements", [], Sequence[Union[Tuple[str, str, str], str]])
self.add_option("replacement_files", [], Sequence[Union[Tuple[str, str, str], str]])
self.add_option("server_replay_use_headers", [], Sequence[str])
self.add_option("setheaders", [], Sequence[Union[Tuple[str, str, str], str]])
self.add_option("server_replay", [], Sequence[str])
self.add_option("stickycookie", None, Optional[str])
self.add_option("stickyauth", None, Optional[str])
self.add_option("stream_large_bodies", None, Optional[int])
self.add_option("verbosity", 2, int)
self.add_option("default_contentview", "auto", str)
self.add_option("streamfile", None, Optional[str])
self.add_option("streamfile_append", False, bool)
self.add_option("server_replay_ignore_content", False, bool)
self.add_option("server_replay_ignore_params", [], Sequence[str])
self.add_option("server_replay_ignore_payload_params", [], Sequence[str])
self.add_option("server_replay_ignore_host", False, bool)
# Proxy options
self.add_option("auth_nonanonymous", False, bool)
self.add_option("auth_singleuser", None, Optional[str])
self.add_option("auth_htpasswd", None, Optional[str])
self.add_option("add_upstream_certs_to_client_chain", False, bool)
self.add_option("body_size_limit", None, Optional[int])
self.add_option("cadir", CA_DIR, str)
self.add_option("certs", [], Sequence[Tuple[str, str]])
self.add_option("ciphers_client", DEFAULT_CLIENT_CIPHERS, str)
self.add_option("ciphers_server", None, Optional[str])
self.add_option("clientcerts", None, Optional[str])
self.add_option("ignore_hosts", [], Sequence[str])
self.add_option("listen_host", "", str)
self.add_option("listen_port", LISTEN_PORT, int)
self.add_option("upstream_bind_address", "", str)
self.add_option("mode", "regular", str)
self.add_option("no_upstream_cert", False, bool)
self.add_option("keep_host_header", False, bool)
self.add_option("http2", True, bool)
self.add_option("http2_priority", False, bool)
self.add_option("websocket", True, bool)
self.add_option("rawtcp", False, bool)
self.add_option("spoof_source_address", False, bool)
self.add_option("upstream_server", None, Optional[str])
self.add_option("upstream_auth", None, Optional[str])
self.add_option("ssl_version_client", "secure", str)
self.add_option("ssl_version_server", "secure", str)
self.add_option("ssl_insecure", False, bool)
self.add_option("ssl_verify_upstream_trusted_cadir", None, Optional[str])
self.add_option("ssl_verify_upstream_trusted_ca", None, Optional[str])
self.add_option("tcp_hosts", [], Sequence[str])
self.add_option("intercept", None, Optional[str])
# Console options
self.add_option("console_eventlog", False, bool)
self.add_option("console_focus_follow", False, bool)
self.add_option("console_palette", "dark", Optional[str])
self.add_option("console_palette_transparent", False, bool)
self.add_option("console_no_mouse", False, bool)
self.add_option("console_order", None, Optional[str])
self.add_option("console_order_reversed", False, bool)
self.add_option("filter", None, Optional[str])
# Web options
self.add_option("web_open_browser", True, bool)
self.add_option("web_debug", False, bool)
self.add_option("web_port", 8081, int)
self.add_option("web_iface", "127.0.0.1", str)
# Dump options
self.add_option("filtstr", None, Optional[str])
self.add_option("flow_detail", 1, int)
self.update(**kwargs)

View File

@ -1,11 +1,11 @@
import contextlib import contextlib
import blinker import blinker
import pprint import pprint
import inspect
import copy import copy
import functools import functools
import weakref import weakref
import os import os
import typing
import ruamel.yaml import ruamel.yaml
@ -17,21 +17,62 @@ from mitmproxy.utils import typecheck
The base implementation for Options. The base implementation for Options.
""" """
unset = object()
class _DefaultsMeta(type):
def __new__(cls, name, bases, namespace, **kwds):
ret = type.__new__(cls, name, bases, dict(namespace))
defaults = {}
for klass in reversed(inspect.getmro(ret)):
for p in inspect.signature(klass.__init__).parameters.values():
if p.kind in (p.KEYWORD_ONLY, p.POSITIONAL_OR_KEYWORD):
if not p.default == p.empty:
defaults[p.name] = p.default
ret._defaults = defaults
return ret
class OptManager(metaclass=_DefaultsMeta): class _Option:
__slots__ = ("name", "typespec", "value", "_default")
def __init__(
self,
name: str,
default: typing.Any,
typespec: typing.Type
) -> None:
typecheck.check_type(name, default, typespec)
self.name = name
self._default = default
self.typespec = typespec
self.value = unset
def __repr__(self):
return "{value} [{type}]".format(value=self.current(), type=self.typespec)
@property
def default(self):
return copy.deepcopy(self._default)
def current(self) -> typing.Any:
if self.value is unset:
v = self.default
else:
v = self.value
return copy.deepcopy(v)
def set(self, value: typing.Any) -> None:
typecheck.check_type(self.name, value, self.typespec)
self.value = value
def reset(self) -> None:
self.value = unset
def has_changed(self) -> bool:
return self.value is not unset
def __eq__(self, other) -> bool:
for i in self.__slots__:
if getattr(self, i) != getattr(other, i):
return False
return True
def __deepcopy__(self, _):
o = _Option(self.name, self.default, self.typespec)
if self.has_changed():
o.value = self.current()
return o
class OptManager:
""" """
OptManager is the base class from which Options objects are derived. OptManager is the base class from which Options objects are derived.
Note that the __init__ method of all child classes must force all Note that the __init__ method of all child classes must force all
@ -45,32 +86,26 @@ class OptManager(metaclass=_DefaultsMeta):
Optmanager always returns a deep copy of options to ensure that Optmanager always returns a deep copy of options to ensure that
mutation doesn't change the option state inadvertently. mutation doesn't change the option state inadvertently.
""" """
_initialized = False
attributes = []
def __new__(cls, *args, **kwargs):
# Initialize instance._opts before __init__ is called.
# This allows us to call super().__init__() last, which then sets
# ._initialized = True as the final operation.
instance = super().__new__(cls)
instance.__dict__["_opts"] = {}
return instance
def __init__(self): def __init__(self):
self.__dict__["_options"] = {}
self.__dict__["changed"] = blinker.Signal() self.__dict__["changed"] = blinker.Signal()
self.__dict__["errored"] = blinker.Signal() self.__dict__["errored"] = blinker.Signal()
self.__dict__["_initialized"] = True
def add_option(self, name: str, default: typing.Any, typespec: typing.Type) -> None:
if name in self._options:
raise ValueError("Option %s already exists" % name)
self._options[name] = _Option(name, default, typespec)
@contextlib.contextmanager @contextlib.contextmanager
def rollback(self, updated): def rollback(self, updated):
old = self._opts.copy() old = copy.deepcopy(self._options)
try: try:
yield yield
except exceptions.OptionsError as e: except exceptions.OptionsError as e:
# Notify error handlers # Notify error handlers
self.errored.send(self, exc=e) self.errored.send(self, exc=e)
# Rollback # Rollback
self.__dict__["_opts"] = old self.__dict__["_options"] = old
self.changed.send(self, updated=updated) self.changed.send(self, updated=updated)
def subscribe(self, func, opts): def subscribe(self, func, opts):
@ -95,61 +130,48 @@ class OptManager(metaclass=_DefaultsMeta):
self.changed.connect(_call, weak=False) self.changed.connect(_call, weak=False)
def __eq__(self, other): def __eq__(self, other):
return self._opts == other._opts return self._options == other._options
def __copy__(self): def __copy__(self):
return self.__class__(**self._opts) o = OptManager()
o.__dict__["_options"] = copy.deepcopy(self._options)
return o
def __getattr__(self, attr): def __getattr__(self, attr):
if attr in self._opts: if attr in self._options:
return copy.deepcopy(self._opts[attr]) return self._options[attr].current()
else: else:
raise AttributeError("No such option: %s" % attr) raise AttributeError("No such option: %s" % attr)
def __setattr__(self, attr, value): def __setattr__(self, attr, value):
if not self._initialized:
self._typecheck(attr, value)
self._opts[attr] = value
return
self.update(**{attr: value}) self.update(**{attr: value})
def _typecheck(self, attr, value):
expected_type = typecheck.get_arg_type_from_constructor_annotation(
type(self), attr
)
if expected_type is None:
return # no type info :(
typecheck.check_type(attr, value, expected_type)
def keys(self): def keys(self):
return set(self._opts.keys()) return set(self._options.keys())
def reset(self): def reset(self):
""" """
Restore defaults for all options. Restore defaults for all options.
""" """
self.update(**self._defaults) for o in self._options.values():
o.reset()
@classmethod
def default(klass, opt):
return copy.deepcopy(klass._defaults[opt])
def update(self, **kwargs): def update(self, **kwargs):
updated = set(kwargs.keys()) updated = set(kwargs.keys())
for k, v in kwargs.items():
if k not in self._opts:
raise KeyError("No such option: %s" % k)
self._typecheck(k, v)
with self.rollback(updated): with self.rollback(updated):
self._opts.update(kwargs) for k, v in kwargs.items():
if k not in self._options:
raise KeyError("No such option: %s" % k)
self._options[k].set(v)
self.changed.send(self, updated=updated) self.changed.send(self, updated=updated)
return self
def setter(self, attr): def setter(self, attr):
""" """
Generate a setter for a given attribute. This returns a callable Generate a setter for a given attribute. This returns a callable
taking a single argument. taking a single argument.
""" """
if attr not in self._opts: if attr not in self._options:
raise KeyError("No such option: %s" % attr) raise KeyError("No such option: %s" % attr)
def setter(x): def setter(x):
@ -161,19 +183,24 @@ class OptManager(metaclass=_DefaultsMeta):
Generate a toggler for a boolean attribute. This returns a callable Generate a toggler for a boolean attribute. This returns a callable
that takes no arguments. that takes no arguments.
""" """
if attr not in self._opts: if attr not in self._options:
raise KeyError("No such option: %s" % attr) raise KeyError("No such option: %s" % attr)
o = self._options[attr]
if o.typespec != bool:
raise ValueError("Toggler can only be used with boolean options")
def toggle(): def toggle():
setattr(self, attr, not getattr(self, attr)) setattr(self, attr, not getattr(self, attr))
return toggle return toggle
def default(self, option: str) -> typing.Any:
return self._options[option].default
def has_changed(self, option): def has_changed(self, option):
""" """
Has the option changed from the default? Has the option changed from the default?
""" """
if getattr(self, option) != self._defaults[option]: return self._options[option].has_changed()
return True
def save(self, path, defaults=False): def save(self, path, defaults=False):
""" """
@ -204,7 +231,7 @@ class OptManager(metaclass=_DefaultsMeta):
if defaults or self.has_changed(k): if defaults or self.has_changed(k):
data[k] = getattr(self, k) data[k] = getattr(self, k)
for k in list(data.keys()): for k in list(data.keys()):
if k not in self._opts: if k not in self._options:
del data[k] del data[k]
return ruamel.yaml.round_trip_dump(data) return ruamel.yaml.round_trip_dump(data)
@ -268,7 +295,7 @@ class OptManager(metaclass=_DefaultsMeta):
self.update(**toset) self.update(**toset)
def __repr__(self): def __repr__(self):
options = pprint.pformat(self._opts, indent=4).strip(" {}") options = pprint.pformat(self._options, indent=4).strip(" {}")
if "\n" in options: if "\n" in options:
options = "\n " + options + "\n" options = "\n " + options + "\n"
return "{mod}.{cls}({{{options}}})".format( return "{mod}.{cls}({{{options}}})".format(

View File

@ -4,7 +4,6 @@ import mitmproxy.master
import mitmproxy.options import mitmproxy.options
from mitmproxy import proxy from mitmproxy import proxy
from mitmproxy import eventsequence from mitmproxy import eventsequence
from mitmproxy import exceptions
class RecordingMaster(mitmproxy.master.Master): class RecordingMaster(mitmproxy.master.Master):
@ -43,14 +42,6 @@ class context:
return False return False
@contextlib.contextmanager @contextlib.contextmanager
def _rollback(self, opts, updates):
old = opts._opts.copy()
try:
yield
except exceptions.OptionsError as e:
opts.__dict__["_opts"] = old
raise
def cycle(self, addon, f): def cycle(self, addon, f):
""" """
Cycles the flow through the events for the flow. Stops if a reply Cycles the flow through the events for the flow. Stops if a reply
@ -70,6 +61,5 @@ class context:
Options object with the given keyword arguments, then calls the Options object with the given keyword arguments, then calls the
configure method on the addon with the updated value. configure method on the addon with the updated value.
""" """
with self._rollback(self.options, kwargs): self.options.update(**kwargs)
self.options.update(**kwargs) addon.configure(self.options, kwargs.keys())
addon.configure(self.options, kwargs.keys())

View File

@ -1,5 +1,3 @@
from typing import Optional
from mitmproxy import controller from mitmproxy import controller
from mitmproxy import exceptions from mitmproxy import exceptions
from mitmproxy import addons from mitmproxy import addons
@ -12,26 +10,11 @@ class DumpError(Exception):
pass pass
class Options(options.Options):
def __init__(
self,
*, # all args are keyword-only.
keepserving: bool = False,
filtstr: Optional[str] = None,
flow_detail: int = 1,
**kwargs
) -> None:
self.filtstr = filtstr
self.flow_detail = flow_detail
self.keepserving = keepserving
super().__init__(**kwargs)
class DumpMaster(master.Master): class DumpMaster(master.Master):
def __init__( def __init__(
self, self,
options: Options, options: options.Options,
server, server,
with_termlog=True, with_termlog=True,
with_dumper=True, with_dumper=True,

View File

@ -1,7 +1,7 @@
import typing import typing
def check_type(attr_name: str, value: typing.Any, typeinfo: type) -> None: def check_type(name: str, value: typing.Any, typeinfo: type) -> None:
""" """
This function checks if the provided value is an instance of typeinfo This function checks if the provided value is an instance of typeinfo
and raises a TypeError otherwise. and raises a TypeError otherwise.
@ -17,7 +17,7 @@ def check_type(attr_name: str, value: typing.Any, typeinfo: type) -> None:
e = TypeError("Expected {} for {}, but got {}.".format( e = TypeError("Expected {} for {}, but got {}.".format(
typeinfo, typeinfo,
attr_name, name,
type(value) type(value)
)) ))
@ -32,7 +32,7 @@ def check_type(attr_name: str, value: typing.Any, typeinfo: type) -> None:
for T in types: for T in types:
try: try:
check_type(attr_name, value, T) check_type(name, value, T)
except TypeError: except TypeError:
pass pass
else: else:
@ -50,7 +50,7 @@ def check_type(attr_name: str, value: typing.Any, typeinfo: type) -> None:
if len(types) != len(value): if len(types) != len(value):
raise e raise e
for i, (x, T) in enumerate(zip(value, types)): for i, (x, T) in enumerate(zip(value, types)):
check_type("{}[{}]".format(attr_name, i), x, T) check_type("{}[{}]".format(name, i), x, T)
return return
elif typename.startswith("typing.Sequence"): elif typename.startswith("typing.Sequence"):
try: try:
@ -62,7 +62,7 @@ def check_type(attr_name: str, value: typing.Any, typeinfo: type) -> None:
if not isinstance(value, (tuple, list)): if not isinstance(value, (tuple, list)):
raise e raise e
for v in value: for v in value:
check_type(attr_name, v, T) check_type(name, v, T)
elif typename.startswith("typing.IO"): elif typename.startswith("typing.IO"):
if hasattr(value, "read"): if hasattr(value, "read"):
return return
@ -70,12 +70,3 @@ def check_type(attr_name: str, value: typing.Any, typeinfo: type) -> None:
raise e raise e
elif not isinstance(value, typeinfo): elif not isinstance(value, typeinfo):
raise e raise e
def get_arg_type_from_constructor_annotation(cls: type, attr: str) -> typing.Optional[type]:
"""
Returns the first type annotation for attr in the class hierarchy.
"""
for c in cls.mro():
if attr in getattr(c.__init__, "__annotations__", ()):
return c.__init__.__annotations__[attr]

View File

@ -4,12 +4,12 @@ import click
from mitmproxy.addons import dumper from mitmproxy.addons import dumper
from mitmproxy.test import tflow from mitmproxy.test import tflow
from mitmproxy.test import taddons from mitmproxy.test import taddons
from mitmproxy.tools import dump from mitmproxy.tools import options
def show(flow_detail, flows): def show(flow_detail, flows):
d = dumper.Dumper() d = dumper.Dumper()
with taddons.context(options=dump.Options()) as ctx: with taddons.context(options=options.Options()) as ctx:
ctx.configure(d, flow_detail=flow_detail) ctx.configure(d, flow_detail=flow_detail)
for f in flows: for f in flows:
ctx.cycle(d, f) ctx.cycle(d, f)

View File

@ -9,13 +9,13 @@ from mitmproxy.test import tutils
from mitmproxy.addons import dumper from mitmproxy.addons import dumper
from mitmproxy import exceptions from mitmproxy import exceptions
from mitmproxy.tools import dump
from mitmproxy import http from mitmproxy import http
from mitmproxy import options
def test_configure(): def test_configure():
d = dumper.Dumper() d = dumper.Dumper()
with taddons.context(options=dump.Options()) as ctx: with taddons.context(options=options.Options()) as ctx:
ctx.configure(d, filtstr="~b foo") ctx.configure(d, filtstr="~b foo")
assert d.filter assert d.filter
@ -34,7 +34,7 @@ def test_configure():
def test_simple(): def test_simple():
sio = io.StringIO() sio = io.StringIO()
d = dumper.Dumper(sio) d = dumper.Dumper(sio)
with taddons.context(options=dump.Options()) as ctx: with taddons.context(options=options.Options()) as ctx:
ctx.configure(d, flow_detail=0) ctx.configure(d, flow_detail=0)
d.response(tflow.tflow(resp=True)) d.response(tflow.tflow(resp=True))
assert not sio.getvalue() assert not sio.getvalue()
@ -103,7 +103,7 @@ def test_echo_body():
sio = io.StringIO() sio = io.StringIO()
d = dumper.Dumper(sio) d = dumper.Dumper(sio)
with taddons.context(options=dump.Options()) as ctx: with taddons.context(options=options.Options()) as ctx:
ctx.configure(d, flow_detail=3) ctx.configure(d, flow_detail=3)
d._echo_message(f.response) d._echo_message(f.response)
t = sio.getvalue() t = sio.getvalue()
@ -113,7 +113,7 @@ def test_echo_body():
def test_echo_request_line(): def test_echo_request_line():
sio = io.StringIO() sio = io.StringIO()
d = dumper.Dumper(sio) d = dumper.Dumper(sio)
with taddons.context(options=dump.Options()) as ctx: with taddons.context(options=options.Options()) as ctx:
ctx.configure(d, flow_detail=3, showhost=True) ctx.configure(d, flow_detail=3, showhost=True)
f = tflow.tflow(client_conn=None, server_conn=True, resp=True) f = tflow.tflow(client_conn=None, server_conn=True, resp=True)
f.request.is_replay = True f.request.is_replay = True
@ -148,7 +148,7 @@ class TestContentView:
view_auto.side_effect = exceptions.ContentViewException("") view_auto.side_effect = exceptions.ContentViewException("")
sio = io.StringIO() sio = io.StringIO()
d = dumper.Dumper(sio) d = dumper.Dumper(sio)
with taddons.context(options=dump.Options()) as ctx: with taddons.context(options=options.Options()) as ctx:
ctx.configure(d, flow_detail=4, verbosity=3) ctx.configure(d, flow_detail=4, verbosity=3)
d.response(tflow.tflow()) d.response(tflow.tflow())
assert "Content viewer failed" in ctx.master.event_log[0][1] assert "Content viewer failed" in ctx.master.event_log[0][1]
@ -157,7 +157,7 @@ class TestContentView:
def test_tcp(): def test_tcp():
sio = io.StringIO() sio = io.StringIO()
d = dumper.Dumper(sio) d = dumper.Dumper(sio)
with taddons.context(options=dump.Options()) as ctx: with taddons.context(options=options.Options()) as ctx:
ctx.configure(d, flow_detail=3, showhost=True) ctx.configure(d, flow_detail=3, showhost=True)
f = tflow.ttcpflow() f = tflow.ttcpflow()
d.tcp_message(f) d.tcp_message(f)
@ -172,7 +172,7 @@ def test_tcp():
def test_websocket(): def test_websocket():
sio = io.StringIO() sio = io.StringIO()
d = dumper.Dumper(sio) d = dumper.Dumper(sio)
with taddons.context(options=dump.Options()) as ctx: with taddons.context(options=options.Options()) as ctx:
ctx.configure(d, flow_detail=3, showhost=True) ctx.configure(d, flow_detail=3, showhost=True)
f = tflow.twebsocketflow() f = tflow.twebsocketflow()
d.websocket_message(f) d.websocket_message(f)

View File

@ -7,15 +7,9 @@ from mitmproxy.test import taddons
from mitmproxy.test import tflow from mitmproxy.test import tflow
class Options(options.Options):
def __init__(self, *, intercept=None, **kwargs):
self.intercept = intercept
super().__init__(**kwargs)
def test_simple(): def test_simple():
r = intercept.Intercept() r = intercept.Intercept()
with taddons.context(options=Options()) as tctx: with taddons.context(options=options.Options()) as tctx:
assert not r.filt assert not r.filt
tctx.configure(r, intercept="~q") tctx.configure(r, intercept="~q")
assert r.filt assert r.filt

View File

@ -7,13 +7,13 @@ from mitmproxy.test import taddons
from mitmproxy import io from mitmproxy import io
from mitmproxy import exceptions from mitmproxy import exceptions
from mitmproxy.tools import dump from mitmproxy import options
from mitmproxy.addons import streamfile from mitmproxy.addons import streamfile
def test_configure(): def test_configure():
sa = streamfile.StreamFile() sa = streamfile.StreamFile()
with taddons.context(options=dump.Options()) as tctx: with taddons.context(options=options.Options()) as tctx:
with tutils.tmpdir() as tdir: with tutils.tmpdir() as tdir:
p = os.path.join(tdir, "foo") p = os.path.join(tdir, "foo")
with pytest.raises(exceptions.OptionsError): with pytest.raises(exceptions.OptionsError):

View File

@ -3,7 +3,7 @@ import pytest
from mitmproxy.addons import termlog from mitmproxy.addons import termlog
from mitmproxy import log from mitmproxy import log
from mitmproxy.tools.dump import Options from mitmproxy.options import Options
from mitmproxy.test import taddons from mitmproxy.test import taddons

View File

@ -15,23 +15,6 @@ def tft(*, method="get", start=0):
return f return f
class Options(options.Options):
def __init__(
self,
*,
filter=None,
console_order=None,
console_order_reversed=False,
console_focus_follow=False,
**kwargs
):
self.filter = filter
self.console_order = console_order
self.console_order_reversed = console_order_reversed
self.console_focus_follow = console_focus_follow
super().__init__(**kwargs)
def test_order_refresh(): def test_order_refresh():
v = view.View() v = view.View()
sargs = [] sargs = []
@ -42,7 +25,7 @@ def test_order_refresh():
v.sig_view_refresh.connect(save) v.sig_view_refresh.connect(save)
tf = tflow.tflow(resp=True) tf = tflow.tflow(resp=True)
with taddons.context(options=Options()) as tctx: with taddons.context(options=options.Options()) as tctx:
tctx.configure(v, console_order="time") tctx.configure(v, console_order="time")
v.add(tf) v.add(tf)
tf.request.timestamp_start = 1 tf.request.timestamp_start = 1
@ -149,7 +132,7 @@ def test_filter():
def test_order(): def test_order():
v = view.View() v = view.View()
with taddons.context(options=Options()) as tctx: with taddons.context(options=options.Options()) as tctx:
v.request(tft(method="get", start=1)) v.request(tft(method="get", start=1))
v.request(tft(method="put", start=2)) v.request(tft(method="put", start=2))
v.request(tft(method="get", start=3)) v.request(tft(method="get", start=3))
@ -280,7 +263,7 @@ def test_signals():
def test_focus_follow(): def test_focus_follow():
v = view.View() v = view.View()
with taddons.context(options=Options()) as tctx: with taddons.context(options=options.Options()) as tctx:
tctx.configure(v, console_focus_follow=True, filter="~m get") tctx.configure(v, console_focus_follow=True, filter="~m get")
v.add(tft(start=5)) v.add(tft(start=5))
@ -394,7 +377,7 @@ def test_settings():
def test_configure(): def test_configure():
v = view.View() v = view.View()
with taddons.context(options=Options()) as tctx: with taddons.context(options=options.Options()) as tctx:
tctx.configure(v, filter="~q") tctx.configure(v, filter="~q")
with pytest.raises(Exception, match="Invalid interception filter"): with pytest.raises(Exception, match="Invalid interception filter"):
tctx.configure(v, filter="~~") tctx.configure(v, filter="~~")

View File

@ -1,6 +1,7 @@
import copy import copy
import os import os
import pytest import pytest
import typing
from mitmproxy import options from mitmproxy import options
from mitmproxy import optmanager from mitmproxy import optmanager
@ -9,48 +10,45 @@ from mitmproxy.test import tutils
class TO(optmanager.OptManager): class TO(optmanager.OptManager):
def __init__(self, one=None, two=None): def __init__(self):
self.one = one
self.two = two
super().__init__() super().__init__()
self.add_option("one", None, typing.Optional[int])
self.add_option("two", 2, typing.Optional[int])
self.add_option("bool", False, bool)
class TD(optmanager.OptManager): class TD(optmanager.OptManager):
def __init__(self, *, one="done", two="dtwo", three="error"): def __init__(self):
self.one = one
self.two = two
self.three = three
super().__init__() super().__init__()
self.add_option("one", "done", str)
self.add_option("two", "dtwo", str)
class TD2(TD): class TD2(TD):
def __init__(self, *, three="dthree", four="dfour", **kwargs): def __init__(self):
self.three = three super().__init__()
self.four = four self.add_option("three", "dthree", str)
super().__init__(three=three, **kwargs) self.add_option("four", "dfour", str)
class TM(optmanager.OptManager): class TM(optmanager.OptManager):
def __init__(self, one="one", two=["foo"], three=None): def __init__(self):
self.one = one
self.two = two
self.three = three
super().__init__() super().__init__()
self.add_option("two", ["foo"], typing.Sequence[str])
self.add_option("one", None, typing.Optional[str])
def test_defaults(): def test_defaults():
assert TD2.default("one") == "done"
assert TD2.default("two") == "dtwo"
assert TD2.default("three") == "dthree"
assert TD2.default("four") == "dfour"
o = TD2() o = TD2()
assert o._defaults == { defaults = {
"one": "done", "one": "done",
"two": "dtwo", "two": "dtwo",
"three": "dthree", "three": "dthree",
"four": "dfour", "four": "dfour",
} }
for k, v in defaults.items():
assert o.default(k) == v
assert not o.has_changed("one") assert not o.has_changed("one")
newvals = dict( newvals = dict(
one="xone", one="xone",
@ -64,18 +62,19 @@ def test_defaults():
assert v == getattr(o, k) assert v == getattr(o, k)
o.reset() o.reset()
assert not o.has_changed("one") assert not o.has_changed("one")
for k, v in o._defaults.items():
assert v == getattr(o, k) for k in o.keys():
assert not o.has_changed(k)
def test_options(): def test_options():
o = TO(two="three") o = TO()
assert o.keys() == set(["one", "two"]) assert o.keys() == set(["bool", "one", "two"])
assert o.one is None assert o.one is None
assert o.two == "three" assert o.two == 2
o.one = "one" o.one = 1
assert o.one == "one" assert o.one == 1
with pytest.raises(TypeError): with pytest.raises(TypeError):
TO(nonexistent = "value") TO(nonexistent = "value")
@ -91,34 +90,38 @@ def test_options():
o.changed.connect(sub) o.changed.connect(sub)
o.one = "ninety" o.one = 90
assert len(rec) == 1 assert len(rec) == 1
assert rec[-1].one == "ninety" assert rec[-1].one == 90
o.update(one="oink") o.update(one=3)
assert len(rec) == 2 assert len(rec) == 2
assert rec[-1].one == "oink" assert rec[-1].one == 3
def test_setter(): def test_setter():
o = TO(two="three") o = TO()
f = o.setter("two") f = o.setter("two")
f("xxx") f(99)
assert o.two == "xxx" assert o.two == 99
with pytest.raises(Exception, match="No such option"): with pytest.raises(Exception, match="No such option"):
o.setter("nonexistent") o.setter("nonexistent")
def test_toggler(): def test_toggler():
o = TO(two=True) o = TO()
f = o.toggler("two") f = o.toggler("bool")
assert o.bool is False
f() f()
assert o.two is False assert o.bool is True
f() f()
assert o.two is True assert o.bool is False
with pytest.raises(Exception, match="No such option"): with pytest.raises(Exception, match="No such option"):
o.toggler("nonexistent") o.toggler("nonexistent")
with pytest.raises(Exception, match="boolean options"):
o.toggler("one")
class Rec(): class Rec():
def __init__(self): def __init__(self):
@ -132,19 +135,19 @@ def test_subscribe():
o = TO() o = TO()
r = Rec() r = Rec()
o.subscribe(r, ["two"]) o.subscribe(r, ["two"])
o.one = "foo" o.one = 2
assert not r.called assert not r.called
o.two = "foo" o.two = 3
assert r.called assert r.called
assert len(o.changed.receivers) == 1 assert len(o.changed.receivers) == 1
del r del r
o.two = "bar" o.two = 4
assert len(o.changed.receivers) == 0 assert len(o.changed.receivers) == 0
def test_rollback(): def test_rollback():
o = TO(one="two") o = TO()
rec = [] rec = []
@ -157,27 +160,24 @@ def test_rollback():
recerr.append(kwargs) recerr.append(kwargs)
def err(opts, updated): def err(opts, updated):
if opts.one == "ten": if opts.one == 10:
raise exceptions.OptionsError() raise exceptions.OptionsError()
o.changed.connect(sub) o.changed.connect(sub)
o.changed.connect(err) o.changed.connect(err)
o.errored.connect(errsub) o.errored.connect(errsub)
o.one = "ten" assert o.one is None
o.one = 10
assert isinstance(recerr[0]["exc"], exceptions.OptionsError) assert isinstance(recerr[0]["exc"], exceptions.OptionsError)
assert o.one == "two" assert o.one is None
assert len(rec) == 2 assert len(rec) == 2
assert rec[0].one == "ten" assert rec[0].one == 10
assert rec[1].one == "two" assert rec[1].one is None
def test_repr(): def test_repr():
assert repr(TO()) == "test.mitmproxy.test_optmanager.TO({'one': None, 'two': None})" assert repr(TO())
assert repr(TO(one='x' * 60)) == """test.mitmproxy.test_optmanager.TO({
'one': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
'two': None
})"""
def test_serialize(): def test_serialize():
@ -249,3 +249,17 @@ def test_merge():
assert m.one == "two" assert m.one == "two"
m.merge(dict(two=["bar"])) m.merge(dict(two=["bar"]))
assert m.two == ["foo", "bar"] assert m.two == ["foo", "bar"]
def test_option():
o = optmanager._Option("test", 1, int)
assert o.current() == 1
with pytest.raises(TypeError):
o.set("foo")
with pytest.raises(TypeError):
optmanager._Option("test", 1, str)
o2 = optmanager._Option("test", 1, int)
assert o2 == o
o2.set(5)
assert o2 != o

View File

@ -5,6 +5,7 @@ from unittest import mock
from mitmproxy import proxy from mitmproxy import proxy
from mitmproxy import log from mitmproxy import log
from mitmproxy import controller from mitmproxy import controller
from mitmproxy import options
from mitmproxy.tools import dump from mitmproxy.tools import dump
from mitmproxy.test import tutils from mitmproxy.test import tutils
@ -12,8 +13,8 @@ from .. import tservers
class TestDumpMaster(tservers.MasterTest): class TestDumpMaster(tservers.MasterTest):
def mkmaster(self, flt, **options): def mkmaster(self, flt, **opts):
o = dump.Options(filtstr=flt, verbosity=-1, flow_detail=0, **options) o = options.Options(filtstr=flt, verbosity=-1, flow_detail=0, **opts)
m = dump.DumpMaster(o, proxy.DummyServer(), with_termlog=False, with_dumper=False) m = dump.DumpMaster(o, proxy.DummyServer(), with_termlog=False, with_dumper=False)
return m return m
@ -40,13 +41,13 @@ class TestDumpMaster(tservers.MasterTest):
@pytest.mark.parametrize("termlog", [False, True]) @pytest.mark.parametrize("termlog", [False, True])
def test_addons_termlog(self, termlog): def test_addons_termlog(self, termlog):
with mock.patch('sys.stdout'): with mock.patch('sys.stdout'):
o = dump.Options() o = options.Options()
m = dump.DumpMaster(o, proxy.DummyServer(), with_termlog=termlog) m = dump.DumpMaster(o, proxy.DummyServer(), with_termlog=termlog)
assert (m.addons.get('termlog') is not None) == termlog assert (m.addons.get('termlog') is not None) == termlog
@pytest.mark.parametrize("dumper", [False, True]) @pytest.mark.parametrize("dumper", [False, True])
def test_addons_dumper(self, dumper): def test_addons_dumper(self, dumper):
with mock.patch('sys.stdout'): with mock.patch('sys.stdout'):
o = dump.Options() o = options.Options()
m = dump.DumpMaster(o, proxy.DummyServer(), with_dumper=dumper) m = dump.DumpMaster(o, proxy.DummyServer(), with_dumper=dumper)
assert (m.addons.get('dumper') is not None) == dumper assert (m.addons.get('dumper') is not None) == dumper

View File

@ -16,12 +16,6 @@ class T(TBase):
super(T, self).__init__(42) super(T, self).__init__(42)
def test_get_arg_type_from_constructor_annotation():
assert typecheck.get_arg_type_from_constructor_annotation(T, "foo") == str
assert typecheck.get_arg_type_from_constructor_annotation(T, "bar") == int
assert not typecheck.get_arg_type_from_constructor_annotation(T, "baz")
def test_check_type(): def test_check_type():
typecheck.check_type("foo", 42, int) typecheck.check_type("foo", 42, int)
with pytest.raises(TypeError): with pytest.raises(TypeError):