From 93c49c47ae42a89c3c6b30988ea553df4ff19ab4 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 24 Feb 2018 16:42:32 +1300 Subject: [PATCH 1/4] flow_detail option to /addons/dumper --- mitmproxy/addons/dumper.py | 12 ++++++++++++ mitmproxy/options.py | 13 ------------- test/mitmproxy/addons/test_dumper.py | 14 +++++++------- test/mitmproxy/tools/test_dump.py | 2 +- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/mitmproxy/addons/dumper.py b/mitmproxy/addons/dumper.py index 48bc81187..8cfacde69 100644 --- a/mitmproxy/addons/dumper.py +++ b/mitmproxy/addons/dumper.py @@ -31,6 +31,18 @@ class Dumper: self.filter = None # type: flowfilter.TFilter self.outfp = outfile # type: typing.io.TextIO + def load(self, loader): + loader.add_option( + "flow_detail", int, 1, + """ + The display detail level for flows in mitmdump: 0 (almost quiet) to 3 (very verbose). + 0: shortened request URL, response status code, WebSocket and TCP message notifications. + 1: full request URL with response status code + 2: 1 + HTTP headers + 3: 2 + full response content, content of WebSocket and TCP messages. + """ + ) + def configure(self, updated): if "view_filter" in updated: if ctx.options.view_filter: diff --git a/mitmproxy/options.py b/mitmproxy/options.py index 70d454fd3..139b197f4 100644 --- a/mitmproxy/options.py +++ b/mitmproxy/options.py @@ -58,7 +58,6 @@ class Options(optmanager.OptManager): # because they're used by more than one addon, or because they're # embedded in the core code somehow. default_contentview = None # type: str - flow_detail = None # type: int intercept = None # type: Optional[str] intercept_active = None # type: bool proxyauth = None # type: Optional[str] @@ -267,16 +266,4 @@ class Options(optmanager.OptManager): "Limit which flows are displayed." ) - # Dump options - self.add_option( - "flow_detail", int, 1, - """ - The display detail level for flows in mitmdump: 0 (almost quiet) to 3 (very verbose). - 0: shortened request URL, response status code, WebSocket and TCP message notifications. - 1: full request URL with response status code - 2: 1 + HTTP headers - 3: 2 + full response content, content of WebSocket and TCP messages. - """ - ) - self.update(**kwargs) diff --git a/test/mitmproxy/addons/test_dumper.py b/test/mitmproxy/addons/test_dumper.py index 9774e1316..ead6b7e72 100644 --- a/test/mitmproxy/addons/test_dumper.py +++ b/test/mitmproxy/addons/test_dumper.py @@ -14,7 +14,7 @@ from mitmproxy import http def test_configure(): d = dumper.Dumper() - with taddons.context() as ctx: + with taddons.context(d) as ctx: ctx.configure(d, view_filter="~b foo") assert d.filter @@ -33,7 +33,7 @@ def test_configure(): def test_simple(): sio = io.StringIO() d = dumper.Dumper(sio) - with taddons.context() as ctx: + with taddons.context(d) as ctx: ctx.configure(d, flow_detail=0) d.response(tflow.tflow(resp=True)) assert not sio.getvalue() @@ -101,7 +101,7 @@ def test_echo_body(): sio = io.StringIO() d = dumper.Dumper(sio) - with taddons.context() as ctx: + with taddons.context(d) as ctx: ctx.configure(d, flow_detail=3) d._echo_message(f.response) t = sio.getvalue() @@ -111,7 +111,7 @@ def test_echo_body(): def test_echo_request_line(): sio = io.StringIO() d = dumper.Dumper(sio) - with taddons.context() as ctx: + with taddons.context(d) as ctx: ctx.configure(d, flow_detail=3, showhost=True) f = tflow.tflow(client_conn=None, server_conn=True, resp=True) f.request.is_replay = True @@ -146,7 +146,7 @@ class TestContentView: view_auto.side_effect = exceptions.ContentViewException("") sio = io.StringIO() d = dumper.Dumper(sio) - with taddons.context() as ctx: + with taddons.context(d) as ctx: ctx.configure(d, flow_detail=4, verbosity='debug') d.response(tflow.tflow()) assert ctx.master.has_log("content viewer failed") @@ -155,7 +155,7 @@ class TestContentView: def test_tcp(): sio = io.StringIO() d = dumper.Dumper(sio) - with taddons.context() as ctx: + with taddons.context(d) as ctx: ctx.configure(d, flow_detail=3, showhost=True) f = tflow.ttcpflow() d.tcp_message(f) @@ -170,7 +170,7 @@ def test_tcp(): def test_websocket(): sio = io.StringIO() d = dumper.Dumper(sio) - with taddons.context() as ctx: + with taddons.context(d) as ctx: ctx.configure(d, flow_detail=3, showhost=True) f = tflow.twebsocketflow() d.websocket_message(f) diff --git a/test/mitmproxy/tools/test_dump.py b/test/mitmproxy/tools/test_dump.py index 952c3f4fd..f303c8088 100644 --- a/test/mitmproxy/tools/test_dump.py +++ b/test/mitmproxy/tools/test_dump.py @@ -11,7 +11,7 @@ from .. import tservers class TestDumpMaster(tservers.MasterTest): def mkmaster(self, flt, **opts): - o = options.Options(view_filter=flt, verbosity='error', flow_detail=0, **opts) + o = options.Options(view_filter=flt, verbosity='error', **opts) m = dump.DumpMaster(o, with_termlog=False, with_dumper=False) return m From f49aae312fb0df3c4dd484c6790c8a558fe540e8 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 24 Feb 2018 17:19:42 +1300 Subject: [PATCH 2/4] Move intercept options to addon As part of this, we zap an un-needed console command and use a boolean toggle setter instead. Also remove an ancient regression test that breaks encapsulation. --- mitmproxy/addons/intercept.py | 13 +++++++++++++ mitmproxy/options.py | 12 ------------ mitmproxy/tools/console/consoleaddons.py | 9 --------- mitmproxy/tools/console/defaultkeys.py | 2 +- test/mitmproxy/addons/test_intercept.py | 2 +- test/mitmproxy/tools/console/test_master.py | 15 --------------- 6 files changed, 15 insertions(+), 38 deletions(-) diff --git a/mitmproxy/addons/intercept.py b/mitmproxy/addons/intercept.py index 9e1a283eb..d39d19622 100644 --- a/mitmproxy/addons/intercept.py +++ b/mitmproxy/addons/intercept.py @@ -1,3 +1,5 @@ +import typing + from mitmproxy import flowfilter from mitmproxy import exceptions from mitmproxy import ctx @@ -7,6 +9,17 @@ class Intercept: def __init__(self): self.filt = None + def load(self, loader): + loader.add_option( + "intercept_active", bool, False, + "Intercept toggle" + ) + + loader.add_option( + "intercept", typing.Optional[str], None, + "Intercept filter expression." + ) + def configure(self, updated): if "intercept" in updated: if not ctx.options.intercept: diff --git a/mitmproxy/options.py b/mitmproxy/options.py index 139b197f4..05332b3fb 100644 --- a/mitmproxy/options.py +++ b/mitmproxy/options.py @@ -58,8 +58,6 @@ class Options(optmanager.OptManager): # because they're used by more than one addon, or because they're # embedded in the core code somehow. default_contentview = None # type: str - intercept = None # type: Optional[str] - intercept_active = None # type: bool proxyauth = None # type: Optional[str] showhost = None # type: bool verbosity = None # type: str @@ -251,16 +249,6 @@ class Options(optmanager.OptManager): """ ) - self.add_option( - "intercept_active", bool, False, - "Intercept toggle" - ) - - self.add_option( - "intercept", Optional[str], None, - "Intercept filter expression." - ) - self.add_option( "view_filter", Optional[str], None, "Limit which flows are displayed." diff --git a/mitmproxy/tools/console/consoleaddons.py b/mitmproxy/tools/console/consoleaddons.py index c73eda42e..9856e80f5 100644 --- a/mitmproxy/tools/console/consoleaddons.py +++ b/mitmproxy/tools/console/consoleaddons.py @@ -110,15 +110,6 @@ class ConsoleAddon: """ return ["single", "vertical", "horizontal"] - @command.command("console.intercept.toggle") - def intercept_toggle(self) -> None: - """ - Toggles interception on/off leaving intercept filters intact. - """ - ctx.options.update( - intercept_active = not ctx.options.intercept_active - ) - @command.command("console.layout.cycle") def layout_cycle(self) -> None: """ diff --git a/mitmproxy/tools/console/defaultkeys.py b/mitmproxy/tools/console/defaultkeys.py index c78762882..7f65c1f7d 100644 --- a/mitmproxy/tools/console/defaultkeys.py +++ b/mitmproxy/tools/console/defaultkeys.py @@ -26,7 +26,7 @@ def map(km): km.add("ctrl f", "console.nav.pagedown", ["global"], "Page down") km.add("ctrl b", "console.nav.pageup", ["global"], "Page up") - km.add("I", "console.intercept.toggle", ["global"], "Toggle intercept") + km.add("I", "set intercept_active=toggle", ["global"], "Toggle intercept") km.add("i", "console.command.set intercept", ["global"], "Set intercept") km.add("W", "console.command.set save_stream_file", ["global"], "Stream to file") km.add("A", "flow.resume @all", ["flowlist", "flowview"], "Resume all intercepted flows") diff --git a/test/mitmproxy/addons/test_intercept.py b/test/mitmproxy/addons/test_intercept.py index d95981010..b3d24626c 100644 --- a/test/mitmproxy/addons/test_intercept.py +++ b/test/mitmproxy/addons/test_intercept.py @@ -8,7 +8,7 @@ from mitmproxy.test import tflow def test_simple(): r = intercept.Intercept() - with taddons.context() as tctx: + with taddons.context(r) as tctx: assert not r.filt tctx.configure(r, intercept="~q") assert r.filt diff --git a/test/mitmproxy/tools/console/test_master.py b/test/mitmproxy/tools/console/test_master.py index 6ea61991d..5be035e88 100644 --- a/test/mitmproxy/tools/console/test_master.py +++ b/test/mitmproxy/tools/console/test_master.py @@ -1,8 +1,6 @@ import urwid from mitmproxy import options -from mitmproxy.test import tflow -from mitmproxy.test import tutils from mitmproxy.tools import console from ... import tservers @@ -24,16 +22,3 @@ class TestMaster(tservers.MasterTest): except urwid.ExitMainLoop: pass assert len(m.view) == i - - def test_intercept(self): - """regression test for https://github.com/mitmproxy/mitmproxy/issues/1605""" - m = self.mkmaster(intercept="~b bar") - f = tflow.tflow(req=tutils.treq(content=b"foo")) - m.addons.handle_lifecycle("request", f) - assert not m.view[0].intercepted - f = tflow.tflow(req=tutils.treq(content=b"bar")) - m.addons.handle_lifecycle("request", f) - assert m.view[1].intercepted - f = tflow.tflow(resp=tutils.tresp(content=b"bar")) - m.addons.handle_lifecycle("request", f) - assert m.view[2].intercepted From 59db25bd0ff2faaab7ba982cf51d472e108612f6 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 24 Feb 2018 19:09:32 +1300 Subject: [PATCH 3/4] Split default_contentview into dumper_* and console_* varieties Then move the new options into their respective addons. This was a shared option, and things are much much simpler if it isn't. --- mitmproxy/addons/dumper.py | 7 ++++++- mitmproxy/options.py | 7 ------- mitmproxy/tools/console/consoleaddons.py | 7 ++++++- mitmproxy/tools/console/statusbar.py | 4 ++-- test/mitmproxy/tools/console/test_statusbar.py | 2 +- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/mitmproxy/addons/dumper.py b/mitmproxy/addons/dumper.py index 8cfacde69..0df2d3395 100644 --- a/mitmproxy/addons/dumper.py +++ b/mitmproxy/addons/dumper.py @@ -42,6 +42,11 @@ class Dumper: 3: 2 + full response content, content of WebSocket and TCP messages. """ ) + loader.add_option( + "dumper_default_contentview", str, "auto", + "The default content view mode.", + choices = [i.name.lower() for i in contentviews.views] + ) def configure(self, updated): if "view_filter" in updated: @@ -73,7 +78,7 @@ class Dumper: def _echo_message(self, message): _, lines, error = contentviews.get_message_content_view( - ctx.options.default_contentview, + ctx.options.dumper_default_contentview, message ) if error: diff --git a/mitmproxy/options.py b/mitmproxy/options.py index 05332b3fb..39fbff8fc 100644 --- a/mitmproxy/options.py +++ b/mitmproxy/options.py @@ -1,7 +1,6 @@ from typing import Optional, Sequence from mitmproxy import optmanager -from mitmproxy import contentviews from mitmproxy.net import tls log_verbosity = [ @@ -57,7 +56,6 @@ class Options(optmanager.OptManager): # FIXME: Options that must be migrated to addons, but are complicated # because they're used by more than one addon, or because they're # embedded in the core code somehow. - default_contentview = None # type: str proxyauth = None # type: Optional[str] showhost = None # type: bool verbosity = None # type: str @@ -78,11 +76,6 @@ class Options(optmanager.OptManager): "Log verbosity.", choices=log_verbosity ) - self.add_option( - "default_contentview", str, "auto", - "The default content view mode.", - choices = [i.name.lower() for i in contentviews.views] - ) # Proxy options self.add_option( diff --git a/mitmproxy/tools/console/consoleaddons.py b/mitmproxy/tools/console/consoleaddons.py index 9856e80f5..939757d7a 100644 --- a/mitmproxy/tools/console/consoleaddons.py +++ b/mitmproxy/tools/console/consoleaddons.py @@ -76,6 +76,11 @@ class ConsoleAddon: self.started = False def load(self, loader): + loader.add_option( + "console_default_contentview", str, "auto", + "The default content view mode.", + choices = [i.name.lower() for i in contentviews.views] + ) loader.add_option( "console_layout", str, "single", "Console layout.", @@ -531,7 +536,7 @@ class ConsoleAddon: [ "@focus", "flowview_mode_%s" % idx, - self.master.options.default_contentview, + self.master.options.console_default_contentview, ] ) diff --git a/mitmproxy/tools/console/statusbar.py b/mitmproxy/tools/console/statusbar.py index bdb390136..8553a66ff 100644 --- a/mitmproxy/tools/console/statusbar.py +++ b/mitmproxy/tools/console/statusbar.py @@ -197,10 +197,10 @@ class StatusBar(urwid.WidgetWrap): r.append("[") r.append(("heading_key", "u")) r.append(":%s]" % self.master.options.stickyauth) - if self.master.options.default_contentview != "auto": + if self.master.options.console_default_contentview != "auto": r.append("[") r.append(("heading_key", "M")) - r.append(":%s]" % self.master.options.default_contentview) + r.append(":%s]" % self.master.options.console_default_contentview) if self.master.options.has_changed("view_order"): r.append("[") r.append(("heading_key", "o")) diff --git a/test/mitmproxy/tools/console/test_statusbar.py b/test/mitmproxy/tools/console/test_statusbar.py index ac17c5c08..db8a63a7d 100644 --- a/test/mitmproxy/tools/console/test_statusbar.py +++ b/test/mitmproxy/tools/console/test_statusbar.py @@ -14,7 +14,7 @@ def test_statusbar(monkeypatch): view_filter="~dst example.com", stickycookie="~dst example.com", stickyauth="~dst example.com", - default_contentview="javascript", + console_default_contentview="javascript", anticache=True, anticomp=True, showhost=True, From 6ea81a96d49c06e2877b727ee7c2d350d3e1a167 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 24 Feb 2018 19:16:13 +1300 Subject: [PATCH 4/4] addon options: proxyauth to /addons --- mitmproxy/addons/proxyauth.py | 12 ++++++++++++ mitmproxy/options.py | 11 ----------- test/mitmproxy/addons/test_allowremote.py | 5 +++-- test/mitmproxy/addons/test_proxyauth.py | 10 +++++----- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/mitmproxy/addons/proxyauth.py b/mitmproxy/addons/proxyauth.py index dc99d5cc3..c0a3f83a3 100644 --- a/mitmproxy/addons/proxyauth.py +++ b/mitmproxy/addons/proxyauth.py @@ -52,6 +52,18 @@ class ProxyAuth: self.authenticated = weakref.WeakKeyDictionary() # type: MutableMapping[connections.ClientConnection, Tuple[str, str]] """Contains all connections that are permanently authenticated after an HTTP CONNECT""" + def load(self, loader): + loader.add_option( + "proxyauth", Optional[str], None, + """ + Require proxy authentication. Format: + "username:pass", + "any" to accept any user/pass combination, + "@path" to use an Apache htpasswd file, + or "ldap[s]:url_server_ldap:dn_auth:password:dn_subtree" for LDAP authentication. + """ + ) + def enabled(self) -> bool: return any([self.nonanonymous, self.htpasswd, self.singleuser, self.ldapconn, self.ldapserver]) diff --git a/mitmproxy/options.py b/mitmproxy/options.py index 39fbff8fc..7a00bb879 100644 --- a/mitmproxy/options.py +++ b/mitmproxy/options.py @@ -56,7 +56,6 @@ class Options(optmanager.OptManager): # FIXME: Options that must be migrated to addons, but are complicated # because they're used by more than one addon, or because they're # embedded in the core code somehow. - proxyauth = None # type: Optional[str] showhost = None # type: bool verbosity = None # type: str view_filter = None # type: Optional[str] @@ -78,16 +77,6 @@ class Options(optmanager.OptManager): ) # Proxy options - self.add_option( - "proxyauth", Optional[str], None, - """ - Require proxy authentication. Format: - "username:pass", - "any" to accept any user/pass combination, - "@path" to use an Apache htpasswd file, - or "ldap[s]:url_server_ldap:dn_auth:password:dn_subtree" for LDAP authentication. - """ - ) self.add_option( "add_upstream_certs_to_client_chain", bool, False, """ diff --git a/test/mitmproxy/addons/test_allowremote.py b/test/mitmproxy/addons/test_allowremote.py index 52dae68d4..69019726f 100644 --- a/test/mitmproxy/addons/test_allowremote.py +++ b/test/mitmproxy/addons/test_allowremote.py @@ -1,7 +1,7 @@ from unittest import mock import pytest -from mitmproxy.addons import allowremote +from mitmproxy.addons import allowremote, proxyauth from mitmproxy.test import taddons @@ -19,7 +19,8 @@ from mitmproxy.test import taddons ]) def test_allowremote(allow_remote, ip, should_be_killed): ar = allowremote.AllowRemote() - with taddons.context(ar) as tctx: + up = proxyauth.ProxyAuth() + with taddons.context(ar, up) as tctx: tctx.options.allow_remote = allow_remote with mock.patch('mitmproxy.proxy.protocol.base.Layer') as layer: diff --git a/test/mitmproxy/addons/test_proxyauth.py b/test/mitmproxy/addons/test_proxyauth.py index 97259d1c9..9e2365cfc 100644 --- a/test/mitmproxy/addons/test_proxyauth.py +++ b/test/mitmproxy/addons/test_proxyauth.py @@ -49,7 +49,7 @@ class TestProxyAuth: ]) def test_is_proxy_auth(self, mode, expected): up = proxyauth.ProxyAuth() - with taddons.context() as ctx: + with taddons.context(up) as ctx: ctx.options.mode = mode assert up.is_proxy_auth() is expected @@ -75,7 +75,7 @@ class TestProxyAuth: def test_check(self): up = proxyauth.ProxyAuth() - with taddons.context() as ctx: + with taddons.context(up) as ctx: ctx.configure(up, proxyauth="any", mode="regular") f = tflow.tflow() assert not up.check(f) @@ -133,7 +133,7 @@ class TestProxyAuth: def test_authenticate(self): up = proxyauth.ProxyAuth() - with taddons.context() as ctx: + with taddons.context(up) as ctx: ctx.configure(up, proxyauth="any", mode="regular") f = tflow.tflow() @@ -165,7 +165,7 @@ class TestProxyAuth: def test_configure(self): up = proxyauth.ProxyAuth() - with taddons.context() as ctx: + with taddons.context(up) as ctx: with pytest.raises(exceptions.OptionsError): ctx.configure(up, proxyauth="foo") @@ -223,7 +223,7 @@ class TestProxyAuth: def test_handlers(self): up = proxyauth.ProxyAuth() - with taddons.context() as ctx: + with taddons.context(up) as ctx: ctx.configure(up, proxyauth="any", mode="regular") f = tflow.tflow()