diff options
Diffstat (limited to 'test')
117 files changed, 2846 insertions, 1034 deletions
diff --git a/test/conftest.py b/test/conftest.py index bb913548..27918cf9 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,15 +1,10 @@ import os -import pytest -import OpenSSL +import socket -import mitmproxy.net.tcp +import pytest pytest_plugins = ('test.full_coverage_plugin',) -requires_alpn = pytest.mark.skipif( - not mitmproxy.net.tcp.HAS_ALPN, - reason='requires OpenSSL with ALPN support') - skip_windows = pytest.mark.skipif( os.name == "nt", reason='Skipping due to Windows' @@ -25,8 +20,16 @@ skip_appveyor = pytest.mark.skipif( reason='Skipping due to Appveyor' ) - -@pytest.fixture() -def disable_alpn(monkeypatch): - monkeypatch.setattr(mitmproxy.net.tcp, 'HAS_ALPN', False) - monkeypatch.setattr(OpenSSL.SSL._lib, 'Cryptography_HAS_ALPN', False) +try: + s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + s.bind(("::1", 0)) + s.close() +except OSError: + no_ipv6 = True +else: + no_ipv6 = False + +skip_no_ipv6 = pytest.mark.skipif( + no_ipv6, + reason='Host has no IPv6 support' +) diff --git a/test/examples/test_xss_scanner.py b/test/examples/test_xss_scanner.py index 14ee6902..e15d7e10 100644 --- a/test/examples/test_xss_scanner.py +++ b/test/examples/test_xss_scanner.py @@ -314,7 +314,13 @@ class TestXSSScanner(): assert logger.args == [] xss.find_unclaimed_URLs("<html><script src=\"http://unclaimedDomainName.com\"></script></html>", "https://example.com") - assert logger.args[0] == 'XSS found in https://example.com due to unclaimed URL "http://unclaimedDomainName.com" in script tag.' + assert logger.args[0] == 'XSS found in https://example.com due to unclaimed URL "http://unclaimedDomainName.com".' + xss.find_unclaimed_URLs("<html><iframe src=\"http://unclaimedDomainName.com\"></iframe></html>", + "https://example.com") + assert logger.args[0] == 'XSS found in https://example.com due to unclaimed URL "http://unclaimedDomainName.com".' + xss.find_unclaimed_URLs("<html><link rel=\"stylesheet\" href=\"http://unclaimedDomainName.com\"></html>", + "https://example.com") + assert logger.args[0] == 'XSS found in https://example.com due to unclaimed URL "http://unclaimedDomainName.com".' def test_log_XSS_data(self, monkeypatch, logger): logger.args = [] diff --git a/test/filename_matching.py b/test/filename_matching.py index 51cedf03..e74848d4 100644 --- a/test/filename_matching.py +++ b/test/filename_matching.py @@ -22,7 +22,7 @@ def check_src_files_have_test(): def check_test_files_have_src(): unknown_test_files = [] - excluded = ['test/mitmproxy/data/', 'test/mitmproxy/net/data/', '/tservers.py'] + excluded = ['test/mitmproxy/data/', 'test/mitmproxy/net/data/', '/tservers.py', '/conftest.py'] test_files = glob.glob('test/mitmproxy/**/*.py', recursive=True) + glob.glob('test/pathod/**/*.py', recursive=True) test_files = [f for f in test_files if os.path.basename(f) != '__init__.py'] test_files = [f for f in test_files if not any(os.path.normpath(p) in f for p in excluded)] 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_allowremote.py b/test/mitmproxy/addons/test_allowremote.py new file mode 100644 index 00000000..9fc71525 --- /dev/null +++ b/test/mitmproxy/addons/test_allowremote.py @@ -0,0 +1,38 @@ +from unittest import mock +import pytest + +from mitmproxy.addons import allowremote +from mitmproxy.test import taddons + + +@pytest.mark.parametrize("allow_remote, ip, should_be_killed", [ + (True, "192.168.1.3", False), + (True, "122.176.243.101", False), + (False, "192.168.1.3", False), + (False, "122.176.243.101", True), + (True, "::ffff:1:2", False), + (True, "fe80::", False), + (True, "2001:4860:4860::8888", False), + (False, "::ffff:1:2", False), + (False, "fe80::", False), + (False, "2001:4860:4860::8888", True), +]) +def test_allowremote(allow_remote, ip, should_be_killed): + ar = allowremote.AllowRemote() + with taddons.context() as tctx: + tctx.master.addons.register(ar) + tctx.options.allow_remote = allow_remote + + with mock.patch('mitmproxy.proxy.protocol.base.Layer') as layer: + layer.client_conn.address = (ip, 12345) + + ar.clientconnect(layer) + if should_be_killed: + assert tctx.master.has_log("Client connection was killed", "warn") + else: + assert tctx.master.logs == [] + tctx.master.clear() + + tctx.options.proxyauth = "any" + ar.clientconnect(layer) + assert tctx.master.logs == [] diff --git a/test/mitmproxy/addons/test_browser.py b/test/mitmproxy/addons/test_browser.py new file mode 100644 index 00000000..407a3fe6 --- /dev/null +++ b/test/mitmproxy/addons/test_browser.py @@ -0,0 +1,31 @@ +from unittest import mock + +from mitmproxy.addons import browser +from mitmproxy.test import taddons + + +def test_browser(): + with mock.patch("subprocess.Popen") as po, mock.patch("shutil.which") as which: + which.return_value = "chrome" + b = browser.Browser() + with taddons.context() as tctx: + b.start() + assert po.called + b.start() + + assert not tctx.master.has_log("already running") + b.browser.poll = lambda: None + b.start() + assert tctx.master.has_log("already running") + b.done() + assert not b.browser + + +def test_no_browser(): + with mock.patch("shutil.which") as which: + which.return_value = False + + b = browser.Browser() + with taddons.context() as tctx: + b.start() + assert tctx.master.has_log("platform is not supported") diff --git a/test/mitmproxy/addons/test_check_alpn.py b/test/mitmproxy/addons/test_check_alpn.py deleted file mode 100644 index 2b1d6058..00000000 --- a/test/mitmproxy/addons/test_check_alpn.py +++ /dev/null @@ -1,23 +0,0 @@ -from mitmproxy.addons import check_alpn -from mitmproxy.test import taddons -from ...conftest import requires_alpn - - -class TestCheckALPN: - - @requires_alpn - def test_check_alpn(self): - msg = 'ALPN support missing' - - with taddons.context() as tctx: - a = check_alpn.CheckALPN() - tctx.configure(a) - assert not tctx.master.has_log(msg) - - def test_check_no_alpn(self, disable_alpn): - msg = 'ALPN support missing' - - with taddons.context() as tctx: - a = check_alpn.CheckALPN() - tctx.configure(a) - assert tctx.master.has_log(msg) diff --git a/test/mitmproxy/addons/test_clientplayback.py b/test/mitmproxy/addons/test_clientplayback.py index 6089b2d5..3f990668 100644 --- a/test/mitmproxy/addons/test_clientplayback.py +++ b/test/mitmproxy/addons/test_clientplayback.py @@ -36,9 +36,12 @@ class TestClientPlayback: assert rp.called assert cp.current_thread - cp.flows = None - cp.current_thread = None + cp.flows = [] + cp.current_thread.is_alive.return_value = False + assert cp.count() == 1 cp.tick() + assert cp.count() == 0 + assert tctx.master.has_event("update") assert tctx.master.has_event("processing_complete") cp.current_thread = MockThread() @@ -49,6 +52,10 @@ class TestClientPlayback: cp.stop_replay() assert not cp.flows + df = tflow.DummyFlow(tflow.tclient_conn(), tflow.tserver_conn(), True) + with pytest.raises(exceptions.CommandError, match="Can't replay live flow."): + cp.start_replay([df]) + def test_load_file(self, tmpdir): cp = clientplayback.ClientPlayback() with taddons.context(): diff --git a/test/mitmproxy/addons/test_core.py b/test/mitmproxy/addons/test_core.py index c132d80a..5aa4ef37 100644 --- a/test/mitmproxy/addons/test_core.py +++ b/test/mitmproxy/addons/test_core.py @@ -69,9 +69,6 @@ def test_flow_set(): f = tflow.tflow(resp=True) assert sa.flow_set_options() - with pytest.raises(exceptions.CommandError): - sa.flow_set([f], "flibble", "post") - assert f.request.method != "post" sa.flow_set([f], "method", "post") assert f.request.method == "POST" @@ -126,9 +123,6 @@ def test_encoding(): sa.encode_toggle([f], "request") assert "content-encoding" not in f.request.headers - with pytest.raises(exceptions.CommandError): - sa.encode([f], "request", "invalid") - def test_options(tmpdir): p = str(tmpdir.join("path")) diff --git a/test/mitmproxy/addons/test_core_option_validation.py b/test/mitmproxy/addons/test_core_option_validation.py index 6d6d5ba4..cd5d4dfa 100644 --- a/test/mitmproxy/addons/test_core_option_validation.py +++ b/test/mitmproxy/addons/test_core_option_validation.py @@ -11,7 +11,6 @@ def test_simple(): with pytest.raises(exceptions.OptionsError): tctx.configure(sa, body_size_limit = "invalid") tctx.configure(sa, body_size_limit = "1m") - assert tctx.master.options._processed["body_size_limit"] with pytest.raises(exceptions.OptionsError, match="mutually exclusive"): tctx.configure( diff --git a/test/mitmproxy/addons/test_cut.py b/test/mitmproxy/addons/test_cut.py index e028331f..c444b8ee 100644 --- a/test/mitmproxy/addons/test_cut.py +++ b/test/mitmproxy/addons/test_cut.py @@ -13,83 +13,51 @@ from unittest import mock def test_extract(): tf = tflow.tflow(resp=True) tests = [ - ["q.method", "GET"], - ["q.scheme", "http"], - ["q.host", "address"], - ["q.port", "22"], - ["q.path", "/path"], - ["q.url", "http://address:22/path"], - ["q.text", "content"], - ["q.content", b"content"], - ["q.raw_content", b"content"], - ["q.header[header]", "qvalue"], - - ["s.status_code", "200"], - ["s.reason", "OK"], - ["s.text", "message"], - ["s.content", b"message"], - ["s.raw_content", b"message"], - ["s.header[header-response]", "svalue"], - - ["cc.address.port", "22"], - ["cc.address.host", "address"], - ["cc.tls_version", "TLSv1.2"], - ["cc.sni", "address"], - ["cc.ssl_established", "false"], - - ["sc.address.port", "22"], - ["sc.address.host", "address"], - ["sc.ip_address.host", "192.168.0.1"], - ["sc.tls_version", "TLSv1.2"], - ["sc.sni", "address"], - ["sc.ssl_established", "false"], + ["request.method", "GET"], + ["request.scheme", "http"], + ["request.host", "address"], + ["request.http_version", "HTTP/1.1"], + ["request.port", "22"], + ["request.path", "/path"], + ["request.url", "http://address:22/path"], + ["request.text", "content"], + ["request.content", b"content"], + ["request.raw_content", b"content"], + ["request.timestamp_start", "946681200"], + ["request.timestamp_end", "946681201"], + ["request.header[header]", "qvalue"], + + ["response.status_code", "200"], + ["response.reason", "OK"], + ["response.text", "message"], + ["response.content", b"message"], + ["response.raw_content", b"message"], + ["response.header[header-response]", "svalue"], + ["response.timestamp_start", "946681202"], + ["response.timestamp_end", "946681203"], + + ["client_conn.address.port", "22"], + ["client_conn.address.host", "127.0.0.1"], + ["client_conn.tls_version", "TLSv1.2"], + ["client_conn.sni", "address"], + ["client_conn.tls_established", "false"], + + ["server_conn.address.port", "22"], + ["server_conn.address.host", "address"], + ["server_conn.ip_address.host", "192.168.0.1"], + ["server_conn.tls_version", "TLSv1.2"], + ["server_conn.sni", "address"], + ["server_conn.tls_established", "false"], ] - for t in tests: - ret = cut.extract(t[0], tf) - if ret != t[1]: - raise AssertionError("%s: Expected %s, got %s" % (t[0], t[1], ret)) + for spec, expected in tests: + ret = cut.extract(spec, tf) + assert spec and ret == expected with open(tutils.test_data.path("mitmproxy/net/data/text_cert"), "rb") as f: d = f.read() - c1 = certs.SSLCert.from_pem(d) + c1 = certs.Cert.from_pem(d) tf.server_conn.cert = c1 - assert "CERTIFICATE" in cut.extract("sc.cert", tf) - - -def test_parse_cutspec(): - tests = [ - ("", None, True), - ("req.method", ("@all", ["req.method"]), False), - ( - "req.method,req.host", - ("@all", ["req.method", "req.host"]), - False - ), - ( - "req.method,req.host|~b foo", - ("~b foo", ["req.method", "req.host"]), - False - ), - ( - "req.method,req.host|~b foo | ~b bar", - ("~b foo | ~b bar", ["req.method", "req.host"]), - False - ), - ( - "req.method, req.host | ~b foo | ~b bar", - ("~b foo | ~b bar", ["req.method", "req.host"]), - False - ), - ] - for cutspec, output, err in tests: - try: - assert cut.parse_cutspec(cutspec) == output - except exceptions.CommandError: - if not err: - raise - else: - if err: - raise AssertionError("Expected error.") + assert "CERTIFICATE" in cut.extract("server_conn.cert", tf) def test_headername(): @@ -110,69 +78,69 @@ def test_cut_clip(): v.add([tflow.tflow(resp=True)]) with mock.patch('pyperclip.copy') as pc: - tctx.command(c.clip, "q.method|@all") + tctx.command(c.clip, "@all", "request.method") assert pc.called with mock.patch('pyperclip.copy') as pc: - tctx.command(c.clip, "q.content|@all") + tctx.command(c.clip, "@all", "request.content") assert pc.called with mock.patch('pyperclip.copy') as pc: - tctx.command(c.clip, "q.method,q.content|@all") + tctx.command(c.clip, "@all", "request.method,request.content") assert pc.called -def test_cut_file(tmpdir): +def test_cut_save(tmpdir): f = str(tmpdir.join("path")) v = view.View() c = cut.Cut() with taddons.context() as tctx: tctx.master.addons.add(v, c) - v.add([tflow.tflow(resp=True)]) - tctx.command(c.save, "q.method|@all", f) + tctx.command(c.save, "@all", "request.method", f) assert qr(f) == b"GET" - tctx.command(c.save, "q.content|@all", f) + tctx.command(c.save, "@all", "request.content", f) assert qr(f) == b"content" - tctx.command(c.save, "q.content|@all", "+" + f) + tctx.command(c.save, "@all", "request.content", "+" + f) assert qr(f) == b"content\ncontent" v.add([tflow.tflow(resp=True)]) - tctx.command(c.save, "q.method|@all", f) + tctx.command(c.save, "@all", "request.method", f) assert qr(f).splitlines() == [b"GET", b"GET"] - tctx.command(c.save, "q.method,q.content|@all", f) + tctx.command(c.save, "@all", "request.method,request.content", f) assert qr(f).splitlines() == [b"GET,content", b"GET,content"] def test_cut(): - v = view.View() c = cut.Cut() - with taddons.context() as tctx: - v.add([tflow.tflow(resp=True)]) - tctx.master.addons.add(v, c) - assert c.cut("q.method|@all") == [["GET"]] - assert c.cut("q.scheme|@all") == [["http"]] - assert c.cut("q.host|@all") == [["address"]] - assert c.cut("q.port|@all") == [["22"]] - assert c.cut("q.path|@all") == [["/path"]] - assert c.cut("q.url|@all") == [["http://address:22/path"]] - assert c.cut("q.content|@all") == [[b"content"]] - assert c.cut("q.header[header]|@all") == [["qvalue"]] - assert c.cut("q.header[unknown]|@all") == [[""]] - - assert c.cut("s.status_code|@all") == [["200"]] - assert c.cut("s.reason|@all") == [["OK"]] - assert c.cut("s.content|@all") == [[b"message"]] - assert c.cut("s.header[header-response]|@all") == [["svalue"]] - assert c.cut("moo") == [[""]] + with taddons.context(): + tflows = [tflow.tflow(resp=True)] + assert c.cut(tflows, ["request.method"]) == [["GET"]] + assert c.cut(tflows, ["request.scheme"]) == [["http"]] + assert c.cut(tflows, ["request.host"]) == [["address"]] + assert c.cut(tflows, ["request.port"]) == [["22"]] + assert c.cut(tflows, ["request.path"]) == [["/path"]] + assert c.cut(tflows, ["request.url"]) == [["http://address:22/path"]] + assert c.cut(tflows, ["request.content"]) == [[b"content"]] + assert c.cut(tflows, ["request.header[header]"]) == [["qvalue"]] + assert c.cut(tflows, ["request.header[unknown]"]) == [[""]] + + assert c.cut(tflows, ["response.status_code"]) == [["200"]] + assert c.cut(tflows, ["response.reason"]) == [["OK"]] + assert c.cut(tflows, ["response.content"]) == [[b"message"]] + assert c.cut(tflows, ["response.header[header-response]"]) == [["svalue"]] + assert c.cut(tflows, ["moo"]) == [[""]] with pytest.raises(exceptions.CommandError): - assert c.cut("__dict__") == [[""]] + assert c.cut(tflows, ["__dict__"]) == [[""]] + + with taddons.context(): + tflows = [tflow.tflow(resp=False)] + assert c.cut(tflows, ["response.reason"]) == [[""]] + assert c.cut(tflows, ["response.header[key]"]) == [[""]] - v = view.View() c = cut.Cut() - with taddons.context() as tctx: - tctx.master.addons.add(v, c) - v.add([tflow.ttcpflow()]) - assert c.cut("q.method|@all") == [[""]] - assert c.cut("s.status|@all") == [[""]] + with taddons.context(): + tflows = [tflow.ttcpflow()] + assert c.cut(tflows, ["request.method"]) == [[""]] + assert c.cut(tflows, ["response.status"]) == [[""]] diff --git a/test/mitmproxy/addons/test_dumper.py b/test/mitmproxy/addons/test_dumper.py index d8aa593b..fb80f3ce 100644 --- a/test/mitmproxy/addons/test_dumper.py +++ b/test/mitmproxy/addons/test_dumper.py @@ -148,7 +148,7 @@ class TestContentView: sio = io.StringIO() d = dumper.Dumper(sio) with taddons.context(options=options.Options()) as ctx: - ctx.configure(d, flow_detail=4, verbosity=3) + ctx.configure(d, flow_detail=4, verbosity='debug') d.response(tflow.tflow()) assert ctx.master.has_log("content viewer failed") diff --git a/test/mitmproxy/addons/test_eventstore.py b/test/mitmproxy/addons/test_eventstore.py index f54b9980..8ac26b05 100644 --- a/test/mitmproxy/addons/test_eventstore.py +++ b/test/mitmproxy/addons/test_eventstore.py @@ -30,3 +30,18 @@ def test_simple(): assert not sig_add.called assert sig_refresh.called + + +def test_max_size(): + store = eventstore.EventStore(3) + assert store.size == 3 + store.log(log.LogEntry("foo", "info")) + store.log(log.LogEntry("bar", "info")) + store.log(log.LogEntry("baz", "info")) + assert len(store.data) == 3 + assert ["foo", "bar", "baz"] == [x.msg for x in store.data] + + # overflow + store.log(log.LogEntry("boo", "info")) + assert len(store.data) == 3 + assert ["bar", "baz", "boo"] == [x.msg for x in store.data] diff --git a/test/mitmproxy/addons/test_intercept.py b/test/mitmproxy/addons/test_intercept.py index f436a817..d4999eb5 100644 --- a/test/mitmproxy/addons/test_intercept.py +++ b/test/mitmproxy/addons/test_intercept.py @@ -13,10 +13,12 @@ def test_simple(): assert not r.filt tctx.configure(r, intercept="~q") assert r.filt + assert tctx.options.intercept_active with pytest.raises(exceptions.OptionsError): tctx.configure(r, intercept="~~") tctx.configure(r, intercept=None) assert not r.filt + assert not tctx.options.intercept_active tctx.configure(r, intercept="~s") @@ -31,3 +33,13 @@ def test_simple(): f = tflow.tflow(resp=True) r.response(f) assert f.intercepted + + tctx.configure(r, intercept_active=False) + f = tflow.tflow(resp=True) + tctx.cycle(r, f) + assert not f.intercepted + + tctx.configure(r, intercept_active=True) + f = tflow.tflow(resp=True) + tctx.cycle(r, f) + assert f.intercepted diff --git a/test/mitmproxy/addons/test_proxyauth.py b/test/mitmproxy/addons/test_proxyauth.py index 40044bf0..97259d1c 100644 --- a/test/mitmproxy/addons/test_proxyauth.py +++ b/test/mitmproxy/addons/test_proxyauth.py @@ -10,197 +10,242 @@ from mitmproxy.test import tflow from mitmproxy.test import tutils -def test_parse_http_basic_auth(): - assert proxyauth.parse_http_basic_auth( - proxyauth.mkauth("test", "test") - ) == ("basic", "test", "test") - with pytest.raises(ValueError): - proxyauth.parse_http_basic_auth("") - with pytest.raises(ValueError): - proxyauth.parse_http_basic_auth("foo bar") - with pytest.raises(ValueError): - proxyauth.parse_http_basic_auth("basic abc") - with pytest.raises(ValueError): - v = "basic " + binascii.b2a_base64(b"foo").decode("ascii") - proxyauth.parse_http_basic_auth(v) - - -def test_configure(): - up = proxyauth.ProxyAuth() - with taddons.context() as ctx: - with pytest.raises(exceptions.OptionsError): - ctx.configure(up, proxyauth="foo") - - ctx.configure(up, proxyauth="foo:bar") - assert up.singleuser == ["foo", "bar"] - - ctx.configure(up, proxyauth=None) - assert up.singleuser is None - - ctx.configure(up, proxyauth="any") - assert up.nonanonymous - ctx.configure(up, proxyauth=None) - assert not up.nonanonymous - - with mock.patch('ldap3.Server', return_value="ldap://fake_server:389 - cleartext"): - with mock.patch('ldap3.Connection', return_value="test"): - ctx.configure(up, proxyauth="ldap:localhost:cn=default,dc=cdhdt,dc=com:password:ou=application,dc=cdhdt,dc=com") - assert up.ldapserver - ctx.configure(up, proxyauth="ldaps:localhost:cn=default,dc=cdhdt,dc=com:password:ou=application,dc=cdhdt,dc=com") - assert up.ldapserver - - with pytest.raises(exceptions.OptionsError): - ctx.configure(up, proxyauth="ldap:test:test:test") - - with pytest.raises(IndexError): - ctx.configure(up, proxyauth="ldap:fake_serveruid=?dc=example,dc=com:person") - - with pytest.raises(exceptions.OptionsError): - ctx.configure(up, proxyauth="ldapssssssss:fake_server:dn:password:tree") - - with pytest.raises(exceptions.OptionsError): +class TestMkauth: + def test_mkauth_scheme(self): + assert proxyauth.mkauth('username', 'password') == 'basic dXNlcm5hbWU6cGFzc3dvcmQ=\n' + + @pytest.mark.parametrize('scheme, expected', [ + ('', ' dXNlcm5hbWU6cGFzc3dvcmQ=\n'), + ('basic', 'basic dXNlcm5hbWU6cGFzc3dvcmQ=\n'), + ('foobar', 'foobar dXNlcm5hbWU6cGFzc3dvcmQ=\n'), + ]) + def test_mkauth(self, scheme, expected): + assert proxyauth.mkauth('username', 'password', scheme) == expected + + +class TestParseHttpBasicAuth: + @pytest.mark.parametrize('input', [ + '', + 'foo bar', + 'basic abc', + 'basic ' + binascii.b2a_base64(b"foo").decode("ascii"), + ]) + def test_parse_http_basic_auth_error(self, input): + with pytest.raises(ValueError): + proxyauth.parse_http_basic_auth(input) + + def test_parse_http_basic_auth(self): + input = proxyauth.mkauth("test", "test") + assert proxyauth.parse_http_basic_auth(input) == ("basic", "test", "test") + + +class TestProxyAuth: + @pytest.mark.parametrize('mode, expected', [ + ('', False), + ('foobar', False), + ('regular', True), + ('upstream:', True), + ('upstream:foobar', True), + ]) + def test_is_proxy_auth(self, mode, expected): + up = proxyauth.ProxyAuth() + with taddons.context() as ctx: + ctx.options.mode = mode + assert up.is_proxy_auth() is expected + + @pytest.mark.parametrize('is_proxy_auth, expected', [ + (True, 'Proxy-Authorization'), + (False, 'Authorization'), + ]) + def test_which_auth_header(self, is_proxy_auth, expected): + up = proxyauth.ProxyAuth() + with mock.patch('mitmproxy.addons.proxyauth.ProxyAuth.is_proxy_auth', return_value=is_proxy_auth): + assert up.which_auth_header() == expected + + @pytest.mark.parametrize('is_proxy_auth, expected_status_code, expected_header', [ + (True, 407, 'Proxy-Authenticate'), + (False, 401, 'WWW-Authenticate'), + ]) + def test_auth_required_response(self, is_proxy_auth, expected_status_code, expected_header): + up = proxyauth.ProxyAuth() + with mock.patch('mitmproxy.addons.proxyauth.ProxyAuth.is_proxy_auth', return_value=is_proxy_auth): + resp = up.auth_required_response() + assert resp.status_code == expected_status_code + assert expected_header in resp.headers.keys() + + def test_check(self): + up = proxyauth.ProxyAuth() + with taddons.context() as ctx: + ctx.configure(up, proxyauth="any", mode="regular") + f = tflow.tflow() + assert not up.check(f) + f.request.headers["Proxy-Authorization"] = proxyauth.mkauth( + "test", "test" + ) + assert up.check(f) + + f.request.headers["Proxy-Authorization"] = "invalid" + assert not up.check(f) + + f.request.headers["Proxy-Authorization"] = proxyauth.mkauth( + "test", "test", scheme="unknown" + ) + assert not up.check(f) + + ctx.configure(up, proxyauth="test:test") + f.request.headers["Proxy-Authorization"] = proxyauth.mkauth( + "test", "test" + ) + assert up.check(f) + ctx.configure(up, proxyauth="test:foo") + assert not up.check(f) + ctx.configure( up, - proxyauth= "@" + tutils.test_data.path("mitmproxy/net/data/server.crt") + proxyauth="@" + tutils.test_data.path( + "mitmproxy/net/data/htpasswd" + ) ) - with pytest.raises(exceptions.OptionsError): - ctx.configure(up, proxyauth="@nonexistent") + f.request.headers["Proxy-Authorization"] = proxyauth.mkauth( + "test", "test" + ) + assert up.check(f) + f.request.headers["Proxy-Authorization"] = proxyauth.mkauth( + "test", "foo" + ) + assert not up.check(f) + + with mock.patch('ldap3.Server', return_value="ldap://fake_server:389 - cleartext"): + with mock.patch('ldap3.Connection', search="test"): + with mock.patch('ldap3.Connection.search', return_value="test"): + ctx.configure( + up, + proxyauth="ldap:localhost:cn=default,dc=cdhdt,dc=com:password:ou=application,dc=cdhdt,dc=com" + ) + f.request.headers["Proxy-Authorization"] = proxyauth.mkauth( + "test", "test" + ) + assert up.check(f) + f.request.headers["Proxy-Authorization"] = proxyauth.mkauth( + "", "" + ) + assert not up.check(f) + + def test_authenticate(self): + up = proxyauth.ProxyAuth() + with taddons.context() as ctx: + ctx.configure(up, proxyauth="any", mode="regular") + + f = tflow.tflow() + assert not f.response + up.authenticate(f) + assert f.response.status_code == 407 + + f = tflow.tflow() + f.request.headers["Proxy-Authorization"] = proxyauth.mkauth( + "test", "test" + ) + up.authenticate(f) + assert not f.response + assert not f.request.headers.get("Proxy-Authorization") + + f = tflow.tflow() + ctx.configure(up, mode="reverse") + assert not f.response + up.authenticate(f) + assert f.response.status_code == 401 + + f = tflow.tflow() + f.request.headers["Authorization"] = proxyauth.mkauth( + "test", "test" + ) + up.authenticate(f) + assert not f.response + assert not f.request.headers.get("Authorization") + + def test_configure(self): + up = proxyauth.ProxyAuth() + with taddons.context() as ctx: + with pytest.raises(exceptions.OptionsError): + ctx.configure(up, proxyauth="foo") + + ctx.configure(up, proxyauth="foo:bar") + assert up.singleuser == ["foo", "bar"] + + ctx.configure(up, proxyauth=None) + assert up.singleuser is None + + ctx.configure(up, proxyauth="any") + assert up.nonanonymous + ctx.configure(up, proxyauth=None) + assert not up.nonanonymous + + with mock.patch('ldap3.Server', return_value="ldap://fake_server:389 - cleartext"): + with mock.patch('ldap3.Connection', return_value="test"): + ctx.configure(up, proxyauth="ldap:localhost:cn=default,dc=cdhdt,dc=com:password:ou=application,dc=cdhdt,dc=com") + assert up.ldapserver + ctx.configure(up, proxyauth="ldaps:localhost:cn=default,dc=cdhdt,dc=com:password:ou=application,dc=cdhdt,dc=com") + assert up.ldapserver + + with pytest.raises(exceptions.OptionsError): + ctx.configure(up, proxyauth="ldap:test:test:test") + + with pytest.raises(exceptions.OptionsError): + ctx.configure(up, proxyauth="ldap:fake_serveruid=?dc=example,dc=com:person") + + with pytest.raises(exceptions.OptionsError): + ctx.configure(up, proxyauth="ldapssssssss:fake_server:dn:password:tree") + + with pytest.raises(exceptions.OptionsError): + ctx.configure( + up, + proxyauth= "@" + tutils.test_data.path("mitmproxy/net/data/server.crt") + ) + with pytest.raises(exceptions.OptionsError): + ctx.configure(up, proxyauth="@nonexistent") - ctx.configure( - up, - proxyauth= "@" + tutils.test_data.path( - "mitmproxy/net/data/htpasswd" + ctx.configure( + up, + proxyauth= "@" + tutils.test_data.path( + "mitmproxy/net/data/htpasswd" + ) ) - ) - assert up.htpasswd - assert up.htpasswd.check_password("test", "test") - assert not up.htpasswd.check_password("test", "foo") - ctx.configure(up, proxyauth=None) - assert not up.htpasswd - - with pytest.raises(exceptions.OptionsError): - ctx.configure(up, proxyauth="any", mode="transparent") - with pytest.raises(exceptions.OptionsError): - ctx.configure(up, proxyauth="any", mode="socks5") - - -def test_check(monkeypatch): - up = proxyauth.ProxyAuth() - with taddons.context() as ctx: - ctx.configure(up, proxyauth="any", mode="regular") - f = tflow.tflow() - assert not up.check(f) - f.request.headers["Proxy-Authorization"] = proxyauth.mkauth( - "test", "test" - ) - assert up.check(f) - - f.request.headers["Proxy-Authorization"] = "invalid" - assert not up.check(f) - - f.request.headers["Proxy-Authorization"] = proxyauth.mkauth( - "test", "test", scheme="unknown" - ) - assert not up.check(f) - - ctx.configure(up, proxyauth="test:test") - f.request.headers["Proxy-Authorization"] = proxyauth.mkauth( - "test", "test" - ) - assert up.check(f) - ctx.configure(up, proxyauth="test:foo") - assert not up.check(f) - - ctx.configure( - up, - proxyauth="@" + tutils.test_data.path( - "mitmproxy/net/data/htpasswd" + assert up.htpasswd + assert up.htpasswd.check_password("test", "test") + assert not up.htpasswd.check_password("test", "foo") + ctx.configure(up, proxyauth=None) + assert not up.htpasswd + + with pytest.raises(exceptions.OptionsError): + ctx.configure(up, proxyauth="any", mode="transparent") + with pytest.raises(exceptions.OptionsError): + ctx.configure(up, proxyauth="any", mode="socks5") + + def test_handlers(self): + up = proxyauth.ProxyAuth() + with taddons.context() as ctx: + ctx.configure(up, proxyauth="any", mode="regular") + + f = tflow.tflow() + assert not f.response + up.requestheaders(f) + assert f.response.status_code == 407 + + f = tflow.tflow() + f.request.method = "CONNECT" + assert not f.response + up.http_connect(f) + assert f.response.status_code == 407 + + f = tflow.tflow() + f.request.method = "CONNECT" + f.request.headers["Proxy-Authorization"] = proxyauth.mkauth( + "test", "test" ) - ) - f.request.headers["Proxy-Authorization"] = proxyauth.mkauth( - "test", "test" - ) - assert up.check(f) - f.request.headers["Proxy-Authorization"] = proxyauth.mkauth( - "test", "foo" - ) - assert not up.check(f) - - with mock.patch('ldap3.Server', return_value="ldap://fake_server:389 - cleartext"): - with mock.patch('ldap3.Connection', search="test"): - with mock.patch('ldap3.Connection.search', return_value="test"): - ctx.configure( - up, - proxyauth="ldap:localhost:cn=default,dc=cdhdt,dc=com:password:ou=application,dc=cdhdt,dc=com" - ) - f.request.headers["Proxy-Authorization"] = proxyauth.mkauth( - "test", "test" - ) - assert up.check(f) - f.request.headers["Proxy-Authorization"] = proxyauth.mkauth( - "", "" - ) - assert not up.check(f) - - -def test_authenticate(): - up = proxyauth.ProxyAuth() - with taddons.context() as ctx: - ctx.configure(up, proxyauth="any", mode="regular") - - f = tflow.tflow() - assert not f.response - up.authenticate(f) - assert f.response.status_code == 407 - - f = tflow.tflow() - f.request.headers["Proxy-Authorization"] = proxyauth.mkauth( - "test", "test" - ) - up.authenticate(f) - assert not f.response - assert not f.request.headers.get("Proxy-Authorization") - - f = tflow.tflow() - ctx.configure(up, mode="reverse") - assert not f.response - up.authenticate(f) - assert f.response.status_code == 401 - - f = tflow.tflow() - f.request.headers["Authorization"] = proxyauth.mkauth( - "test", "test" - ) - up.authenticate(f) - assert not f.response - assert not f.request.headers.get("Authorization") - - -def test_handlers(): - up = proxyauth.ProxyAuth() - with taddons.context() as ctx: - ctx.configure(up, proxyauth="any", mode="regular") - - f = tflow.tflow() - assert not f.response - up.requestheaders(f) - assert f.response.status_code == 407 - - f = tflow.tflow() - f.request.method = "CONNECT" - assert not f.response - up.http_connect(f) - assert f.response.status_code == 407 - - f = tflow.tflow() - f.request.method = "CONNECT" - f.request.headers["Proxy-Authorization"] = proxyauth.mkauth( - "test", "test" - ) - up.http_connect(f) - assert not f.response - - f2 = tflow.tflow(client_conn=f.client_conn) - up.requestheaders(f2) - assert not f2.response - assert f2.metadata["proxyauth"] == ('test', 'test') + up.http_connect(f) + assert not f.response + + f2 = tflow.tflow(client_conn=f.client_conn) + up.requestheaders(f2) + assert not f2.response + assert f2.metadata["proxyauth"] == ('test', 'test') diff --git a/test/mitmproxy/addons/test_save.py b/test/mitmproxy/addons/test_save.py index a4e425cd..2dee708f 100644 --- a/test/mitmproxy/addons/test_save.py +++ b/test/mitmproxy/addons/test_save.py @@ -44,6 +44,19 @@ def test_tcp(tmpdir): assert rd(p) +def test_websocket(tmpdir): + sa = save.Save() + with taddons.context() as tctx: + p = str(tmpdir.join("foo")) + tctx.configure(sa, save_stream_file=p) + + f = tflow.twebsocketflow() + sa.websocket_start(f) + sa.websocket_end(f) + tctx.configure(sa, save_stream_file=None) + assert rd(p) + + def test_save_command(tmpdir): sa = save.Save() with taddons.context() as tctx: diff --git a/test/mitmproxy/addons/test_script.py b/test/mitmproxy/addons/test_script.py index dd5349cb..c4fe6b43 100644 --- a/test/mitmproxy/addons/test_script.py +++ b/test/mitmproxy/addons/test_script.py @@ -1,46 +1,65 @@ -import traceback +import os import sys -import time +import traceback +from unittest import mock + import pytest -from unittest import mock -from mitmproxy.test import tflow -from mitmproxy.test import tutils -from mitmproxy.test import taddons from mitmproxy import addonmanager from mitmproxy import exceptions +from mitmproxy import log from mitmproxy.addons import script +from mitmproxy.test import taddons +from mitmproxy.test import tflow +from mitmproxy.test import tutils 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(): + """ + Test that loading two scripts at locations a/foo.py and b/foo.py works. + This only succeeds if they get assigned different basenames. + + """ + ns = script.load_script( + tutils.test_data.path( + "mitmproxy/data/addonscripts/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") def test_script_print_stdout(): with taddons.context() as tctx: - with mock.patch('mitmproxy.ctx.log.warn') as mock_warn: + with mock.patch('mitmproxy.ctx.master.tell') as mock_warn: with addonmanager.safecall(): ns = script.load_script( - tctx.ctx(), tutils.test_data.path( "mitmproxy/data/addonscripts/print.py" ) ) ns.load(addonmanager.Loader(tctx.master)) - mock_warn.assert_called_once_with("stdoutprint") + mock_warn.assert_called_once_with("log", log.LogEntry("stdoutprint", "warn")) class TestScript: @@ -78,11 +97,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: @@ -96,8 +117,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: @@ -183,6 +204,34 @@ class TestScriptLoader: scripts = ["one", "one"] ) + def test_script_deletion(self): + tdir = tutils.test_data.path("mitmproxy/data/addonscripts/") + with open(tdir + "/dummy.py", 'w') as f: + f.write("\n") + with taddons.context() as tctx: + sl = script.ScriptLoader() + tctx.master.addons.add(sl) + tctx.configure(sl, scripts=[tutils.test_data.path("mitmproxy/data/addonscripts/dummy.py")]) + + os.remove(tutils.test_data.path("mitmproxy/data/addonscripts/dummy.py")) + tctx.invoke(sl, "tick") + assert not tctx.options.scripts + assert not sl.addons + + def test_load_err(self): + sc = script.ScriptLoader() + with taddons.context() as tctx: + tctx.configure(sc, scripts=[ + tutils.test_data.path("mitmproxy/data/addonscripts/load_error.py") + ]) + try: + tctx.invoke(sc, "tick") + except ValueError: + pass # this is expected and normally guarded. + # on the next tick we should not fail however. + tctx.invoke(sc, "tick") + assert len(tctx.master.addons) == 0 + def test_order(self): rec = tutils.test_data.path("mitmproxy/data/addonscripts/recorder") sc = script.ScriptLoader() diff --git a/test/mitmproxy/addons/test_streambodies.py b/test/mitmproxy/addons/test_streambodies.py index c6ce5e81..426ec9ae 100644 --- a/test/mitmproxy/addons/test_streambodies.py +++ b/test/mitmproxy/addons/test_streambodies.py @@ -29,3 +29,9 @@ def test_simple(): f = tflow.tflow(resp=True) f.response.headers["content-length"] = "invalid" tctx.cycle(sa, f) + + tctx.configure(sa, stream_websockets = True) + f = tflow.twebsocketflow() + assert not f.stream + sa.websocket_start(f) + assert f.stream diff --git a/test/mitmproxy/addons/test_termlog.py b/test/mitmproxy/addons/test_termlog.py index 2133b74d..027bdfeb 100644 --- a/test/mitmproxy/addons/test_termlog.py +++ b/test/mitmproxy/addons/test_termlog.py @@ -16,7 +16,7 @@ class TestTermLog: ]) def test_output(self, outfile, expected_out, expected_err, capfd): t = termlog.TermLog(outfile=outfile) - with taddons.context(options=Options(verbosity=2)) as tctx: + with taddons.context(options=Options(verbosity='info')) as tctx: tctx.configure(t) t.log(log.LogEntry("one", "info")) t.log(log.LogEntry("two", "debug")) diff --git a/test/mitmproxy/addons/test_termstatus.py b/test/mitmproxy/addons/test_termstatus.py index 2debaff5..5f960a1c 100644 --- a/test/mitmproxy/addons/test_termstatus.py +++ b/test/mitmproxy/addons/test_termstatus.py @@ -1,3 +1,4 @@ +from mitmproxy import proxy from mitmproxy.addons import termstatus from mitmproxy.test import taddons @@ -5,6 +6,7 @@ from mitmproxy.test import taddons def test_configure(): ts = termstatus.TermStatus() with taddons.context() as ctx: + ctx.master.server = proxy.DummyServer() ctx.configure(ts, server=False) ts.running() assert not ctx.master.logs diff --git a/test/mitmproxy/addons/test_view.py b/test/mitmproxy/addons/test_view.py index d5a3a456..a95d059d 100644 --- a/test/mitmproxy/addons/test_view.py +++ b/test/mitmproxy/addons/test_view.py @@ -7,6 +7,7 @@ from mitmproxy import flowfilter from mitmproxy import exceptions from mitmproxy import io from mitmproxy.test import taddons +from mitmproxy.tools.console import consoleaddons def tft(*, method="get", start=0): @@ -27,9 +28,9 @@ def test_order_refresh(): tf = tflow.tflow(resp=True) with taddons.context() as tctx: - tctx.configure(v, console_order="time") + tctx.configure(v, view_order="time") v.add([tf]) - tf.request.timestamp_start = 1 + tf.request.timestamp_start = 10 assert not sargs v.update([tf]) assert sargs @@ -40,7 +41,7 @@ def test_order_generators(): tf = tflow.tflow(resp=True) rs = view.OrderRequestStart(v) - assert rs.generate(tf) == 0 + assert rs.generate(tf) == 946681200 rm = view.OrderRequestMethod(v) assert rm.generate(tf) == tf.request.method @@ -146,6 +147,10 @@ def test_create(): assert v[0].request.url == "http://foo.com/" v.create("get", "http://foo.com") assert len(v) == 2 + with pytest.raises(exceptions.CommandError, match="Invalid URL"): + v.create("get", "http://foo.com\\") + with pytest.raises(exceptions.CommandError, match="Invalid URL"): + v.create("get", "http://") def test_orders(): @@ -170,6 +175,10 @@ def test_load(tmpdir): assert len(v) == 2 v.load_file(path) assert len(v) == 4 + try: + v.load_file("nonexistent_file_path") + except IOError: + assert False def test_resolve(): @@ -262,6 +271,16 @@ def test_duplicate(): assert v.focus.index == 2 +def test_remove(): + v = view.View() + with taddons.context(): + f = [tflow.tflow(), tflow.tflow()] + v.add(f) + assert len(v) == 2 + v.remove(f) + assert len(v) == 0 + + def test_setgetval(): v = view.View() with taddons.context(): @@ -286,12 +305,12 @@ def test_order(): v.request(tft(method="put", start=4)) assert [i.request.timestamp_start for i in v] == [1, 2, 3, 4] - tctx.configure(v, console_order="method") + tctx.configure(v, view_order="method") assert [i.request.method for i in v] == ["GET", "GET", "PUT", "PUT"] v.set_reversed(True) assert [i.request.method for i in v] == ["PUT", "PUT", "GET", "GET"] - tctx.configure(v, console_order="time") + tctx.configure(v, view_order="time") assert [i.request.timestamp_start for i in v] == [4, 3, 2, 1] v.set_reversed(False) @@ -411,6 +430,8 @@ def test_signals(): def test_focus_follow(): v = view.View() with taddons.context() as tctx: + console_addon = consoleaddons.ConsoleAddon(tctx.master) + tctx.configure(console_addon) tctx.configure(v, console_focus_follow=True, view_filter="~m get") v.add([tft(start=5)]) @@ -532,11 +553,11 @@ def test_configure(): with pytest.raises(Exception, match="Invalid interception filter"): tctx.configure(v, view_filter="~~") - tctx.configure(v, console_order="method") + tctx.configure(v, view_order="method") with pytest.raises(Exception, match="Unknown flow order"): - tctx.configure(v, console_order="no") + tctx.configure(v, view_order="no") - tctx.configure(v, console_order_reversed=True) + tctx.configure(v, view_order_reversed=True) tctx.configure(v, console_focus_follow=True) assert v.focus_follow diff --git a/test/mitmproxy/contentviews/image/test_image_parser.py b/test/mitmproxy/contentviews/image/test_image_parser.py index 3cb44ca6..fdc72165 100644 --- a/test/mitmproxy/contentviews/image/test_image_parser.py +++ b/test/mitmproxy/contentviews/image/test_image_parser.py @@ -167,3 +167,26 @@ def test_parse_gif(filename, metadata): def test_parse_jpeg(filename, metadata): with open(tutils.test_data.path(filename), 'rb') as f: assert metadata == image_parser.parse_jpeg(f.read()) + + +@pytest.mark.parametrize("filename, metadata", { + "mitmproxy/data/image.ico": [ + ('Format', 'ICO'), + ('Number of images', '3'), + ('Image 1', "Size: {} x {}\n" + "{: >18}Bits per pixel: {}\n" + "{: >18}PNG: {}".format(48, 48, '', 24, '', False) + ), + ('Image 2', "Size: {} x {}\n" + "{: >18}Bits per pixel: {}\n" + "{: >18}PNG: {}".format(32, 32, '', 24, '', False) + ), + ('Image 3', "Size: {} x {}\n" + "{: >18}Bits per pixel: {}\n" + "{: >18}PNG: {}".format(16, 16, '', 24, '', False) + ) + ] +}.items()) +def test_ico(filename, metadata): + with open(tutils.test_data.path(filename), 'rb') as f: + assert metadata == image_parser.parse_ico(f.read()) diff --git a/test/mitmproxy/contentviews/image/test_view.py b/test/mitmproxy/contentviews/image/test_view.py index 34f655a1..6da5b1d0 100644 --- a/test/mitmproxy/contentviews/image/test_view.py +++ b/test/mitmproxy/contentviews/image/test_view.py @@ -9,8 +9,7 @@ def test_view_image(): "mitmproxy/data/image.png", "mitmproxy/data/image.gif", "mitmproxy/data/all.jpeg", - # https://bugs.python.org/issue21574 - # "mitmproxy/data/image.ico", + "mitmproxy/data/image.ico", ]: with open(tutils.test_data.path(img), "rb") as f: viewname, lines = v(f.read()) diff --git a/test/mitmproxy/contentviews/test_auto.py b/test/mitmproxy/contentviews/test_auto.py index 2ff43139..cd888a2d 100644 --- a/test/mitmproxy/contentviews/test_auto.py +++ b/test/mitmproxy/contentviews/test_auto.py @@ -1,6 +1,6 @@ from mitmproxy.contentviews import auto from mitmproxy.net import http -from mitmproxy.types import multidict +from mitmproxy.coretypes import multidict from . import full_eval diff --git a/test/mitmproxy/contentviews/test_css.py b/test/mitmproxy/contentviews/test_css.py index ecb9259b..814f6e83 100644 --- a/test/mitmproxy/contentviews/test_css.py +++ b/test/mitmproxy/contentviews/test_css.py @@ -1,29 +1,42 @@ +import pytest + from mitmproxy.contentviews import css from mitmproxy.test import tutils from . import full_eval -try: - import cssutils -except: - cssutils = None - - -def test_view_css(): +data = tutils.test_data.push("mitmproxy/contentviews/test_css_data/") + + +@pytest.mark.parametrize("filename", [ + "animation-keyframe.css", + "blank-lines-and-spaces.css", + "block-comment.css", + "empty-rule.css", + "import-directive.css", + "indentation.css", + "media-directive.css", + "quoted-string.css", + "selectors.css", + "simple.css", +]) +def test_beautify(filename): + path = data.path(filename) + with open(path) as f: + input = f.read() + with open("-formatted.".join(path.rsplit(".", 1))) as f: + expected = f.read() + formatted = css.beautify(input) + assert formatted == expected + + +def test_simple(): v = full_eval(css.ViewCSS()) - - with open(tutils.test_data.path('mitmproxy/data/1.css'), 'r') as fp: - fixture_1 = fp.read() - - result = v('a') - - if cssutils: - assert len(list(result[1])) == 0 - else: - assert len(list(result[1])) == 1 - - result = v(fixture_1) - - if cssutils: - assert len(list(result[1])) > 1 - else: - assert len(list(result[1])) == 1 + assert v(b"#foo{color:red}") == ('CSS', [ + [('text', '#foo {')], + [('text', ' color: red')], + [('text', '}')] + ]) + assert v(b"") == ('CSS', [[('text', '')]]) + assert v(b"console.log('not really css')") == ( + 'CSS', [[('text', "console.log('not really css')")]] + ) diff --git a/test/mitmproxy/contentviews/test_css_data/animation-keyframe-formatted.css b/test/mitmproxy/contentviews/test_css_data/animation-keyframe-formatted.css new file mode 100644 index 00000000..3f91d508 --- /dev/null +++ b/test/mitmproxy/contentviews/test_css_data/animation-keyframe-formatted.css @@ -0,0 +1,11 @@ +@-webkit-keyframes anim { +0% { + -webkit-transform: translate3d(0px, 0px, 0px); +} + +100% { + -webkit-transform: translate3d(150px, 0px, 0px) +} + + +} diff --git a/test/mitmproxy/contentviews/test_css_data/animation-keyframe.css b/test/mitmproxy/contentviews/test_css_data/animation-keyframe.css new file mode 100644 index 00000000..ce63da5c --- /dev/null +++ b/test/mitmproxy/contentviews/test_css_data/animation-keyframe.css @@ -0,0 +1,3 @@ +@-webkit-keyframes anim { +0% { -webkit-transform: translate3d(0px, 0px, 0px); } +100% { -webkit-transform: translate3d(150px, 0px, 0px) }} diff --git a/test/mitmproxy/contentviews/test_css_data/blank-lines-and-spaces-formatted.css b/test/mitmproxy/contentviews/test_css_data/blank-lines-and-spaces-formatted.css new file mode 100644 index 00000000..de6bd045 --- /dev/null +++ b/test/mitmproxy/contentviews/test_css_data/blank-lines-and-spaces-formatted.css @@ -0,0 +1,35 @@ +/* only one blank line between */ +menu { + color: red +} + +navi { + color: black +} + +/* automatically insert a blank line */ +button { + border: 1px +} + +sidebar { + color: #ffe +} + +/* always whitespace before { */ +hidden { + opacity: 0% +} + +/* no blank lines inside ruleset */ +imprint { + color: blue; + opacity: 0.5; + font-size: small +} + +/* before colon: no space, after colon: one space only */ +footer { + font-family: Arial; + float: right; +} diff --git a/test/mitmproxy/contentviews/test_css_data/blank-lines-and-spaces.css b/test/mitmproxy/contentviews/test_css_data/blank-lines-and-spaces.css new file mode 100644 index 00000000..c6892105 --- /dev/null +++ b/test/mitmproxy/contentviews/test_css_data/blank-lines-and-spaces.css @@ -0,0 +1,30 @@ +/* only one blank line between */ +menu { color: red } + + + + +navi { color: black } + +/* automatically insert a blank line */ +button { border: 1px } sidebar { color: #ffe } + +/* always whitespace before { */ +hidden{opacity:0%} + +/* no blank lines inside ruleset */ +imprint { + color: blue; + + + opacity: 0.5; + + font-size: small +} + +/* before colon: no space, after colon: one space only */ +footer { + font-family: Arial; + + float :right; + } diff --git a/test/mitmproxy/contentviews/test_css_data/block-comment-formatted.css b/test/mitmproxy/contentviews/test_css_data/block-comment-formatted.css new file mode 100644 index 00000000..83e0f4e6 --- /dev/null +++ b/test/mitmproxy/contentviews/test_css_data/block-comment-formatted.css @@ -0,0 +1,22 @@ +/* line comment */ +navigation { + color: blue +} + +menu { + /* line comment inside */ + border: 2px +} + +/* block +comment */ +sidebar { + color: red +} + +invisible { + /* block + * comment + * inside */ + color: #eee +} diff --git a/test/mitmproxy/contentviews/test_css_data/block-comment.css b/test/mitmproxy/contentviews/test_css_data/block-comment.css new file mode 100644 index 00000000..3ba26540 --- /dev/null +++ b/test/mitmproxy/contentviews/test_css_data/block-comment.css @@ -0,0 +1,18 @@ +/* line comment */ +navigation { color: blue } + +menu { + /* line comment inside */ + border: 2px +} + +/* block + comment */ +sidebar { color: red } + +invisible { + /* block + * comment + * inside */ + color: #eee +} diff --git a/test/mitmproxy/contentviews/test_css_data/empty-rule-formatted.css b/test/mitmproxy/contentviews/test_css_data/empty-rule-formatted.css new file mode 100644 index 00000000..7c0a78f4 --- /dev/null +++ b/test/mitmproxy/contentviews/test_css_data/empty-rule-formatted.css @@ -0,0 +1,2 @@ +menu { +} diff --git a/test/mitmproxy/contentviews/test_css_data/empty-rule.css b/test/mitmproxy/contentviews/test_css_data/empty-rule.css new file mode 100644 index 00000000..7d6ecfcd --- /dev/null +++ b/test/mitmproxy/contentviews/test_css_data/empty-rule.css @@ -0,0 +1 @@ +menu{} diff --git a/test/mitmproxy/contentviews/test_css_data/import-directive-formatted.css b/test/mitmproxy/contentviews/test_css_data/import-directive-formatted.css new file mode 100644 index 00000000..08a0ad57 --- /dev/null +++ b/test/mitmproxy/contentviews/test_css_data/import-directive-formatted.css @@ -0,0 +1,8 @@ +menu { + background-color: red +} + +@import url('foobar.css') screen; +nav { + margin: 0 +} diff --git a/test/mitmproxy/contentviews/test_css_data/import-directive.css b/test/mitmproxy/contentviews/test_css_data/import-directive.css new file mode 100644 index 00000000..61979f0a --- /dev/null +++ b/test/mitmproxy/contentviews/test_css_data/import-directive.css @@ -0,0 +1,2 @@ +menu{background-color:red} @import url('foobar.css') screen; +nav{margin:0} diff --git a/test/mitmproxy/contentviews/test_css_data/indentation-formatted.css b/test/mitmproxy/contentviews/test_css_data/indentation-formatted.css new file mode 100644 index 00000000..18ea527d --- /dev/null +++ b/test/mitmproxy/contentviews/test_css_data/indentation-formatted.css @@ -0,0 +1,3 @@ +navigation { + color: blue +} diff --git a/test/mitmproxy/contentviews/test_css_data/indentation.css b/test/mitmproxy/contentviews/test_css_data/indentation.css new file mode 100644 index 00000000..77e00f83 --- /dev/null +++ b/test/mitmproxy/contentviews/test_css_data/indentation.css @@ -0,0 +1,3 @@ + navigation { + color: blue + } diff --git a/test/mitmproxy/contentviews/test_css_data/media-directive-formatted.css b/test/mitmproxy/contentviews/test_css_data/media-directive-formatted.css new file mode 100644 index 00000000..84d95421 --- /dev/null +++ b/test/mitmproxy/contentviews/test_css_data/media-directive-formatted.css @@ -0,0 +1,17 @@ +@import "subs.css"; +@import "print-main.css" print; +@media print { +body { + font-size: 10pt +} + +nav { + color: blue; +} + + +} + +h1 { + color: red; +} diff --git a/test/mitmproxy/contentviews/test_css_data/media-directive.css b/test/mitmproxy/contentviews/test_css_data/media-directive.css new file mode 100644 index 00000000..ddf67c58 --- /dev/null +++ b/test/mitmproxy/contentviews/test_css_data/media-directive.css @@ -0,0 +1,7 @@ +@import "subs.css"; +@import "print-main.css" print; +@media print { + body { font-size: 10pt } + nav { color: blue; } +} +h1 {color: red; } diff --git a/test/mitmproxy/contentviews/test_css_data/quoted-string-formatted.css b/test/mitmproxy/contentviews/test_css_data/quoted-string-formatted.css new file mode 100644 index 00000000..ab4c3412 --- /dev/null +++ b/test/mitmproxy/contentviews/test_css_data/quoted-string-formatted.css @@ -0,0 +1,7 @@ +nav:after { + content: '}' +} + +nav:before { + content: "}" +} diff --git a/test/mitmproxy/contentviews/test_css_data/quoted-string.css b/test/mitmproxy/contentviews/test_css_data/quoted-string.css new file mode 100644 index 00000000..f5f3279e --- /dev/null +++ b/test/mitmproxy/contentviews/test_css_data/quoted-string.css @@ -0,0 +1,2 @@ +nav:after{content:'}'} +nav:before{content:"}"} diff --git a/test/mitmproxy/contentviews/test_css_data/selectors-formatted.css b/test/mitmproxy/contentviews/test_css_data/selectors-formatted.css new file mode 100644 index 00000000..166251cb --- /dev/null +++ b/test/mitmproxy/contentviews/test_css_data/selectors-formatted.css @@ -0,0 +1,19 @@ +* { + border: 0px solid blue; +} + +div[class="{}"] { + color: red; +} + +a[id=\"foo"] { + padding: 0; +} + +[id=\"foo"] { + margin: 0; +} + +#menu, #nav, #footer { + color: royalblue; +} diff --git a/test/mitmproxy/contentviews/test_css_data/selectors.css b/test/mitmproxy/contentviews/test_css_data/selectors.css new file mode 100644 index 00000000..dc36f9e5 --- /dev/null +++ b/test/mitmproxy/contentviews/test_css_data/selectors.css @@ -0,0 +1,5 @@ +* { border: 0px solid blue; } +div[class="{}"] { color: red; } +a[id=\"foo"] { padding: 0; } +[id=\"foo"] { margin: 0; } +#menu, #nav, #footer { color: royalblue; } diff --git a/test/mitmproxy/contentviews/test_css_data/simple-formatted.css b/test/mitmproxy/contentviews/test_css_data/simple-formatted.css new file mode 100644 index 00000000..9435236b --- /dev/null +++ b/test/mitmproxy/contentviews/test_css_data/simple-formatted.css @@ -0,0 +1,16 @@ +menu { + color: blue; +} + +box { + border-radius: 4px; + background-color: red +} + +a { + color: green +} + +b { + color: red +} diff --git a/test/mitmproxy/contentviews/test_css_data/simple.css b/test/mitmproxy/contentviews/test_css_data/simple.css new file mode 100644 index 00000000..33b29a03 --- /dev/null +++ b/test/mitmproxy/contentviews/test_css_data/simple.css @@ -0,0 +1,5 @@ +menu { color: blue; } + +box { border-radius: 4px; background-color: red } +a { color: green } +b { color: red } diff --git a/test/mitmproxy/contentviews/test_html_outline.py b/test/mitmproxy/contentviews/test_html_outline.py deleted file mode 100644 index 9e664e52..00000000 --- a/test/mitmproxy/contentviews/test_html_outline.py +++ /dev/null @@ -1,9 +0,0 @@ -from mitmproxy.contentviews import html_outline -from test.mitmproxy.contentviews import full_eval - - -def test_view_html_outline(): - v = full_eval(html_outline.ViewHTMLOutline()) - s = b"<html><br><br></br><p>one</p></html>" - assert v(s) - assert v(b'\xfe') diff --git a/test/mitmproxy/contentviews/test_javascript.py b/test/mitmproxy/contentviews/test_javascript.py index 43039c93..23dd106e 100644 --- a/test/mitmproxy/contentviews/test_javascript.py +++ b/test/mitmproxy/contentviews/test_javascript.py @@ -1,10 +1,32 @@ +import pytest + from mitmproxy.contentviews import javascript +from mitmproxy.test import tutils from . import full_eval +data = tutils.test_data.push("mitmproxy/contentviews/test_js_data/") + def test_view_javascript(): v = full_eval(javascript.ViewJavaScript()) assert v(b"[1, 2, 3]") assert v(b"[1, 2, 3") - assert v(b"function(a){[1, 2, 3]}") + assert v(b"function(a){[1, 2, 3]}") == ("JavaScript", [ + [('text', 'function(a) {')], + [('text', ' [1, 2, 3]')], + [('text', '}')] + ]) assert v(b"\xfe") # invalid utf-8 + + +@pytest.mark.parametrize("filename", [ + "simple.js", +]) +def test_format_xml(filename): + path = data.path(filename) + with open(path) as f: + input = f.read() + with open("-formatted.".join(path.rsplit(".", 1))) as f: + expected = f.read() + js = javascript.beautify(input) + assert js == expected diff --git a/test/mitmproxy/contentviews/test_js_data/simple-formatted.js b/test/mitmproxy/contentviews/test_js_data/simple-formatted.js new file mode 100644 index 00000000..2b665f02 --- /dev/null +++ b/test/mitmproxy/contentviews/test_js_data/simple-formatted.js @@ -0,0 +1,68 @@ +/* _GlobalPrefix_ */ +this.gbar_=this.gbar_||{}; +(function(_) { + var window=this; + +/* _Module_:sy25 */ + try { + var Mn=function(){}; + _.y(Mn,Error); + _.Nn=function() { + this.b="pending"; + this.B=[]; + this.w=this.C=void 0 + }; + _.fe(_.Nn); + var On=function() { + _.qa.call(this,"Multiple attempts to set the state of this Result") + }; + _.y(On,_.qa); + _.Nn.prototype.ta=function() { + return this.C + }; + _.Pn=function(a,c,d) { + "pending"==a.b?a.B.push( { + hb:c,scope:d||null + } + ):c.call(d,a) + }; + _.Nn.prototype.A=function(a) { + if("pending"==this.b)this.C=a,this.b="success",Qn(this); + else if(!Rn(this))throw new On; + }; + _.Nn.prototype.o=function(a) { + if("pending"==this.b)this.w=a,this.b="error",Qn(this); + else if(!Rn(this))throw new On; + }; + var Qn=function(a) { + var c=a.B; + a.B=[]; + for(var d=0;d<c.length;d++) { + var e=c[d]; + e.hb.call(e.scope,a) + } + + }; + _.Nn.prototype.cancel=function() { + return"pending"==this.b?(this.o(new Mn),!0):!1 + }; + var Rn=function(a) { + return"error"==a.b&&a.w instanceof Mn + }; + _.Nn.prototype.then=function(a,c,d) { + var e,f,g=new _.ie(function(a,c) { + e=a; + f=c + } + ); + _.Pn(this,function(a) { + Rn(a)?g.cancel():"success"==a.b?e(a.ta()):"error"==a.b&&f(a.w) + } + ); + return g.then(a,c,d) + }; + + } + catch(e) { + _._DumpException(e) + } diff --git a/test/mitmproxy/contentviews/test_js_data/simple.js b/test/mitmproxy/contentviews/test_js_data/simple.js new file mode 100644 index 00000000..66a56d8b --- /dev/null +++ b/test/mitmproxy/contentviews/test_js_data/simple.js @@ -0,0 +1,8 @@ +/* _GlobalPrefix_ */ +this.gbar_=this.gbar_||{};(function(_){var window=this; +/* _Module_:sy25 */ +try{ +var Mn=function(){};_.y(Mn,Error);_.Nn=function(){this.b="pending";this.B=[];this.w=this.C=void 0};_.fe(_.Nn);var On=function(){_.qa.call(this,"Multiple attempts to set the state of this Result")};_.y(On,_.qa);_.Nn.prototype.ta=function(){return this.C};_.Pn=function(a,c,d){"pending"==a.b?a.B.push({hb:c,scope:d||null}):c.call(d,a)};_.Nn.prototype.A=function(a){if("pending"==this.b)this.C=a,this.b="success",Qn(this);else if(!Rn(this))throw new On;}; +_.Nn.prototype.o=function(a){if("pending"==this.b)this.w=a,this.b="error",Qn(this);else if(!Rn(this))throw new On;};var Qn=function(a){var c=a.B;a.B=[];for(var d=0;d<c.length;d++){var e=c[d];e.hb.call(e.scope,a)}};_.Nn.prototype.cancel=function(){return"pending"==this.b?(this.o(new Mn),!0):!1};var Rn=function(a){return"error"==a.b&&a.w instanceof Mn}; _.Nn.prototype.then=function(a,c,d){var e,f,g=new _.ie(function(a,c){e=a;f=c});_.Pn(this,function(a){Rn(a)?g.cancel():"success"==a.b?e(a.ta()):"error"==a.b&&f(a.w)});return g.then(a,c,d)}; + +}catch(e){_._DumpException(e)} diff --git a/test/mitmproxy/contentviews/test_protobuf.py b/test/mitmproxy/contentviews/test_protobuf.py index 71e51576..6c6e37f2 100644 --- a/test/mitmproxy/contentviews/test_protobuf.py +++ b/test/mitmproxy/contentviews/test_protobuf.py @@ -1,52 +1,31 @@ -from unittest import mock import pytest from mitmproxy.contentviews import protobuf from mitmproxy.test import tutils from . import full_eval +data = tutils.test_data.push("mitmproxy/contentviews/test_protobuf_data/") + def test_view_protobuf_request(): v = full_eval(protobuf.ViewProtobuf()) - p = tutils.test_data.path("mitmproxy/data/protobuf01") - - with mock.patch('mitmproxy.contentviews.protobuf.ViewProtobuf.is_available'): - with mock.patch('subprocess.Popen') as n: - m = mock.Mock() - attrs = {'communicate.return_value': (b'1: "3bbc333c-e61c-433b-819a-0b9a8cc103b8"', True)} - m.configure_mock(**attrs) - n.return_value = m - - with open(p, "rb") as f: - data = f.read() - content_type, output = v(data) - assert content_type == "Protobuf" - assert output[0] == [('text', b'1: "3bbc333c-e61c-433b-819a-0b9a8cc103b8"')] - - m.communicate = mock.MagicMock() - m.communicate.return_value = (None, None) - with pytest.raises(ValueError, matches="Failed to parse input."): - v(b'foobar') - - -def test_view_protobuf_availability(): - with mock.patch('subprocess.Popen') as n: - m = mock.Mock() - attrs = {'communicate.return_value': (b'libprotoc fake version', True)} - m.configure_mock(**attrs) - n.return_value = m - assert protobuf.ViewProtobuf().is_available() - - m = mock.Mock() - attrs = {'communicate.return_value': (b'command not found', True)} - m.configure_mock(**attrs) - n.return_value = m - assert not protobuf.ViewProtobuf().is_available() - - -def test_view_protobuf_fallback(): - with mock.patch('subprocess.Popen.communicate') as m: - m.side_effect = OSError() - v = full_eval(protobuf.ViewProtobuf()) - with pytest.raises(NotImplementedError, matches='protoc not found'): - v(b'foobar') + p = data.path("protobuf01") + + with open(p, "rb") as f: + raw = f.read() + content_type, output = v(raw) + assert content_type == "Protobuf" + assert output == [[('text', '1: 3bbc333c-e61c-433b-819a-0b9a8cc103b8')]] + with pytest.raises(ValueError, matches="Failed to parse input."): + v(b'foobar') + + +@pytest.mark.parametrize("filename", ["protobuf02", "protobuf03"]) +def test_format_pbuf(filename): + path = data.path(filename) + with open(path, "rb") as f: + input = f.read() + with open(path + "-decoded") as f: + expected = f.read() + + assert protobuf.format_pbuf(input) == expected diff --git a/test/mitmproxy/data/protobuf01 b/test/mitmproxy/contentviews/test_protobuf_data/protobuf01 index fbfdbff3..fbfdbff3 100644 --- a/test/mitmproxy/data/protobuf01 +++ b/test/mitmproxy/contentviews/test_protobuf_data/protobuf01 diff --git a/test/mitmproxy/contentviews/test_protobuf_data/protobuf02 b/test/mitmproxy/contentviews/test_protobuf_data/protobuf02 Binary files differnew file mode 100644 index 00000000..a47c45d5 --- /dev/null +++ b/test/mitmproxy/contentviews/test_protobuf_data/protobuf02 diff --git a/test/mitmproxy/contentviews/test_protobuf_data/protobuf02-decoded b/test/mitmproxy/contentviews/test_protobuf_data/protobuf02-decoded new file mode 100644 index 00000000..9be61e28 --- /dev/null +++ b/test/mitmproxy/contentviews/test_protobuf_data/protobuf02-decoded @@ -0,0 +1,65 @@ +1 { + 1: tpbuf + 4 { + 1: Person + 2 { + 1: name + 3: 1 + 4: 2 + 5: 9 + } + 2 { + 1: id + 3: 2 + 4: 2 + 5: 5 + } + 2 { + 1 { + 12: 1818845549 + } + 3: 3 + 4: 1 + 5: 9 + } + 2 { + 1: phone + 3: 4 + 4: 3 + 5: 11 + 6: .Person.PhoneNumber + } + 3 { + 1: PhoneNumber + 2 { + 1: number + 3: 1 + 4: 2 + 5: 9 + } + 2 { + 1: type + 3: 2 + 4: 1 + 5: 14 + 6: .Person.PhoneType + 7: HOME + } + } + 4 { + 1: PhoneType + 2 { + 1: MOBILE + 2: 0 + } + 2 { + 1: HOME + 2: 1 + } + 2 { + 1: WORK + 2: 2 + } + } + } +} diff --git a/test/mitmproxy/contentviews/test_protobuf_data/protobuf03 b/test/mitmproxy/contentviews/test_protobuf_data/protobuf03 new file mode 100644 index 00000000..9fb230b3 --- /dev/null +++ b/test/mitmproxy/contentviews/test_protobuf_data/protobuf03 @@ -0,0 +1 @@ +€ ð
\ No newline at end of file diff --git a/test/mitmproxy/contentviews/test_protobuf_data/protobuf03-decoded b/test/mitmproxy/contentviews/test_protobuf_data/protobuf03-decoded new file mode 100644 index 00000000..3d3392e1 --- /dev/null +++ b/test/mitmproxy/contentviews/test_protobuf_data/protobuf03-decoded @@ -0,0 +1,4 @@ +2 { +3: 3840 +4: 2160 +} diff --git a/test/mitmproxy/contentviews/test_query.py b/test/mitmproxy/contentviews/test_query.py index d2bddd05..741b23f1 100644 --- a/test/mitmproxy/contentviews/test_query.py +++ b/test/mitmproxy/contentviews/test_query.py @@ -1,5 +1,5 @@ from mitmproxy.contentviews import query -from mitmproxy.types import multidict +from mitmproxy.coretypes import multidict from . import full_eval diff --git a/test/mitmproxy/types/__init__.py b/test/mitmproxy/coretypes/__init__.py index e69de29b..e69de29b 100644 --- a/test/mitmproxy/types/__init__.py +++ b/test/mitmproxy/coretypes/__init__.py diff --git a/test/mitmproxy/types/test_basethread.py b/test/mitmproxy/coretypes/test_basethread.py index a91588eb..4a383fea 100644 --- a/test/mitmproxy/types/test_basethread.py +++ b/test/mitmproxy/coretypes/test_basethread.py @@ -1,5 +1,5 @@ import re -from mitmproxy.types import basethread +from mitmproxy.coretypes import basethread def test_basethread(): diff --git a/test/mitmproxy/types/test_bidi.py b/test/mitmproxy/coretypes/test_bidi.py index e3a259fd..3bdad3c2 100644 --- a/test/mitmproxy/types/test_bidi.py +++ b/test/mitmproxy/coretypes/test_bidi.py @@ -1,5 +1,5 @@ import pytest -from mitmproxy.types import bidi +from mitmproxy.coretypes import bidi def test_bidi(): diff --git a/test/mitmproxy/types/test_multidict.py b/test/mitmproxy/coretypes/test_multidict.py index c76cd753..273d8ca2 100644 --- a/test/mitmproxy/types/test_multidict.py +++ b/test/mitmproxy/coretypes/test_multidict.py @@ -1,6 +1,6 @@ import pytest -from mitmproxy.types import multidict +from mitmproxy.coretypes import multidict class _TMulti: diff --git a/test/mitmproxy/types/test_serializable.py b/test/mitmproxy/coretypes/test_serializable.py index 390d17e1..a316f876 100644 --- a/test/mitmproxy/types/test_serializable.py +++ b/test/mitmproxy/coretypes/test_serializable.py @@ -1,6 +1,6 @@ import copy -from mitmproxy.types import serializable +from mitmproxy.coretypes import serializable class SerializableDummy(serializable.Serializable): diff --git a/test/mitmproxy/data/addonscripts/concurrent_decorator_class.py b/test/mitmproxy/data/addonscripts/concurrent_decorator_class.py index 2a7d300c..b52f55c5 100644 --- a/test/mitmproxy/data/addonscripts/concurrent_decorator_class.py +++ b/test/mitmproxy/data/addonscripts/concurrent_decorator_class.py @@ -5,7 +5,7 @@ from mitmproxy.script import concurrent class ConcurrentClass: @concurrent - def request(flow): + def request(self, flow): time.sleep(0.1) diff --git a/test/mitmproxy/data/addonscripts/load_error.py b/test/mitmproxy/data/addonscripts/load_error.py new file mode 100644 index 00000000..4c05e9ed --- /dev/null +++ b/test/mitmproxy/data/addonscripts/load_error.py @@ -0,0 +1,2 @@ +def load(_): + raise ValueError() diff --git a/test/mitmproxy/data/addonscripts/same_filename/addon.py b/test/mitmproxy/data/addonscripts/same_filename/addon.py new file mode 100644 index 00000000..c84a9b13 --- /dev/null +++ b/test/mitmproxy/data/addonscripts/same_filename/addon.py @@ -0,0 +1 @@ +foo = 42 diff --git a/test/mitmproxy/data/addonscripts/shutdown.py b/test/mitmproxy/data/addonscripts/shutdown.py new file mode 100644 index 00000000..51a99b5c --- /dev/null +++ b/test/mitmproxy/data/addonscripts/shutdown.py @@ -0,0 +1,5 @@ +from mitmproxy import ctx + + +def running(): + ctx.master.shutdown() diff --git a/test/mitmproxy/data/no_common_name.pem b/test/mitmproxy/data/no_common_name.pem index fc271a0e..d46448f5 100644 --- a/test/mitmproxy/data/no_common_name.pem +++ b/test/mitmproxy/data/no_common_name.pem @@ -1,20 +1,84 @@ -----BEGIN RSA PRIVATE KEY----- -MIIBOQIBAAJBAKVJ43C+8SjOvN9/pP/8HwzmHGQmRvdK/R6KlWdr7He6iiXDQNfH -RAp+gqX0hBRT80eRjGhSmTTBLCWiXVny4UUCAwEAAQJAUQ8nZ0d85VJd9g2XUaLH -Z4ACNGtBKk2wTKYSFyIqWZxsF5qhh7HGshJIAP6tYiX8ZW+mMSfme+zsJzWe8ChL -gQIhAM8QpAgUHnNteZvkv0XqceX1GILEWifMt+hO9yTp4dY5AiEAzFnKr77CKCri -/DPig4R/5q4KMpMx9EqJufHdGNmIA20CICMARxnufK86RCIr6oEg/hvG8Fu6YRr1 -Kekk3/XnavtRAiBVLVQ7vwKE5aNpRmMzOKZrS736aLpYvjz8IaFr+zgjXQIgdad5 -QZoTD49NTyMEgyZp70gTXcXQLrX2PgQKL4uNmoU= +MIIJKgIBAAKCAgEA4Jut+R51opC773ToeUhwJOVGnpxNqzZTDMImO141WPvKMjMs +i15f0U3OKqK8YERDfDzaAbgqz6MNgqc8QbNJ0e9VxtMUzTkCwSlbDHMFgZNyXVRX +OQEBJ/fTMlU+LimOH38QY0orifdAHH+kPUYIiqTBzgJvCy8w1o4hGSlzf2HW400d +mlRSJEVgBj6nXQENbVxmxf6f9H19eWpnLuP5aJIwP4LEsGdqLP0ESnWfZVPIAiEs +LuYkhbRXqiuJfnc1am8LexzLi4VQMCw0K4Tm1lTbapcnOUakO6orQvX7MOEKYBU+ +ogGGby0MyOTaCkuUFi0YdTtU4DEdWJ9HfogRO/uG1325/8/T+tq8RgOkp9cUvjU3 +ONqpLZC6zs0YR8OzlTbXClmV+Mr6d1qEb3jk6zWlLykLVozOS3z5vdVxpbJqM/Ct +HporAA0cSer2EGtN68OB3Hy3Bh7MPjqpSFSJ1uTQ755jS3qOzwggoQFCz2dBmfyi +7nOGHFW4h/5NaF1mnIlJJVnJIZajSNl9e6klGeXmJv4ZtiqJd/CG0jTUnGWOTimu +kSmxVNcWs2vFjwQhuRJwawzGo7O1gZPkq3/0/F+yLp5karmRJs8sQ/JDvGL4rW5Y +qW7u1WuYQeHYHscfPwf0be8teKWcURIqBoHPxdJV3s9zf8Y/AN9OFFdPqwcCAwEA +AQKCAgEAtdHQV2Ws3FhFimYc+nEFNxjSvfrRdNOZDy7rPAvbK5lH6LM8T+WpswlE +54as71DTQHMSF2o6XbMkcKtoP9ce3u7bhQPCRw7rh+ouZjmGL4pofdyUbvS9NtmL +Aae3mi7RefWmEnosHJcmMuuwzFkw+Oq+aEHYGjmtU0Hi0TeY43kUNxRp7lBr3ii6 +vtNhMAx2Dh1KpOSmH4imVe8ob/DkKR6OKBt3lUVh0eFP4+arjZ7wvaiU17I9xm5i +uMJdnx5pAyu5I4P/0YWtkBF4efIv2zj+FZ8ehWMF97adJqtxF/RULcuE1Chf5weU +3dtEFilwSzNeJShOYN3hX6gwe+Ex8NQ7EIcbkJBaxzteEbcCJJf2rUq5oHwt3MFR +H1m1iZ/H3erNHOIqr+LW5+ZXI9Lyyn9z1YTodP2KIfB6Rg8ldsaEnkuCD/S+iikO +xNo/OwJf3rHtbeRtpAfSqkSMhT57hadRTvPlBMAoFEsX3hdw1XuRLOp9YAhsjdhU +GBzD+U/kB8FlYRhPjvjT97lJ7uE2AosGgIZBwpyv/UqPU75u/3SgubLmIHoXYWBn +jPig0H3zqpT+1D+88umMP5Ka+V/KsofUvObSjipdw3U1ZekI+08SLlGQqLvA7tk/ +3WvT7QN3k0OUx261pJNHmVhPZysJT0DNpJX9x/4jFiLVBxYyomECggEBAPNtLge0 +YJGvKTTpQoKlIb/a47wncBizyeVRhjcYNRQn6JcSzPPao8x1/ARoKPGeTXqDWiHt +o8RllF1aEKbO0gTXtBM7jjwf1ueco1F4IQYc7EnA1TwNyQ0JeA5RFcpOzLOx4SK5 +67jMaAI/gQ2Q4H3uISWF+hPlckRySpwouz+xS3QP6PZ8S6lZwHF5S9LMrxTlwbPj +yAjgIllFIvs91J6mDljll1ZG601CU8piaY5zPoJVQIesA/YRK+rlHBPICOGXJBm7 +G8kbYT9EFibxklWjiz9C9G70WPulPCZpAqaqIp70nOFR+Kp5g8VuPaMKFx8p3qc/ +AQJGlO06lFVaL0UCggEBAOw1qkzBFTdBmZW9VSBHBQdTJuuuWjoLCp1tfPUYluqG +VrzIKN0dlnB2Qy5uOHd0fVTq88+EadnXwt2hOkp4uGeWOIZfE0welWmNZ1ym9vkg +PSLgPYBDSL1rDntLeAgEUYl4gnFL8zyD0uKw/+oMoMaW2jNEqmGJH9y5uG2lkFGC +A6MbEw32sfJKoWB2GvnlIPGAMfZy9Varxq/r24OeFi7J6ja89DBL9OEWsB49/CP0 +92dkS2+7KekTN9go3cFaqnseDHNUuRhHsIJBpaWSaxlUwpVP6QQtn5NMEW44YpnE +PDk/EsxwOVAGRVaS5+pBJSUbLplXRHlzVZW4lEc+f9sCggEAe0ZuSh6RzRVck9wQ +/6JqzgMm03FRdmEOPKCljJ8oujVft6ogutmdm/ygDQdGvN3DNOjyKz5ychJTKVdk +GWWhvCwUmKzPYilpps+PccGZT8Qz8UHDeu8sQvrpnq53j4WKavIJJpHrCyIRBhps +25bj6UI/7QXFWHAZBwquOBj0gtPhdzxbaQAXPQMjzxNzT6Syga29A8G12rDPFFBL +39o3I8TKfUB//IRbwzt0vYhLFoXMQSq1TD/Tnbiieglex7HEtaHZ+WHlN1ozTFvJ +sB0kU1RIP1hD+zCpI39RT85cNlTwxXjxPbZKbOKu1bv3YOrKPNDyXdYtR57A6saA +uhy61QKCAQEAnax5AIFGyzrD7duTjlc5+Ri9e0dIPUSPkmS6q9T9MJH6JkwqUudk +O7AFymGS2dJtsxifJV/LVLoc/tqX0Yxh8+un0bJ3bDFiJTJZ09Q0OjoV9UjgZNUF +IkPrR8wp1JglYXGLCVvcgwGv7NigC7jgPZAHGX/1h+QD29AxVyfUfUQfb2osPv70 +67p7nKtZ+IPFiM+9CjjUokVJ/LahMmt9fUAVUvKwweiCDxqY96cCv3HPEDo3zN6P +7GCCv40P8fi2ojZ9syLT52w7W8e8bhid2yvkM81CyyI1ShrV69BBqUj/tmru/n7P +EycMc+zeWFWiGPHbGkrRj4y4jZfHiwMiTwKCAQEAk77dpDHxxTbmW0sdRYsEgwbz +hhbi4vhaEsJODDg0WsqZYOxsfbjTXYYSClCel3QHuJGHmEqCTcj8SqWXwpQhTeW1 +Ngs1tOprfLAo+XCbUTy51b9Acx6l/EgP1n5lrjNnGgcygfAYSG34zXSnXpiJzqQP +vKXntjJii5U8d+Qzvvwf6taU5v0JqO1J2ZWmvmgZN9qgVR42KVvdBc1F6qsN4F68 +ZrY9lULqi776Z60Dfl9S/7E4BCSEyZm4PjJ39zQYMYc4B77YYv/aO4WFZBm7gbc/ +0UP8WkOzc8wYOqgLF2KatJkaOLBfbhMGaKPvjvWDfjyVfZ8I4Gqx2rI8ILHEuQ== -----END RSA PRIVATE KEY----- -----BEGIN CERTIFICATE----- -MIIBgTCCASugAwIBAgIJAKlcXsPLQAQuMA0GCSqGSIb3DQEBBQUAMA0xCzAJBgNV -BAYTAkFVMB4XDTEzMTIxMjAxMzA1NVoXDTE0MDExMTAxMzA1NVowDTELMAkGA1UE -BhMCQVUwXDANBgkqhkiG9w0BAQEFAANLADBIAkEApUnjcL7xKM6833+k//wfDOYc -ZCZG90r9HoqVZ2vsd7qKJcNA18dECn6CpfSEFFPzR5GMaFKZNMEsJaJdWfLhRQID -AQABo24wbDAdBgNVHQ4EFgQUJm8BXcVRsROy0PVt5stkB3eVnEgwPQYDVR0jBDYw -NIAUJm8BXcVRsROy0PVt5stkB3eVnEihEaQPMA0xCzAJBgNVBAYTAkFVggkAqVxe -w8tABC4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAANBAHHxcBEpWrIqtLVH -m6Yn1hgqrAbfMj9IK6zY9C5Cbad/DfUj3AZMb5u758WJK0x9brmckgqdrQsuf9He -Ef51/SU= +MIIFtTCCA52gAwIBAgIJAM/qkBYP5ExSMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTcwNzI1MDg0MDAwWhcNMTgwNzI1MDg0MDAwWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEA4Jut+R51opC773ToeUhwJOVGnpxNqzZTDMImO141WPvKMjMsi15f0U3O +KqK8YERDfDzaAbgqz6MNgqc8QbNJ0e9VxtMUzTkCwSlbDHMFgZNyXVRXOQEBJ/fT +MlU+LimOH38QY0orifdAHH+kPUYIiqTBzgJvCy8w1o4hGSlzf2HW400dmlRSJEVg +Bj6nXQENbVxmxf6f9H19eWpnLuP5aJIwP4LEsGdqLP0ESnWfZVPIAiEsLuYkhbRX +qiuJfnc1am8LexzLi4VQMCw0K4Tm1lTbapcnOUakO6orQvX7MOEKYBU+ogGGby0M +yOTaCkuUFi0YdTtU4DEdWJ9HfogRO/uG1325/8/T+tq8RgOkp9cUvjU3ONqpLZC6 +zs0YR8OzlTbXClmV+Mr6d1qEb3jk6zWlLykLVozOS3z5vdVxpbJqM/CtHporAA0c +Ser2EGtN68OB3Hy3Bh7MPjqpSFSJ1uTQ755jS3qOzwggoQFCz2dBmfyi7nOGHFW4 +h/5NaF1mnIlJJVnJIZajSNl9e6klGeXmJv4ZtiqJd/CG0jTUnGWOTimukSmxVNcW +s2vFjwQhuRJwawzGo7O1gZPkq3/0/F+yLp5karmRJs8sQ/JDvGL4rW5YqW7u1WuY +QeHYHscfPwf0be8teKWcURIqBoHPxdJV3s9zf8Y/AN9OFFdPqwcCAwEAAaOBpzCB +pDAdBgNVHQ4EFgQUZrQUSE9A8i0N4ZuQZq/F4I74QlwwdQYDVR0jBG4wbIAUZrQU +SE9A8i0N4ZuQZq/F4I74QlyhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT +b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQDP +6pAWD+RMUjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQDPeylCUY4k +nG1KoT139g5T5G1/lxgmYnDqQB1+5JYCQWsPK7sy19tD58bq3+2N2Tozu2/f/GkG +LZtouLyRciFtcAWy4LQlSR4hTLAWeik2WV/h+ovfv4XwvRuwS5PYVQNHQsOAO3ea +hX+W+w+rwI+MFlgEHJO85P61ijcNVbiTpgd0s47RViKyJVDqfhCmpobzS5eTbbXn +F1oFlV84lgEt84BE4RJxlr6fSIxZn6rQPjdbY65snol7Zs2oAt7nLb3hpZgWKobF +3xAfkC9m19nQHeYz3JlNC7sf80top2H2HEZMVVOAD+MxXkcAjNbjBRT3/KAIyWex +2fmoGRbvCIU0LFyyyk7/tG1xTgxNuBmd4Byz1LI25uz6eK4Ey8LeZmp5mvaewI53 +t65sAGBkx+LIRt0yGmMCRRFl735Ya4SJD7je1GTiw9I3Yd63dtaJTVd65kkFkLOk +LD56iJHSyCY6JkDXd8RjozIVoaXkVQh2wFq/ZfXzAgIx/u5cJQCMG2DAu6/WI74+ +7invOv7dbYfoI02N4iB57iRbPxE4gSrRayYxUVdH1R/tlXbN9Fkd30fl2WfSO897 +QC/ODA9w86FSFANhn6nv2KuKIMUSEW+5ZhBowSFIBEdAaMS7yj9uuBWmQKrWNfOh +mZJF1YiFmgRybkdKHPrlCSZyvVBdmnmM6g== -----END CERTIFICATE----- diff --git a/test/mitmproxy/net/http/http1/test_read.py b/test/mitmproxy/net/http/http1/test_read.py index b3589c92..4084c360 100644 --- a/test/mitmproxy/net/http/http1/test_read.py +++ b/test/mitmproxy/net/http/http1/test_read.py @@ -194,6 +194,17 @@ def test_expected_http_body_size(): treq(headers=Headers(content_length="42")) ) == 42 + # more than 1 content-length headers with same value + assert expected_http_body_size( + treq(headers=Headers([(b'content-length', b'42'), (b'content-length', b'42')])) + ) == 42 + + # more than 1 content-length headers with conflicting value + with pytest.raises(exceptions.HttpSyntaxException): + expected_http_body_size( + treq(headers=Headers([(b'content-length', b'42'), (b'content-length', b'45')])) + ) + # no length assert expected_http_body_size( treq(headers=Headers()) diff --git a/test/mitmproxy/net/http/test_response.py b/test/mitmproxy/net/http/test_response.py index fa1770fe..af35bab3 100644 --- a/test/mitmproxy/net/http/test_response.py +++ b/test/mitmproxy/net/http/test_response.py @@ -150,10 +150,10 @@ class TestResponseUtils: n = time.time() r.headers["date"] = email.utils.formatdate(n) pre = r.headers["date"] - r.refresh(n) + r.refresh(946681202) assert pre == r.headers["date"] - r.refresh(n + 60) + r.refresh(946681262) d = email.utils.parsedate_tz(r.headers["date"]) d = email.utils.mktime_tz(d) # Weird that this is not exact... diff --git a/test/mitmproxy/net/test_tcp.py b/test/mitmproxy/net/test_tcp.py index 73de0879..8c012e42 100644 --- a/test/mitmproxy/net/test_tcp.py +++ b/test/mitmproxy/net/test_tcp.py @@ -1,9 +1,9 @@ from io import BytesIO +import re import queue import time import socket import random -import os import threading import pytest from unittest import mock @@ -13,9 +13,9 @@ from mitmproxy import certs from mitmproxy.net import tcp from mitmproxy import exceptions from mitmproxy.test import tutils +from ...conftest import skip_no_ipv6 from . import tservers -from ...conftest import requires_alpn class EchoHandler(tcp.BaseHandler): @@ -96,7 +96,13 @@ class TestServerBind(tservers.ServerTestBase): class handler(tcp.BaseHandler): def handle(self): - self.wfile.write(str(self.connection.getpeername()).encode()) + # We may get an ipv4-mapped ipv6 address here, e.g. ::ffff:127.0.0.1. + # Those still appear as "127.0.0.1" in the table, so we need to strip the prefix. + peername = self.connection.getpeername() + address = re.sub("^::ffff:(?=\d+.\d+.\d+.\d+$)", "", peername[0]) + port = peername[1] + + self.wfile.write(str((address, port)).encode()) self.wfile.flush() def test_bind(self): @@ -114,6 +120,7 @@ class TestServerBind(tservers.ServerTestBase): pass +@skip_no_ipv6 class TestServerIPv6(tservers.ServerTestBase): handler = EchoHandler addr = ("::1", 0) @@ -171,7 +178,7 @@ class TestServerSSL(tservers.ServerTestBase): def test_echo(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl(sni="foo.com", options=SSL.OP_ALL) + c.convert_to_tls(sni="foo.com", options=SSL.OP_ALL) testval = b"echo!\n" c.wfile.write(testval) c.wfile.flush() @@ -181,7 +188,7 @@ class TestServerSSL(tservers.ServerTestBase): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): assert not c.get_current_cipher() - c.convert_to_ssl(sni="foo.com") + c.convert_to_tls(sni="foo.com") ret = c.get_current_cipher() assert ret assert "AES" in ret[0] @@ -198,7 +205,7 @@ class TestSSLv3Only(tservers.ServerTestBase): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): with pytest.raises(exceptions.TlsException): - c.convert_to_ssl(sni="foo.com") + c.convert_to_tls(sni="foo.com") class TestInvalidTrustFile(tservers.ServerTestBase): @@ -206,9 +213,9 @@ class TestInvalidTrustFile(tservers.ServerTestBase): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): with pytest.raises(exceptions.TlsException): - c.convert_to_ssl( + c.convert_to_tls( sni="example.mitmproxy.org", - verify_options=SSL.VERIFY_PEER, + verify=SSL.VERIFY_PEER, ca_pemfile=tutils.test_data.path("mitmproxy/net/data/verificationcerts/generate.py") ) @@ -224,7 +231,7 @@ class TestSSLUpstreamCertVerificationWBadServerCert(tservers.ServerTestBase): def test_mode_default_should_pass(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl() + c.convert_to_tls() # Verification errors should be saved even if connection isn't aborted # aborted @@ -238,7 +245,7 @@ class TestSSLUpstreamCertVerificationWBadServerCert(tservers.ServerTestBase): def test_mode_none_should_pass(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl(verify_options=SSL.VERIFY_NONE) + c.convert_to_tls(verify=SSL.VERIFY_NONE) # Verification errors should be saved even if connection isn't aborted assert c.ssl_verification_error @@ -252,9 +259,9 @@ class TestSSLUpstreamCertVerificationWBadServerCert(tservers.ServerTestBase): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): with pytest.raises(exceptions.InvalidCertificateException): - c.convert_to_ssl( + c.convert_to_tls( sni="example.mitmproxy.org", - verify_options=SSL.VERIFY_PEER, + verify=SSL.VERIFY_PEER, ca_pemfile=tutils.test_data.path("mitmproxy/net/data/verificationcerts/trusted-root.crt") ) @@ -277,18 +284,28 @@ class TestSSLUpstreamCertVerificationWBadHostname(tservers.ServerTestBase): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): with pytest.raises(exceptions.TlsException): - c.convert_to_ssl( - verify_options=SSL.VERIFY_PEER, + c.convert_to_tls( + verify=SSL.VERIFY_PEER, ca_pemfile=tutils.test_data.path("mitmproxy/net/data/verificationcerts/trusted-root.crt") ) + def test_mode_none_should_pass_without_sni(self): + c = tcp.TCPClient(("127.0.0.1", self.port)) + with c.connect(): + c.convert_to_tls( + verify=SSL.VERIFY_NONE, + ca_path=tutils.test_data.path("mitmproxy/net/data/verificationcerts/") + ) + + assert "'no-hostname' doesn't match" in str(c.ssl_verification_error) + def test_should_fail(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): with pytest.raises(exceptions.InvalidCertificateException): - c.convert_to_ssl( + c.convert_to_tls( sni="mitmproxy.org", - verify_options=SSL.VERIFY_PEER, + verify=SSL.VERIFY_PEER, ca_pemfile=tutils.test_data.path("mitmproxy/net/data/verificationcerts/trusted-root.crt") ) assert c.ssl_verification_error @@ -305,9 +322,9 @@ class TestSSLUpstreamCertVerificationWValidCertChain(tservers.ServerTestBase): def test_mode_strict_w_pemfile_should_pass(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl( + c.convert_to_tls( sni="example.mitmproxy.org", - verify_options=SSL.VERIFY_PEER, + verify=SSL.VERIFY_PEER, ca_pemfile=tutils.test_data.path("mitmproxy/net/data/verificationcerts/trusted-root.crt") ) @@ -321,9 +338,9 @@ class TestSSLUpstreamCertVerificationWValidCertChain(tservers.ServerTestBase): def test_mode_strict_w_cadir_should_pass(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl( + c.convert_to_tls( sni="example.mitmproxy.org", - verify_options=SSL.VERIFY_PEER, + verify=SSL.VERIFY_PEER, ca_path=tutils.test_data.path("mitmproxy/net/data/verificationcerts/") ) @@ -355,7 +372,7 @@ class TestSSLClientCert(tservers.ServerTestBase): def test_clientcert(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl( + c.convert_to_tls( cert=tutils.test_data.path("mitmproxy/net/data/clientcert/client.pem")) assert c.rfile.readline().strip() == b"1" @@ -363,7 +380,7 @@ class TestSSLClientCert(tservers.ServerTestBase): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): with pytest.raises(exceptions.TlsException): - c.convert_to_ssl(cert=tutils.test_data.path("mitmproxy/net/data/clientcert/make")) + c.convert_to_tls(cert=tutils.test_data.path("mitmproxy/net/data/clientcert/make")) class TestSNI(tservers.ServerTestBase): @@ -383,15 +400,15 @@ class TestSNI(tservers.ServerTestBase): def test_echo(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl(sni="foo.com") + c.convert_to_tls(sni="foo.com") assert c.sni == "foo.com" assert c.rfile.readline() == b"foo.com" def test_idn(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl(sni="mitmproxyäöüß.example.com") - assert c.ssl_established + c.convert_to_tls(sni="mitmproxyäöüß.example.com") + assert c.tls_established assert "doesn't match" not in str(c.ssl_verification_error) @@ -404,7 +421,7 @@ class TestServerCipherList(tservers.ServerTestBase): def test_echo(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl(sni="foo.com") + c.convert_to_tls(sni="foo.com") expected = b"['AES256-GCM-SHA384']" assert c.rfile.read(len(expected) + 2) == expected @@ -425,7 +442,7 @@ class TestServerCurrentCipher(tservers.ServerTestBase): def test_echo(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl(sni="foo.com") + c.convert_to_tls(sni="foo.com") assert b'AES256-GCM-SHA384' in c.rfile.readline() @@ -439,7 +456,7 @@ class TestServerCipherListError(tservers.ServerTestBase): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): with pytest.raises(Exception, match="handshake error"): - c.convert_to_ssl(sni="foo.com") + c.convert_to_tls(sni="foo.com") class TestClientCipherListError(tservers.ServerTestBase): @@ -452,7 +469,7 @@ class TestClientCipherListError(tservers.ServerTestBase): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): with pytest.raises(Exception, match="cipher specification"): - c.convert_to_ssl(sni="foo.com", cipher_list="bogus") + c.convert_to_tls(sni="foo.com", cipher_list="bogus") class TestSSLDisconnect(tservers.ServerTestBase): @@ -467,7 +484,7 @@ class TestSSLDisconnect(tservers.ServerTestBase): def test_echo(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl() + c.convert_to_tls() # Excercise SSL.ZeroReturnError c.rfile.read(10) c.close() @@ -484,7 +501,7 @@ class TestSSLHardDisconnect(tservers.ServerTestBase): def test_echo(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl() + c.convert_to_tls() # Exercise SSL.SysCallError c.rfile.read(10) c.close() @@ -534,39 +551,21 @@ class TestTimeOut(tservers.ServerTestBase): c.rfile.read(10) -class TestCryptographyALPN: - - def test_has_alpn(self): - if os.environ.get("OPENSSL") == "with-alpn": - assert tcp.HAS_ALPN - assert SSL._lib.Cryptography_HAS_ALPN - elif os.environ.get("OPENSSL") == "old": - assert not tcp.HAS_ALPN - assert not SSL._lib.Cryptography_HAS_ALPN - - class TestALPNClient(tservers.ServerTestBase): handler = ALPNHandler ssl = dict( alpn_select=b"bar" ) - @requires_alpn - @pytest.mark.parametrize('has_alpn,alpn_protos, expected_negotiated, expected_response', [ - (True, [b"foo", b"bar", b"fasel"], b'bar', b'bar'), - (True, [], b'', b'NONE'), - (True, None, b'', b'NONE'), - (False, [b"foo", b"bar", b"fasel"], b'', b'NONE'), - (False, [], b'', b'NONE'), - (False, None, b'', b'NONE'), + @pytest.mark.parametrize('alpn_protos, expected_negotiated, expected_response', [ + ([b"foo", b"bar", b"fasel"], b'bar', b'bar'), + ([], b'', b'NONE'), + (None, b'', b'NONE'), ]) - def test_alpn(self, monkeypatch, has_alpn, alpn_protos, expected_negotiated, expected_response): - monkeypatch.setattr(tcp, 'HAS_ALPN', has_alpn) - monkeypatch.setattr(SSL._lib, 'Cryptography_HAS_ALPN', has_alpn) - + def test_alpn(self, monkeypatch, alpn_protos, expected_negotiated, expected_response): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl(alpn_protos=alpn_protos) + c.convert_to_tls(alpn_protos=alpn_protos) assert c.get_alpn_proto_negotiated() == expected_negotiated assert c.rfile.readline().strip() == expected_response @@ -574,7 +573,7 @@ class TestALPNClient(tservers.ServerTestBase): class TestNoSSLNoALPNClient(tservers.ServerTestBase): handler = ALPNHandler - def test_no_ssl_no_alpn(self, disable_alpn): + def test_no_ssl_no_alpn(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): assert c.get_alpn_proto_negotiated() == b"" @@ -588,7 +587,7 @@ class TestSSLTimeOut(tservers.ServerTestBase): def test_timeout_client(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl() + c.convert_to_tls() c.settimeout(0.1) with pytest.raises(exceptions.TcpTimeout): c.rfile.read(10) @@ -606,7 +605,7 @@ class TestDHParams(tservers.ServerTestBase): def test_dhparams(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl() + c.convert_to_tls() ret = c.get_current_cipher() assert ret[0] == "DHE-RSA-AES256-SHA" @@ -794,10 +793,7 @@ class TestPeek(tservers.ServerTestBase): c.close() with pytest.raises(exceptions.NetlibException): - if c.rfile.peek(1) == b"": - # Workaround for Python 2 on Unix: - # Peeking a closed connection does not raise an exception here. - raise exceptions.NetlibException() + c.rfile.peek(1) class TestPeekSSL(TestPeek): @@ -805,61 +801,5 @@ class TestPeekSSL(TestPeek): def _connect(self, c): with c.connect() as conn: - c.convert_to_ssl() + c.convert_to_tls() return conn.pop() - - -class TestSSLKeyLogger(tservers.ServerTestBase): - handler = EchoHandler - ssl = dict( - cipher_list="AES256-SHA" - ) - - def test_log(self, tmpdir): - testval = b"echo!\n" - _logfun = tcp.log_ssl_key - - logfile = str(tmpdir.join("foo", "bar", "logfile")) - tcp.log_ssl_key = tcp.SSLKeyLogger(logfile) - - c = tcp.TCPClient(("127.0.0.1", self.port)) - with c.connect(): - c.convert_to_ssl() - c.wfile.write(testval) - c.wfile.flush() - assert c.rfile.readline() == testval - c.finish() - - tcp.log_ssl_key.close() - with open(logfile, "rb") as f: - assert f.read().count(b"CLIENT_RANDOM") == 2 - - tcp.log_ssl_key = _logfun - - def test_create_logfun(self): - assert isinstance( - tcp.SSLKeyLogger.create_logfun("test"), - tcp.SSLKeyLogger) - assert not tcp.SSLKeyLogger.create_logfun(False) - - -class TestSSLInvalid(tservers.ServerTestBase): - handler = EchoHandler - ssl = True - - def test_invalid_ssl_method_should_fail(self): - fake_ssl_method = 100500 - c = tcp.TCPClient(("127.0.0.1", self.port)) - with c.connect(): - with pytest.raises(exceptions.TlsException): - c.convert_to_ssl(method=fake_ssl_method) - - def test_alpn_error(self): - c = tcp.TCPClient(("127.0.0.1", self.port)) - with c.connect(): - if tcp.HAS_ALPN: - with pytest.raises(exceptions.TlsException, match="must be a function"): - c.create_ssl_context(alpn_select_callback="foo") - - with pytest.raises(exceptions.TlsException, match="ALPN error"): - c.create_ssl_context(alpn_select="foo", alpn_select_callback="bar") diff --git a/test/mitmproxy/net/test_tls.py b/test/mitmproxy/net/test_tls.py new file mode 100644 index 00000000..00782064 --- /dev/null +++ b/test/mitmproxy/net/test_tls.py @@ -0,0 +1,55 @@ +import pytest + +from mitmproxy import exceptions +from mitmproxy.net import tls +from mitmproxy.net.tcp import TCPClient +from test.mitmproxy.net.test_tcp import EchoHandler +from . import tservers + + +class TestMasterSecretLogger(tservers.ServerTestBase): + handler = EchoHandler + ssl = dict( + cipher_list="AES256-SHA" + ) + + def test_log(self, tmpdir): + testval = b"echo!\n" + _logfun = tls.log_master_secret + + logfile = str(tmpdir.join("foo", "bar", "logfile")) + tls.log_master_secret = tls.MasterSecretLogger(logfile) + + c = TCPClient(("127.0.0.1", self.port)) + with c.connect(): + c.convert_to_tls() + c.wfile.write(testval) + c.wfile.flush() + assert c.rfile.readline() == testval + c.finish() + + tls.log_master_secret.close() + with open(logfile, "rb") as f: + assert f.read().count(b"CLIENT_RANDOM") == 2 + + tls.log_master_secret = _logfun + + def test_create_logfun(self): + assert isinstance( + tls.MasterSecretLogger.create_logfun("test"), + tls.MasterSecretLogger) + assert not tls.MasterSecretLogger.create_logfun(False) + + +class TestTLSInvalid: + def test_invalid_ssl_method_should_fail(self): + fake_ssl_method = 100500 + with pytest.raises(exceptions.TlsException): + tls.create_client_context(method=fake_ssl_method) + + def test_alpn_error(self): + with pytest.raises(exceptions.TlsException, match="must be a function"): + tls.create_client_context(alpn_select_callback="foo") + + with pytest.raises(exceptions.TlsException, match="ALPN error"): + tls.create_client_context(alpn_select="foo", alpn_select_callback="bar") diff --git a/test/mitmproxy/net/tools/getcertnames b/test/mitmproxy/net/tools/getcertnames index d64e5ff5..9349415f 100644 --- a/test/mitmproxy/net/tools/getcertnames +++ b/test/mitmproxy/net/tools/getcertnames @@ -7,7 +7,7 @@ from mitmproxy.net import tcp def get_remote_cert(host, port, sni): c = tcp.TCPClient((host, port)) c.connect() - c.convert_to_ssl(sni=sni) + c.convert_to_tls(sni=sni) return c.cert if len(sys.argv) > 2: diff --git a/test/mitmproxy/net/tservers.py b/test/mitmproxy/net/tservers.py index 44701aa5..22e195e3 100644 --- a/test/mitmproxy/net/tservers.py +++ b/test/mitmproxy/net/tservers.py @@ -60,7 +60,7 @@ class _TServer(tcp.TCPServer): else: method = OpenSSL.SSL.SSLv23_METHOD options = None - h.convert_to_ssl( + h.convert_to_tls( cert, key, method=method, diff --git a/test/mitmproxy/platform/test_pf.py b/test/mitmproxy/platform/test_pf.py index 3292d345..b048a697 100644 --- a/test/mitmproxy/platform/test_pf.py +++ b/test/mitmproxy/platform/test_pf.py @@ -15,6 +15,7 @@ class TestLookup: d = f.read() assert pf.lookup("192.168.1.111", 40000, d) == ("5.5.5.5", 80) + assert pf.lookup("::ffff:192.168.1.111", 40000, d) == ("5.5.5.5", 80) with pytest.raises(Exception, match="Could not resolve original destination"): pf.lookup("192.168.1.112", 40000, d) with pytest.raises(Exception, match="Could not resolve original destination"): diff --git a/test/mitmproxy/proxy/protocol/test_http1.py b/test/mitmproxy/proxy/protocol/test_http1.py index b642afb3..4cca370c 100644 --- a/test/mitmproxy/proxy/protocol/test_http1.py +++ b/test/mitmproxy/proxy/protocol/test_http1.py @@ -1,3 +1,6 @@ +from unittest import mock +import pytest + from mitmproxy.test import tflow from mitmproxy.net.http import http1 from mitmproxy.net.tcp import TCPClient @@ -77,3 +80,32 @@ class TestHeadContentLength(tservers.HTTPProxyTest): """head:'%s/p/200:h"Content-Length"="42"'""" % self.server.urlbase ) assert resp.headers["Content-Length"] == "42" + + +class TestStreaming(tservers.HTTPProxyTest): + + @pytest.mark.parametrize('streaming', [True, False]) + def test_streaming(self, streaming): + + class Stream: + def requestheaders(self, f): + f.request.stream = streaming + + def responseheaders(self, f): + f.response.stream = streaming + + def assert_write(self, v): + if streaming: + assert len(v) <= 4096 + return self.o.write(v) + + self.master.addons.add(Stream()) + p = self.pathoc() + with p.connect(): + with mock.patch("mitmproxy.net.tcp.Writer.write", side_effect=assert_write, autospec=True): + # response with 10000 bytes + r = p.request("post:'%s/p/200:b@10000'" % self.server.urlbase) + assert len(r.content) == 10000 + + # request with 10000 bytes + assert p.request("post:'%s/p/200':b@10000" % self.server.urlbase) diff --git a/test/mitmproxy/proxy/protocol/test_http2.py b/test/mitmproxy/proxy/protocol/test_http2.py index 261f8415..194a57c9 100644 --- a/test/mitmproxy/proxy/protocol/test_http2.py +++ b/test/mitmproxy/proxy/protocol/test_http2.py @@ -8,15 +8,14 @@ import pytest import h2 from mitmproxy import options -from mitmproxy.proxy.config import ProxyConfig import mitmproxy.net from ...net import tservers as net_tservers from mitmproxy import exceptions from mitmproxy.net.http import http1, http2 +from pathod.language import generators from ... import tservers -from ....conftest import requires_alpn import logging logging.getLogger("hyper.packages.hpack.hpack").setLevel(logging.WARNING) @@ -89,10 +88,8 @@ class _Http2TestBase: @classmethod def setup_class(cls): - opts = cls.get_options() - cls.config = ProxyConfig(opts) - - tmaster = tservers.TestMaster(opts, cls.config) + cls.options = cls.get_options() + tmaster = tservers.TestMaster(cls.options) cls.proxy = tservers.ProxyThread(tmaster) cls.proxy.start() @@ -144,7 +141,7 @@ class _Http2TestBase: while self.client.rfile.readline() != b"\r\n": pass - self.client.convert_to_ssl(alpn_protos=[b'h2']) + self.client.convert_to_tls(alpn_protos=[b'h2']) config = h2.config.H2Configuration( client_side=True, @@ -166,7 +163,8 @@ class _Http2TestBase: end_stream=None, priority_exclusive=None, priority_depends_on=None, - priority_weight=None): + priority_weight=None, + streaming=False): if headers is None: headers = [] if end_stream is None: @@ -182,7 +180,8 @@ class _Http2TestBase: ) if body: h2_conn.send_data(stream_id, body) - h2_conn.end_stream(stream_id) + if not streaming: + h2_conn.end_stream(stream_id) wfile.write(h2_conn.data_to_send()) wfile.flush() @@ -200,7 +199,6 @@ class _Http2Test(_Http2TestBase, _Http2ServerBase): _Http2ServerBase.teardown_class() -@requires_alpn class TestSimple(_Http2Test): request_body_buffer = b'' @@ -283,7 +281,6 @@ class TestSimple(_Http2Test): assert response_body_buffer == b'response body' -@requires_alpn class TestRequestWithPriority(_Http2Test): @classmethod @@ -319,7 +316,7 @@ class TestRequestWithPriority(_Http2Test): (False, (None, None, None), (None, None, None)), ]) def test_request_with_priority(self, http2_priority_enabled, priority, expected_priority): - self.config.options.http2_priority = http2_priority_enabled + self.options.http2_priority = http2_priority_enabled h2_conn = self.setup_connection() @@ -365,7 +362,6 @@ class TestRequestWithPriority(_Http2Test): assert resp.headers.get('priority_weight', None) == expected_priority[2] -@requires_alpn class TestPriority(_Http2Test): @classmethod @@ -398,7 +394,7 @@ class TestPriority(_Http2Test): (False, (True, 42424242, 42), []), ]) def test_priority(self, prioritize_before, http2_priority_enabled, priority, expected_priority): - self.config.options.http2_priority = http2_priority_enabled + self.options.http2_priority = http2_priority_enabled self.__class__.priority_data = [] h2_conn = self.setup_connection() @@ -450,7 +446,6 @@ class TestPriority(_Http2Test): assert self.priority_data == expected_priority -@requires_alpn class TestStreamResetFromServer(_Http2Test): @classmethod @@ -501,7 +496,6 @@ class TestStreamResetFromServer(_Http2Test): assert self.master.state.flows[0].response is None -@requires_alpn class TestBodySizeLimit(_Http2Test): @classmethod @@ -511,8 +505,7 @@ class TestBodySizeLimit(_Http2Test): return True def test_body_size_limit(self): - self.config.options.body_size_limit = "20" - self.config.options._processed["body_size_limit"] = 20 + self.options.body_size_limit = "20" h2_conn = self.setup_connection() @@ -551,7 +544,6 @@ class TestBodySizeLimit(_Http2Test): assert len(self.master.state.flows) == 0 -@requires_alpn class TestPushPromise(_Http2Test): @classmethod @@ -720,7 +712,6 @@ class TestPushPromise(_Http2Test): # the other two bodies might not be transmitted before the reset -@requires_alpn class TestConnectionLost(_Http2Test): @classmethod @@ -762,7 +753,6 @@ class TestConnectionLost(_Http2Test): assert self.master.state.flows[0].response is None -@requires_alpn class TestMaxConcurrentStreams(_Http2Test): @classmethod @@ -823,7 +813,6 @@ class TestMaxConcurrentStreams(_Http2Test): assert b"Stream-ID " in flow.response.content -@requires_alpn class TestConnectionTerminated(_Http2Test): @classmethod @@ -862,3 +851,118 @@ class TestConnectionTerminated(_Http2Test): assert connection_terminated_event.error_code == 5 assert connection_terminated_event.last_stream_id == 42 assert connection_terminated_event.additional_data == b'foobar' + + +class TestRequestStreaming(_Http2Test): + + @classmethod + def handle_server_event(cls, event, h2_conn, rfile, wfile): + if isinstance(event, h2.events.ConnectionTerminated): + return False + elif isinstance(event, h2.events.DataReceived): + data = event.data + assert data + h2_conn.close_connection(error_code=5, last_stream_id=42, additional_data=data) + wfile.write(h2_conn.data_to_send()) + wfile.flush() + + return True + + @pytest.mark.parametrize('streaming', [True, False]) + def test_request_streaming(self, streaming): + class Stream: + def requestheaders(self, f): + f.request.stream = streaming + + self.master.addons.add(Stream()) + h2_conn = self.setup_connection() + body = generators.RandomGenerator("bytes", 100)[:] + self._send_request( + self.client.wfile, + h2_conn, + headers=[ + (':authority', "127.0.0.1:{}".format(self.server.server.address[1])), + (':method', 'GET'), + (':scheme', 'https'), + (':path', '/'), + + ], + body=body, + streaming=True + ) + done = False + connection_terminated_event = None + self.client.rfile.o.settimeout(2) + while not done: + try: + raw = b''.join(http2.read_raw_frame(self.client.rfile)) + events = h2_conn.receive_data(raw) + + for event in events: + if isinstance(event, h2.events.ConnectionTerminated): + connection_terminated_event = event + done = True + except: + break + + if streaming: + assert connection_terminated_event.additional_data == body + else: + assert connection_terminated_event is None + + +class TestResponseStreaming(_Http2Test): + + @classmethod + def handle_server_event(cls, event, h2_conn, rfile, wfile): + if isinstance(event, h2.events.ConnectionTerminated): + return False + elif isinstance(event, h2.events.RequestReceived): + data = generators.RandomGenerator("bytes", 100)[:] + h2_conn.send_headers(event.stream_id, [ + (':status', '200'), + ('content-length', '100') + ]) + h2_conn.send_data(event.stream_id, data) + wfile.write(h2_conn.data_to_send()) + wfile.flush() + return True + + @pytest.mark.parametrize('streaming', [True, False]) + def test_response_streaming(self, streaming): + class Stream: + def responseheaders(self, f): + f.response.stream = streaming + + self.master.addons.add(Stream()) + h2_conn = self.setup_connection() + self._send_request( + self.client.wfile, + h2_conn, + headers=[ + (':authority', "127.0.0.1:{}".format(self.server.server.address[1])), + (':method', 'GET'), + (':scheme', 'https'), + (':path', '/'), + + ] + ) + done = False + self.client.rfile.o.settimeout(2) + data = None + while not done: + try: + raw = b''.join(http2.read_raw_frame(self.client.rfile)) + events = h2_conn.receive_data(raw) + + for event in events: + if isinstance(event, h2.events.DataReceived): + data = event.data + done = True + except: + break + + if streaming: + assert data + else: + assert data is None diff --git a/test/mitmproxy/proxy/protocol/test_tls.py b/test/mitmproxy/proxy/protocol/test_tls.py index 980ba7bd..e17ee46f 100644 --- a/test/mitmproxy/proxy/protocol/test_tls.py +++ b/test/mitmproxy/proxy/protocol/test_tls.py @@ -23,5 +23,4 @@ class TestClientHello: ) c = TlsClientHello(data) assert c.sni == 'example.com' - assert c.alpn_protocols[0].name == b'h2' - assert c.alpn_protocols[1].name == b'http/1.1' + assert c.alpn_protocols == [b'h2', b'http/1.1'] diff --git a/test/mitmproxy/proxy/protocol/test_websocket.py b/test/mitmproxy/proxy/protocol/test_websocket.py index f78e173f..5cd9601c 100644 --- a/test/mitmproxy/proxy/protocol/test_websocket.py +++ b/test/mitmproxy/proxy/protocol/test_websocket.py @@ -1,5 +1,6 @@ import pytest import os +import struct import tempfile import traceback @@ -7,7 +8,6 @@ from mitmproxy import options from mitmproxy import exceptions from mitmproxy.http import HTTPFlow from mitmproxy.websocket import WebSocketFlow -from mitmproxy.proxy.config import ProxyConfig from mitmproxy.net import tcp from mitmproxy.net import http @@ -34,6 +34,7 @@ class _WebSocketServerBase(net_tservers.ServerTestBase): connection='upgrade', upgrade='websocket', sec_websocket_accept=b'', + sec_websocket_extensions='permessage-deflate' if "permessage-deflate" in request.headers.values() else '' ), content=b'', ) @@ -49,10 +50,8 @@ class _WebSocketTestBase: @classmethod def setup_class(cls): - opts = cls.get_options() - cls.config = ProxyConfig(opts) - - tmaster = tservers.TestMaster(opts, cls.config) + cls.options = cls.get_options() + tmaster = tservers.TestMaster(cls.options) cls.proxy = tservers.ProxyThread(tmaster) cls.proxy.start() @@ -83,7 +82,7 @@ class _WebSocketTestBase: if self.client: self.client.close() - def setup_connection(self): + def setup_connection(self, extension=False): self.client = tcp.TCPClient(("127.0.0.1", self.proxy.port)) self.client.connect() @@ -102,8 +101,8 @@ class _WebSocketTestBase: response = http.http1.read_response(self.client.rfile, request) if self.ssl: - self.client.convert_to_ssl() - assert self.client.ssl_established + self.client.convert_to_tls() + assert self.client.tls_established request = http.Request( "relative", @@ -118,6 +117,7 @@ class _WebSocketTestBase: upgrade="websocket", sec_websocket_version="13", sec_websocket_key="1234", + sec_websocket_extensions="permessage-deflate" if extension else "" ), content=b'') self.client.wfile.write(http.http1.assemble_request(request)) @@ -148,49 +148,97 @@ class TestSimple(_WebSocketTest): wfile.flush() frame = websockets.Frame.from_file(rfile) - wfile.write(bytes(frame)) + wfile.write(bytes(websockets.Frame(fin=1, opcode=frame.header.opcode, payload=frame.payload))) wfile.flush() frame = websockets.Frame.from_file(rfile) - wfile.write(bytes(frame)) + wfile.write(bytes(websockets.Frame(fin=1, opcode=frame.header.opcode, payload=frame.payload))) wfile.flush() - def test_simple(self): + @pytest.mark.parametrize('streaming', [True, False]) + def test_simple(self, streaming): + class Stream: + def websocket_start(self, f): + f.stream = streaming + + self.master.addons.add(Stream()) self.setup_connection() frame = websockets.Frame.from_file(self.client.rfile) assert frame.payload == b'server-foobar' - self.client.wfile.write(bytes(websockets.Frame(fin=1, opcode=websockets.OPCODE.TEXT, payload=b'self.client-foobar'))) + self.client.wfile.write(bytes(websockets.Frame(fin=1, mask=1, opcode=websockets.OPCODE.TEXT, payload=b'self.client-foobar'))) self.client.wfile.flush() frame = websockets.Frame.from_file(self.client.rfile) assert frame.payload == b'self.client-foobar' - self.client.wfile.write(bytes(websockets.Frame(fin=1, opcode=websockets.OPCODE.BINARY, payload=b'\xde\xad\xbe\xef'))) + self.client.wfile.write(bytes(websockets.Frame(fin=1, mask=1, opcode=websockets.OPCODE.BINARY, payload=b'\xde\xad\xbe\xef'))) self.client.wfile.flush() frame = websockets.Frame.from_file(self.client.rfile) assert frame.payload == b'\xde\xad\xbe\xef' - self.client.wfile.write(bytes(websockets.Frame(fin=1, opcode=websockets.OPCODE.CLOSE))) + self.client.wfile.write(bytes(websockets.Frame(fin=1, mask=1, opcode=websockets.OPCODE.CLOSE))) self.client.wfile.flush() assert len(self.master.state.flows) == 2 assert isinstance(self.master.state.flows[0], HTTPFlow) assert isinstance(self.master.state.flows[1], WebSocketFlow) assert len(self.master.state.flows[1].messages) == 5 - assert self.master.state.flows[1].messages[0].content == b'server-foobar' + assert self.master.state.flows[1].messages[0].content == 'server-foobar' assert self.master.state.flows[1].messages[0].type == websockets.OPCODE.TEXT - assert self.master.state.flows[1].messages[1].content == b'self.client-foobar' + assert self.master.state.flows[1].messages[1].content == 'self.client-foobar' assert self.master.state.flows[1].messages[1].type == websockets.OPCODE.TEXT - assert self.master.state.flows[1].messages[2].content == b'self.client-foobar' + assert self.master.state.flows[1].messages[2].content == 'self.client-foobar' assert self.master.state.flows[1].messages[2].type == websockets.OPCODE.TEXT assert self.master.state.flows[1].messages[3].content == b'\xde\xad\xbe\xef' assert self.master.state.flows[1].messages[3].type == websockets.OPCODE.BINARY assert self.master.state.flows[1].messages[4].content == b'\xde\xad\xbe\xef' assert self.master.state.flows[1].messages[4].type == websockets.OPCODE.BINARY + def test_change_payload(self): + class Addon: + def websocket_message(self, f): + f.messages[-1].content = "foo" + + self.master.addons.add(Addon()) + self.setup_connection() + + frame = websockets.Frame.from_file(self.client.rfile) + assert frame.payload == b'foo' + + self.client.wfile.write(bytes(websockets.Frame(fin=1, mask=1, opcode=websockets.OPCODE.TEXT, payload=b'self.client-foobar'))) + self.client.wfile.flush() + + frame = websockets.Frame.from_file(self.client.rfile) + assert frame.payload == b'foo' + + self.client.wfile.write(bytes(websockets.Frame(fin=1, mask=1, opcode=websockets.OPCODE.BINARY, payload=b'\xde\xad\xbe\xef'))) + self.client.wfile.flush() + + frame = websockets.Frame.from_file(self.client.rfile) + assert frame.payload == b'foo' + + +class TestKillFlow(_WebSocketTest): + + @classmethod + def handle_websockets(cls, rfile, wfile): + wfile.write(bytes(websockets.Frame(fin=1, opcode=websockets.OPCODE.TEXT, payload=b'server-foobar'))) + wfile.flush() + + def test_kill(self): + class KillFlow: + def websocket_message(self, f): + f.kill() + + self.master.addons.add(KillFlow()) + self.setup_connection() + + with pytest.raises(exceptions.TcpDisconnect): + websockets.Frame.from_file(self.client.rfile) + class TestSimpleTLS(_WebSocketTest): ssl = True @@ -201,7 +249,7 @@ class TestSimpleTLS(_WebSocketTest): wfile.flush() frame = websockets.Frame.from_file(rfile) - wfile.write(bytes(frame)) + wfile.write(bytes(websockets.Frame(fin=1, opcode=frame.header.opcode, payload=frame.payload))) wfile.flush() def test_simple_tls(self): @@ -210,13 +258,13 @@ class TestSimpleTLS(_WebSocketTest): frame = websockets.Frame.from_file(self.client.rfile) assert frame.payload == b'server-foobar' - self.client.wfile.write(bytes(websockets.Frame(fin=1, opcode=websockets.OPCODE.TEXT, payload=b'self.client-foobar'))) + self.client.wfile.write(bytes(websockets.Frame(fin=1, mask=1, opcode=websockets.OPCODE.TEXT, payload=b'self.client-foobar'))) self.client.wfile.flush() frame = websockets.Frame.from_file(self.client.rfile) assert frame.payload == b'self.client-foobar' - self.client.wfile.write(bytes(websockets.Frame(fin=1, opcode=websockets.OPCODE.CLOSE))) + self.client.wfile.write(bytes(websockets.Frame(fin=1, mask=1, opcode=websockets.OPCODE.CLOSE))) self.client.wfile.flush() @@ -231,22 +279,24 @@ class TestPing(_WebSocketTest): assert frame.header.opcode == websockets.OPCODE.PONG assert frame.payload == b'foobar' - wfile.write(bytes(websockets.Frame(fin=1, opcode=websockets.OPCODE.TEXT, payload=b'pong-received'))) + wfile.write(bytes(websockets.Frame(fin=1, opcode=websockets.OPCODE.PONG, payload=b'done'))) wfile.flush() + wfile.write(bytes(websockets.Frame(fin=1, opcode=websockets.OPCODE.CLOSE))) + wfile.flush() + websockets.Frame.from_file(rfile) + def test_ping(self): self.setup_connection() frame = websockets.Frame.from_file(self.client.rfile) - assert frame.header.opcode == websockets.OPCODE.PING - assert frame.payload == b'foobar' - - self.client.wfile.write(bytes(websockets.Frame(fin=1, opcode=websockets.OPCODE.PONG, payload=frame.payload))) + websockets.Frame.from_file(self.client.rfile) + self.client.wfile.write(bytes(websockets.Frame(fin=1, mask=1, opcode=websockets.OPCODE.CLOSE))) self.client.wfile.flush() + assert frame.header.opcode == websockets.OPCODE.PING + assert frame.payload == b'' # We don't send payload to other end - frame = websockets.Frame.from_file(self.client.rfile) - assert frame.header.opcode == websockets.OPCODE.TEXT - assert frame.payload == b'pong-received' + assert self.master.has_log("Pong Received from server", "info") class TestPong(_WebSocketTest): @@ -255,20 +305,29 @@ class TestPong(_WebSocketTest): def handle_websockets(cls, rfile, wfile): frame = websockets.Frame.from_file(rfile) assert frame.header.opcode == websockets.OPCODE.PING - assert frame.payload == b'foobar' + assert frame.payload == b'' wfile.write(bytes(websockets.Frame(fin=1, opcode=websockets.OPCODE.PONG, payload=frame.payload))) wfile.flush() + wfile.write(bytes(websockets.Frame(fin=1, opcode=websockets.OPCODE.CLOSE))) + wfile.flush() + websockets.Frame.from_file(rfile) + def test_pong(self): self.setup_connection() - self.client.wfile.write(bytes(websockets.Frame(fin=1, opcode=websockets.OPCODE.PING, payload=b'foobar'))) + self.client.wfile.write(bytes(websockets.Frame(fin=1, mask=1, opcode=websockets.OPCODE.PING, payload=b'foobar'))) self.client.wfile.flush() frame = websockets.Frame.from_file(self.client.rfile) + websockets.Frame.from_file(self.client.rfile) + self.client.wfile.write(bytes(websockets.Frame(fin=1, mask=1, opcode=websockets.OPCODE.CLOSE))) + self.client.wfile.flush() + assert frame.header.opcode == websockets.OPCODE.PONG assert frame.payload == b'foobar' + assert self.master.has_log("Pong Received from server", "info") class TestClose(_WebSocketTest): @@ -276,7 +335,7 @@ class TestClose(_WebSocketTest): @classmethod def handle_websockets(cls, rfile, wfile): frame = websockets.Frame.from_file(rfile) - wfile.write(bytes(frame)) + wfile.write(bytes(websockets.Frame(fin=1, opcode=frame.header.opcode, payload=frame.payload))) wfile.write(bytes(websockets.Frame(fin=1, opcode=websockets.OPCODE.CLOSE))) wfile.flush() @@ -286,7 +345,7 @@ class TestClose(_WebSocketTest): def test_close(self): self.setup_connection() - self.client.wfile.write(bytes(websockets.Frame(fin=1, opcode=websockets.OPCODE.CLOSE))) + self.client.wfile.write(bytes(websockets.Frame(fin=1, mask=1, opcode=websockets.OPCODE.CLOSE))) self.client.wfile.flush() websockets.Frame.from_file(self.client.rfile) @@ -296,7 +355,7 @@ class TestClose(_WebSocketTest): def test_close_payload_1(self): self.setup_connection() - self.client.wfile.write(bytes(websockets.Frame(fin=1, opcode=websockets.OPCODE.CLOSE, payload=b'\00\42'))) + self.client.wfile.write(bytes(websockets.Frame(fin=1, mask=1, opcode=websockets.OPCODE.CLOSE, payload=b'\00\42'))) self.client.wfile.flush() websockets.Frame.from_file(self.client.rfile) @@ -306,7 +365,7 @@ class TestClose(_WebSocketTest): def test_close_payload_2(self): self.setup_connection() - self.client.wfile.write(bytes(websockets.Frame(fin=1, opcode=websockets.OPCODE.CLOSE, payload=b'\00\42foobar'))) + self.client.wfile.write(bytes(websockets.Frame(fin=1, mask=1, opcode=websockets.OPCODE.CLOSE, payload=b'\00\42foobar'))) self.client.wfile.flush() websockets.Frame.from_file(self.client.rfile) @@ -326,5 +385,83 @@ class TestInvalidFrame(_WebSocketTest): # with pytest.raises(exceptions.TcpDisconnect): frame = websockets.Frame.from_file(self.client.rfile) - assert frame.header.opcode == 15 - assert frame.payload == b'foobar' + code, = struct.unpack('!H', frame.payload[:2]) + assert code == 1002 + assert frame.payload[2:].startswith(b'Invalid opcode') + + +class TestStreaming(_WebSocketTest): + + @classmethod + def handle_websockets(cls, rfile, wfile): + wfile.write(bytes(websockets.Frame(opcode=websockets.OPCODE.TEXT, payload=b'server-foobar'))) + wfile.flush() + + @pytest.mark.parametrize('streaming', [True, False]) + def test_streaming(self, streaming): + class Stream: + def websocket_start(self, f): + f.stream = streaming + + self.master.addons.add(Stream()) + self.setup_connection() + + frame = None + if not streaming: + with pytest.raises(exceptions.TcpDisconnect): # Reader.safe_read get nothing as result + frame = websockets.Frame.from_file(self.client.rfile) + assert frame is None + + else: + frame = websockets.Frame.from_file(self.client.rfile) + + assert frame + assert self.master.state.flows[1].messages == [] # Message not appended as the final frame isn't received + + +class TestExtension(_WebSocketTest): + + @classmethod + def handle_websockets(cls, rfile, wfile): + wfile.write(b'\xc1\x0f*N-*K-\xd2M\xcb\xcfOJ,\x02\x00') + wfile.flush() + + frame = websockets.Frame.from_file(rfile) + assert frame.header.rsv1 + wfile.write(b'\xc1\nJ\xce\xc9L\xcd+\x81r\x00\x00') + wfile.flush() + + frame = websockets.Frame.from_file(rfile) + assert frame.header.rsv1 + wfile.write(b'\xc2\x07\xba\xb7v\xdf{\x00\x00') + wfile.flush() + + def test_extension(self): + self.setup_connection(True) + + frame = websockets.Frame.from_file(self.client.rfile) + assert frame.header.rsv1 + + self.client.wfile.write(b'\xc1\x8fQ\xb7vX\x1by\xbf\x14\x9c\x9c\xa7\x15\x9ax9\x12}\xb5v') + self.client.wfile.flush() + + frame = websockets.Frame.from_file(self.client.rfile) + assert frame.header.rsv1 + + self.client.wfile.write(b'\xc2\x87\xeb\xbb\x0csQ\x0cz\xac\x90\xbb\x0c') + self.client.wfile.flush() + + frame = websockets.Frame.from_file(self.client.rfile) + assert frame.header.rsv1 + + assert len(self.master.state.flows[1].messages) == 5 + assert self.master.state.flows[1].messages[0].content == 'server-foobar' + assert self.master.state.flows[1].messages[0].type == websockets.OPCODE.TEXT + assert self.master.state.flows[1].messages[1].content == 'client-foobar' + assert self.master.state.flows[1].messages[1].type == websockets.OPCODE.TEXT + assert self.master.state.flows[1].messages[2].content == 'client-foobar' + assert self.master.state.flows[1].messages[2].type == websockets.OPCODE.TEXT + assert self.master.state.flows[1].messages[3].content == b'\xde\xad\xbe\xef' + assert self.master.state.flows[1].messages[3].type == websockets.OPCODE.BINARY + assert self.master.state.flows[1].messages[4].content == b'\xde\xad\xbe\xef' + assert self.master.state.flows[1].messages[4].type == websockets.OPCODE.BINARY diff --git a/test/mitmproxy/proxy/test_config.py b/test/mitmproxy/proxy/test_config.py index 777ab4dd..a7da980b 100644 --- a/test/mitmproxy/proxy/test_config.py +++ b/test/mitmproxy/proxy/test_config.py @@ -1 +1,38 @@ -# TODO: write tests +import pytest + +from mitmproxy import options +from mitmproxy import exceptions +from mitmproxy.proxy.config import ProxyConfig +from mitmproxy.test import tutils + + +class TestProxyConfig: + def test_upstream_cert_insecure(self): + opts = options.Options() + opts.add_upstream_certs_to_client_chain = True + with pytest.raises(exceptions.OptionsError, match="verify-upstream-cert"): + ProxyConfig(opts) + + def test_invalid_cadir(self): + opts = options.Options() + opts.cadir = "foo" + with pytest.raises(exceptions.OptionsError, match="parent directory does not exist"): + ProxyConfig(opts) + + def test_invalid_client_certs(self): + opts = options.Options() + opts.client_certs = "foo" + with pytest.raises(exceptions.OptionsError, match="certificate path does not exist"): + ProxyConfig(opts) + + def test_valid_client_certs(self): + opts = options.Options() + opts.client_certs = tutils.test_data.path("mitmproxy/data/clientcert/") + p = ProxyConfig(opts) + assert p.client_certs + + def test_invalid_certificate(self): + opts = options.Options() + opts.certs = [tutils.test_data.path("mitmproxy/data/dumpfile-011")] + with pytest.raises(exceptions.OptionsError, match="Invalid certificate format"): + ProxyConfig(opts) diff --git a/test/mitmproxy/proxy/test_server.py b/test/mitmproxy/proxy/test_server.py index bd61f600..56b7b4c9 100644 --- a/test/mitmproxy/proxy/test_server.py +++ b/test/mitmproxy/proxy/test_server.py @@ -117,13 +117,12 @@ class TcpMixin: def _ignore_on(self): assert not hasattr(self, "_ignore_backup") - self._ignore_backup = self.config.check_ignore - self.config.check_ignore = HostMatcher( - [".+:%s" % self.server.port] + self.config.check_ignore.patterns) + self._ignore_backup = self.options.ignore_hosts + self.options.ignore_hosts = [".+:%s" % self.server.port] + self.options.ignore_hosts def _ignore_off(self): assert hasattr(self, "_ignore_backup") - self.config.check_ignore = self._ignore_backup + self.options.ignore_hosts = self._ignore_backup del self._ignore_backup def test_ignore(self): @@ -144,9 +143,9 @@ class TcpMixin: # Test that we get the original SSL cert if self.ssl: - i_cert = certs.SSLCert(i.sslinfo.certchain[0]) - i2_cert = certs.SSLCert(i2.sslinfo.certchain[0]) - n_cert = certs.SSLCert(n.sslinfo.certchain[0]) + i_cert = certs.Cert(i.sslinfo.certchain[0]) + i2_cert = certs.Cert(i2.sslinfo.certchain[0]) + n_cert = certs.Cert(n.sslinfo.certchain[0]) assert i_cert == i2_cert assert i_cert != n_cert @@ -163,13 +162,12 @@ class TcpMixin: def _tcpproxy_on(self): assert not hasattr(self, "_tcpproxy_backup") - self._tcpproxy_backup = self.config.check_tcp - self.config.check_tcp = HostMatcher( - [".+:%s" % self.server.port] + self.config.check_tcp.patterns) + self._tcpproxy_backup = self.options.tcp_hosts + self.options.tcp_hosts = [".+:%s" % self.server.port] + self.options.tcp_hosts def _tcpproxy_off(self): assert hasattr(self, "_tcpproxy_backup") - self.config.check_tcp = self._tcpproxy_backup + self.options.tcp_hosts = self._tcpproxy_backup del self._tcpproxy_backup def test_tcp(self): @@ -190,11 +188,12 @@ class TcpMixin: # Test that we get the original SSL cert if self.ssl: - i_cert = certs.SSLCert(i.sslinfo.certchain[0]) - i2_cert = certs.SSLCert(i2.sslinfo.certchain[0]) - n_cert = certs.SSLCert(n.sslinfo.certchain[0]) + i_cert = certs.Cert(i.sslinfo.certchain[0]) + i2_cert = certs.Cert(i2.sslinfo.certchain[0]) + n_cert = certs.Cert(n.sslinfo.certchain[0]) - assert i_cert == i2_cert == n_cert + assert i_cert == i2_cert + assert i_cert != n_cert # Make sure that TCP messages are in the event log. # Re-enable and fix this when we start keeping TCPFlows in the state. @@ -239,13 +238,28 @@ class TestHTTP(tservers.HTTPProxyTest, CommonMixin): p.request("get:'%s'" % response) def test_reconnect(self): - req = "get:'%s/p/200:b@1:da'" % self.server.urlbase + req = "get:'%s/p/200:b@1'" % self.server.urlbase p = self.pathoc() + + class MockOnce: + call = 0 + + def mock_once(self, http1obj, req): + self.call += 1 + if self.call == 1: + raise exceptions.TcpDisconnect + else: + headers = http1.assemble_request_head(req) + http1obj.server_conn.wfile.write(headers) + http1obj.server_conn.wfile.flush() + with p.connect(): - assert p.request(req) - # Server has disconnected. Mitmproxy should detect this, and reconnect. - assert p.request(req) - assert p.request(req) + with mock.patch("mitmproxy.proxy.protocol.http1.Http1Layer.send_request_headers", + side_effect=MockOnce().mock_once, autospec=True): + # Server disconnects while sending headers but mitmproxy reconnects + resp = p.request(req) + assert resp + assert resp.status_code == 200 def test_get_connection_switching(self): req = "get:'%s/p/200:b@1'" @@ -338,22 +352,22 @@ class TestHTTPS(tservers.HTTPProxyTest, CommonMixin, TcpMixin): def test_clientcert_file(self): try: - self.config.client_certs = os.path.join( + self.options.client_certs = os.path.join( tutils.test_data.path("mitmproxy/data/clientcert"), "client.pem") f = self.pathod("304") assert f.status_code == 304 assert self.server.last_log()["request"]["clientcert"]["keyinfo"] finally: - self.config.client_certs = None + self.options.client_certs = None def test_clientcert_dir(self): try: - self.config.client_certs = tutils.test_data.path("mitmproxy/data/clientcert") + self.options.client_certs = tutils.test_data.path("mitmproxy/data/clientcert") f = self.pathod("304") assert f.status_code == 304 assert self.server.last_log()["request"]["clientcert"]["keyinfo"] finally: - self.config.client_certs = None + self.options.client_certs = None def test_error_post_connect(self): p = self.pathoc() @@ -397,7 +411,7 @@ class TestHTTPSUpstreamServerVerificationWTrustedCert(tservers.HTTPProxyTest): return p.request("get:/p/242") def test_verification_w_cadir(self): - self.config.options.update( + self.options.update( ssl_insecure=False, ssl_verify_upstream_trusted_cadir=tutils.test_data.path( "mitmproxy/data/servercert/" @@ -407,7 +421,7 @@ class TestHTTPSUpstreamServerVerificationWTrustedCert(tservers.HTTPProxyTest): assert self._request().status_code == 242 def test_verification_w_pemfile(self): - self.config.options.update( + self.options.update( ssl_insecure=False, ssl_verify_upstream_trusted_cadir=None, ssl_verify_upstream_trusted_ca=tutils.test_data.path( @@ -443,7 +457,7 @@ class TestHTTPSUpstreamServerVerificationWBadCert(tservers.HTTPProxyTest): return opts def test_no_verification_w_bad_cert(self): - self.config.options.ssl_insecure = True + self.options.ssl_insecure = True r = self._request() assert r.status_code == 242 @@ -451,10 +465,10 @@ class TestHTTPSUpstreamServerVerificationWBadCert(tservers.HTTPProxyTest): # We only test for a single invalid cert here. # Actual testing of different root-causes (invalid hostname, expired, ...) # is done in mitmproxy.net. - self.config.options.ssl_insecure = False + self.options.ssl_insecure = False r = self._request() assert r.status_code == 502 - assert b"Certificate Verification Error" in r.raw_content + assert b"Certificate verification error" in r.raw_content class TestHTTPSNoCommonName(tservers.HTTPProxyTest): @@ -465,7 +479,7 @@ class TestHTTPSNoCommonName(tservers.HTTPProxyTest): ssl = True ssloptions = pathod.SSLOptions( certs=[ - (b"*", tutils.test_data.path("mitmproxy/data/no_common_name.pem")) + ("*", tutils.test_data.path("mitmproxy/data/no_common_name.pem")) ] ) @@ -478,7 +492,7 @@ class TestReverse(tservers.ReverseProxyTest, CommonMixin, TcpMixin): reverse = True def test_host_header(self): - self.config.options.keep_host_header = True + self.options.keep_host_header = True p = self.pathoc() with p.connect(): resp = p.request("get:/p/200:h'Host'='example.com'") @@ -488,7 +502,7 @@ class TestReverse(tservers.ReverseProxyTest, CommonMixin, TcpMixin): assert req.host_header == "example.com" def test_overridden_host_header(self): - self.config.options.keep_host_header = False # default value + self.options.keep_host_header = False # default value p = self.pathoc() with p.connect(): resp = p.request("get:/p/200:h'Host'='example.com'") @@ -565,7 +579,7 @@ class TestSocks5SSL(tservers.SocksModeTest): p = self.pathoc_raw() with p.connect(): p.socks_connect(("localhost", self.server.port)) - p.convert_to_ssl() + p.convert_to_tls() f = p.request("get:/p/200") assert f.status_code == 200 @@ -695,7 +709,7 @@ class TestProxy(tservers.HTTPProxyTest): first_flow = self.master.state.flows[0] second_flow = self.master.state.flows[1] assert first_flow.server_conn.timestamp_tcp_setup - assert first_flow.server_conn.timestamp_ssl_setup is None + assert first_flow.server_conn.timestamp_tls_setup is None assert second_flow.server_conn.timestamp_tcp_setup assert first_flow.server_conn.timestamp_tcp_setup == second_flow.server_conn.timestamp_tcp_setup @@ -714,7 +728,7 @@ class TestProxySSL(tservers.HTTPProxyTest): f = self.pathod("304:b@10k") assert f.status_code == 304 first_flow = self.master.state.flows[0] - assert first_flow.server_conn.timestamp_ssl_setup + assert first_flow.server_conn.timestamp_tls_setup def test_via(self): # tests that the ssl timestamp is present when ssl is used @@ -1072,6 +1086,23 @@ class TestProxyChainingSSLReconnect(tservers.HTTPUpstreamProxyTest): proxified to an upstream http proxy, we need to send the CONNECT request again. """ + + class MockOnce: + call = 0 + + def mock_once(self, http1obj, req): + self.call += 1 + + if self.call == 2: + headers = http1.assemble_request_head(req) + http1obj.server_conn.wfile.write(headers) + http1obj.server_conn.wfile.flush() + raise exceptions.TcpDisconnect + else: + headers = http1.assemble_request_head(req) + http1obj.server_conn.wfile.write(headers) + http1obj.server_conn.wfile.flush() + self.chain[0].tmaster.addons.add(RequestKiller([1, 2])) self.chain[1].tmaster.addons.add(RequestKiller([1])) @@ -1086,7 +1117,10 @@ class TestProxyChainingSSLReconnect(tservers.HTTPUpstreamProxyTest): assert len(self.chain[0].tmaster.state.flows) == 1 assert len(self.chain[1].tmaster.state.flows) == 1 - req = p.request("get:'/p/418:b\"content2\"'") + with mock.patch("mitmproxy.proxy.protocol.http1.Http1Layer.send_request_headers", + side_effect=MockOnce().mock_once, autospec=True): + req = p.request("get:'/p/418:b\"content2\"'") + assert req.status_code == 502 assert len(self.proxy.tmaster.state.flows) == 2 @@ -1108,14 +1142,14 @@ class AddUpstreamCertsToClientChainMixin: ssloptions = pathod.SSLOptions( cn=b"example.mitmproxy.org", certs=[ - (b"example.mitmproxy.org", servercert) + ("example.mitmproxy.org", servercert) ] ) def test_add_upstream_certs_to_client_chain(self): with open(self.servercert, "rb") as f: d = f.read() - upstreamCert = certs.SSLCert.from_pem(d) + upstreamCert = certs.Cert.from_pem(d) p = self.pathoc() with p.connect(): upstream_cert_found_in_client_chain = False diff --git a/test/mitmproxy/test_addonmanager.py b/test/mitmproxy/test_addonmanager.py index 678bc1b7..274d2d90 100644 --- a/test/mitmproxy/test_addonmanager.py +++ b/test/mitmproxy/test_addonmanager.py @@ -6,7 +6,6 @@ from mitmproxy import exceptions from mitmproxy import options from mitmproxy import command from mitmproxy import master -from mitmproxy import proxy from mitmproxy.test import taddons from mitmproxy.test import tflow @@ -51,7 +50,7 @@ def test_command(): def test_halt(): o = options.Options() - m = master.Master(o, proxy.DummyServer(o)) + m = master.Master(o) a = addonmanager.AddonManager(m) halt = THalt() end = TAddon("end") @@ -68,7 +67,7 @@ def test_halt(): def test_lifecycle(): o = options.Options() - m = master.Master(o, proxy.DummyServer(o)) + m = master.Master(o) a = addonmanager.AddonManager(m) a.add(TAddon("one")) @@ -91,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" @@ -115,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") @@ -125,10 +137,12 @@ def test_simple(): a.trigger("custom") assert ta.custom_called + assert ta in a + def test_load_option(): o = options.Options() - m = master.Master(o, proxy.DummyServer(o)) + m = master.Master(o) a = addonmanager.AddonManager(m) a.add(AOption()) assert "custom_option" in m.options._options @@ -136,7 +150,7 @@ def test_load_option(): def test_nesting(): o = options.Options() - m = master.Master(o, proxy.DummyServer(o)) + m = master.Master(o) a = addonmanager.AddonManager(m) a.add( diff --git a/test/mitmproxy/test_certs.py b/test/mitmproxy/test_certs.py index 88c49561..dcc185c0 100644 --- a/test/mitmproxy/test_certs.py +++ b/test/mitmproxy/test_certs.py @@ -102,7 +102,7 @@ class TestCertStore: dc = ca2.get_cert(b"foo.com", [b"sans.example.com"]) dcp = tmpdir.join("dc") dcp.write(dc[0].to_pem()) - ca1.add_cert_file(b"foo.com", str(dcp)) + ca1.add_cert_file("foo.com", str(dcp)) ret = ca1.get_cert(b"foo.com", []) assert ret[0].serial == dc[0].serial @@ -136,18 +136,18 @@ class TestDummyCert: assert r.altnames == [] -class TestSSLCert: +class TestCert: def test_simple(self): with open(tutils.test_data.path("mitmproxy/net/data/text_cert"), "rb") as f: d = f.read() - c1 = certs.SSLCert.from_pem(d) + c1 = certs.Cert.from_pem(d) assert c1.cn == b"google.com" assert len(c1.altnames) == 436 with open(tutils.test_data.path("mitmproxy/net/data/text_cert_2"), "rb") as f: d = f.read() - c2 = certs.SSLCert.from_pem(d) + c2 = certs.Cert.from_pem(d) assert c2.cn == b"www.inode.co.nz" assert len(c2.altnames) == 2 assert c2.digest("sha1") @@ -165,20 +165,20 @@ class TestSSLCert: def test_err_broken_sans(self): with open(tutils.test_data.path("mitmproxy/net/data/text_cert_weird1"), "rb") as f: d = f.read() - c = certs.SSLCert.from_pem(d) + c = certs.Cert.from_pem(d) # This breaks unless we ignore a decoding error. assert c.altnames is not None def test_der(self): with open(tutils.test_data.path("mitmproxy/net/data/dercert"), "rb") as f: d = f.read() - s = certs.SSLCert.from_der(d) + s = certs.Cert.from_der(d) assert s.cn def test_state(self): with open(tutils.test_data.path("mitmproxy/net/data/text_cert"), "rb") as f: d = f.read() - c = certs.SSLCert.from_pem(d) + c = certs.Cert.from_pem(d) c.get_state() c2 = c.copy() @@ -188,6 +188,6 @@ class TestSSLCert: assert c == c2 assert c is not c2 - x = certs.SSLCert('') + x = certs.Cert('') x.set_state(a) assert x == c diff --git a/test/mitmproxy/test_command.py b/test/mitmproxy/test_command.py index 958328b2..c777192d 100644 --- a/test/mitmproxy/test_command.py +++ b/test/mitmproxy/test_command.py @@ -4,27 +4,56 @@ from mitmproxy import flow from mitmproxy import exceptions from mitmproxy.test import tflow from mitmproxy.test import taddons +import mitmproxy.types import io import pytest class TAddon: + @command.command("cmd1") def cmd1(self, foo: str) -> str: """cmd1 help""" return "ret " + foo + @command.command("cmd2") def cmd2(self, foo: str) -> str: return 99 + @command.command("cmd3") def cmd3(self, foo: int) -> int: return foo + @command.command("cmd4") + def cmd4(self, a: int, b: str, c: mitmproxy.types.Path) -> str: + return "ok" + + @command.command("subcommand") + def subcommand(self, cmd: mitmproxy.types.Cmd, *args: mitmproxy.types.Arg) -> str: + return "ok" + + @command.command("empty") def empty(self) -> None: pass - def varargs(self, one: str, *var: typing.Sequence[str]) -> typing.Sequence[str]: + @command.command("varargs") + def varargs(self, one: str, *var: str) -> typing.Sequence[str]: return list(var) + def choices(self) -> typing.Sequence[str]: + return ["one", "two", "three"] + + @command.argument("arg", type=mitmproxy.types.Choice("choices")) + def choose(self, arg: str) -> typing.Sequence[str]: + return ["one", "two", "three"] + + @command.command("path") + def path(self, arg: mitmproxy.types.Path) -> None: + pass + + @command.command("flow") + def flow(self, f: flow.Flow, s: str) -> None: + pass + class TestCommand: def test_varargs(self): @@ -52,6 +81,144 @@ class TestCommand: c = command.Command(cm, "cmd.three", a.cmd3) assert c.call(["1"]) == 1 + def test_parse_partial(self): + tests = [ + [ + "foo bar", + [ + command.ParseResult( + value = "foo", type = mitmproxy.types.Cmd, valid = False + ), + command.ParseResult( + value = "bar", type = mitmproxy.types.Unknown, valid = False + ) + ], + [], + ], + [ + "cmd1 'bar", + [ + command.ParseResult(value = "cmd1", type = mitmproxy.types.Cmd, valid = True), + command.ParseResult(value = "'bar", type = str, valid = True) + ], + [], + ], + [ + "a", + [command.ParseResult(value = "a", type = mitmproxy.types.Cmd, valid = False)], + [], + ], + [ + "", + [command.ParseResult(value = "", type = mitmproxy.types.Cmd, valid = False)], + [] + ], + [ + "cmd3 1", + [ + command.ParseResult(value = "cmd3", type = mitmproxy.types.Cmd, valid = True), + command.ParseResult(value = "1", type = int, valid = True), + ], + [] + ], + [ + "cmd3 ", + [ + command.ParseResult(value = "cmd3", type = mitmproxy.types.Cmd, valid = True), + command.ParseResult(value = "", type = int, valid = False), + ], + [] + ], + [ + "subcommand ", + [ + command.ParseResult( + value = "subcommand", type = mitmproxy.types.Cmd, valid = True, + ), + command.ParseResult(value = "", type = mitmproxy.types.Cmd, valid = False), + ], + ["arg"], + ], + [ + "subcommand cmd3 ", + [ + command.ParseResult(value = "subcommand", type = mitmproxy.types.Cmd, valid = True), + command.ParseResult(value = "cmd3", type = mitmproxy.types.Cmd, valid = True), + command.ParseResult(value = "", type = int, valid = False), + ], + [] + ], + [ + "cmd4", + [ + command.ParseResult(value = "cmd4", type = mitmproxy.types.Cmd, valid = True), + ], + ["int", "str", "path"] + ], + [ + "cmd4 ", + [ + command.ParseResult(value = "cmd4", type = mitmproxy.types.Cmd, valid = True), + command.ParseResult(value = "", type = int, valid = False), + ], + ["str", "path"] + ], + [ + "cmd4 1", + [ + command.ParseResult(value = "cmd4", type = mitmproxy.types.Cmd, valid = True), + command.ParseResult(value = "1", type = int, valid = True), + ], + ["str", "path"] + ], + [ + "cmd4 1", + [ + command.ParseResult(value = "cmd4", type = mitmproxy.types.Cmd, valid = True), + command.ParseResult(value = "1", type = int, valid = True), + ], + ["str", "path"] + ], + [ + "flow", + [ + command.ParseResult(value = "flow", type = mitmproxy.types.Cmd, valid = True), + ], + ["flow", "str"] + ], + [ + "flow ", + [ + command.ParseResult(value = "flow", type = mitmproxy.types.Cmd, valid = True), + command.ParseResult(value = "", type = flow.Flow, valid = False), + ], + ["str"] + ], + [ + "flow x", + [ + command.ParseResult(value = "flow", type = mitmproxy.types.Cmd, valid = True), + command.ParseResult(value = "x", type = flow.Flow, valid = False), + ], + ["str"] + ], + [ + "flow x ", + [ + command.ParseResult(value = "flow", type = mitmproxy.types.Cmd, valid = True), + command.ParseResult(value = "x", type = flow.Flow, valid = False), + command.ParseResult(value = "", type = str, valid = True), + ], + [] + ], + ] + with taddons.context() as tctx: + tctx.master.addons.add(TAddon()) + for s, expected, expectedremain in tests: + current, remain = tctx.master.commands.parse_partial(s) + assert current == expected + assert expectedremain == remain + def test_simple(): with taddons.context() as tctx: @@ -64,7 +231,7 @@ def test_simple(): c.call("nonexistent") with pytest.raises(exceptions.CommandError, match="Invalid"): c.call("") - with pytest.raises(exceptions.CommandError, match="Usage"): + with pytest.raises(exceptions.CommandError, match="argument mismatch"): c.call("one.two too many args") c.add("empty", a.empty) @@ -76,15 +243,18 @@ def test_simple(): def test_typename(): - assert command.typename(str, True) == "str" - assert command.typename(typing.Sequence[flow.Flow], True) == "[flow]" - assert command.typename(typing.Sequence[flow.Flow], False) == "flowspec" + assert command.typename(str) == "str" + assert command.typename(typing.Sequence[flow.Flow]) == "[flow]" - assert command.typename(command.Cuts, False) == "cutspec" - assert command.typename(command.Cuts, True) == "[cuts]" + assert command.typename(mitmproxy.types.Data) == "[data]" + assert command.typename(mitmproxy.types.CutSpec) == "[cut]" - assert command.typename(flow.Flow, False) == "flow" - assert command.typename(typing.Sequence[str], False) == "[str]" + assert command.typename(flow.Flow) == "flow" + assert command.typename(typing.Sequence[str]) == "[str]" + + assert command.typename(mitmproxy.types.Choice("foo")) == "choice" + assert command.typename(mitmproxy.types.Path) == "path" + assert command.typename(mitmproxy.types.Cmd) == "cmd" class DummyConsole: @@ -94,7 +264,7 @@ class DummyConsole: return [tflow.tflow(resp=True)] * n @command.command("cut") - def cut(self, spec: str) -> command.Cuts: + def cut(self, spec: str) -> mitmproxy.types.Data: return [["test"]] @@ -102,38 +272,11 @@ def test_parsearg(): with taddons.context() as tctx: tctx.master.addons.add(DummyConsole()) assert command.parsearg(tctx.master.commands, "foo", str) == "foo" - - assert command.parsearg(tctx.master.commands, "1", int) == 1 + with pytest.raises(exceptions.CommandError, match="Unsupported"): + command.parsearg(tctx.master.commands, "foo", type) with pytest.raises(exceptions.CommandError): command.parsearg(tctx.master.commands, "foo", int) - assert command.parsearg(tctx.master.commands, "true", bool) is True - assert command.parsearg(tctx.master.commands, "false", bool) is False - with pytest.raises(exceptions.CommandError): - command.parsearg(tctx.master.commands, "flobble", bool) - - assert len(command.parsearg( - tctx.master.commands, "2", typing.Sequence[flow.Flow] - )) == 2 - assert command.parsearg(tctx.master.commands, "1", flow.Flow) - with pytest.raises(exceptions.CommandError): - command.parsearg(tctx.master.commands, "2", flow.Flow) - with pytest.raises(exceptions.CommandError): - command.parsearg(tctx.master.commands, "0", flow.Flow) - with pytest.raises(exceptions.CommandError): - command.parsearg(tctx.master.commands, "foo", Exception) - - assert command.parsearg( - tctx.master.commands, "foo", command.Cuts - ) == [["test"]] - - assert command.parsearg( - tctx.master.commands, "foo", typing.Sequence[str] - ) == ["foo"] - assert command.parsearg( - tctx.master.commands, "foo, bar", typing.Sequence[str] - ) == ["foo", "bar"] - class TDec: @command.command("cmd1") @@ -163,3 +306,10 @@ def test_decorator(): with taddons.context() as tctx: tctx.master.addons.add(a) assert tctx.master.commands.call("cmd1 bar") == "ret bar" + + +def test_verify_arg_signature(): + with pytest.raises(exceptions.CommandError): + command.verify_arg_signature(lambda: None, [1, 2], {}) + print('hello there') + command.verify_arg_signature(lambda a, b: None, [1, 2], {}) diff --git a/test/mitmproxy/test_connections.py b/test/mitmproxy/test_connections.py index 99367bb6..9e5d89f1 100644 --- a/test/mitmproxy/test_connections.py +++ b/test/mitmproxy/test_connections.py @@ -29,7 +29,7 @@ class TestClientConnection: def test_repr(self): c = tflow.tclient_conn() - assert 'address:22' in repr(c) + assert '127.0.0.1:22' in repr(c) assert 'ALPN' in repr(c) assert 'TLS' not in repr(c) @@ -41,10 +41,10 @@ class TestClientConnection: def test_tls_established_property(self): c = tflow.tclient_conn() c.tls_established = True - assert c.ssl_established + assert c.tls_established assert c.tls_established c.tls_established = False - assert not c.ssl_established + assert not c.tls_established assert not c.tls_established def test_make_dummy(self): @@ -113,10 +113,10 @@ class TestServerConnection: def test_tls_established_property(self): c = tflow.tserver_conn() c.tls_established = True - assert c.ssl_established + assert c.tls_established assert c.tls_established c.tls_established = False - assert not c.ssl_established + assert not c.tls_established assert not c.tls_established def test_make_dummy(self): @@ -155,7 +155,7 @@ class TestServerConnection: def test_sni(self): c = connections.ServerConnection(('', 1234)) with pytest.raises(ValueError, matches='sni must be str, not '): - c.establish_ssl(None, b'foobar') + c.establish_tls(None, b'foobar') def test_state(self): c = tflow.tserver_conn() @@ -206,7 +206,7 @@ class TestClientConnectionTLS: key = OpenSSL.crypto.load_privatekey( OpenSSL.crypto.FILETYPE_PEM, raw_key) - c.convert_to_ssl(cert, key) + c.convert_to_tls(cert, key) assert c.connected() assert c.sni == sni assert c.tls_established @@ -230,7 +230,7 @@ class TestServerConnectionTLS(tservers.ServerTestBase): def test_tls(self, clientcert): c = connections.ServerConnection(("127.0.0.1", self.port)) c.connect() - c.establish_ssl(clientcert, "foo.com") + c.establish_tls(clientcert, "foo.com") assert c.connected() assert c.sni == "foo.com" assert c.tls_established diff --git a/test/mitmproxy/test_controller.py b/test/mitmproxy/test_controller.py index 2e13d298..e840380a 100644 --- a/test/mitmproxy/test_controller.py +++ b/test/mitmproxy/test_controller.py @@ -30,7 +30,8 @@ class TestMaster: assert ctx.master.should_exit.is_set() def test_server_simple(self): - m = master.Master(None, proxy.DummyServer(None)) + m = master.Master(None) + m.server = proxy.DummyServer() m.start() m.shutdown() m.start() diff --git a/test/mitmproxy/test_flow.py b/test/mitmproxy/test_flow.py index 9f6ed585..8cc11a16 100644 --- a/test/mitmproxy/test_flow.py +++ b/test/mitmproxy/test_flow.py @@ -1,16 +1,16 @@ import io +from unittest import mock import pytest -from mitmproxy.test import tflow +from mitmproxy.test import tflow, tutils import mitmproxy.io from mitmproxy import flowfilter from mitmproxy import options -from mitmproxy.proxy import config from mitmproxy.io import tnetstring from mitmproxy.exceptions import FlowReadException, ReplayException, ControlException from mitmproxy import flow from mitmproxy import http -from mitmproxy.proxy.server import DummyServer +from mitmproxy.net import http as net_http from mitmproxy import master from . import tservers @@ -84,22 +84,47 @@ class TestSerialize: with pytest.raises(Exception, match="version"): list(r.stream()) + def test_copy(self): + """ + _backup may be shared across instances. That should not raise errors. + """ + f = tflow.tflow() + f.backup() + f.request.path = "/foo" + f2 = f.copy() + f2.revert() + f.revert() + class TestFlowMaster: - def test_load_flow_reverse(self): + def test_load_http_flow_reverse(self): s = tservers.TestState() opts = options.Options( mode="reverse:https://use-this-domain" ) - conf = config.ProxyConfig(opts) - fm = master.Master(opts, DummyServer(conf)) + fm = master.Master(opts) fm.addons.add(s) f = tflow.tflow(resp=True) fm.load_flow(f) assert s.flows[0].request.host == "use-this-domain" + def test_load_websocket_flow(self): + s = tservers.TestState() + opts = options.Options( + mode="reverse:https://use-this-domain" + ) + fm = master.Master(opts) + fm.addons.add(s) + f = tflow.twebsocketflow() + fm.load_flow(f.handshake_flow) + fm.load_flow(f) + assert s.flows[0].request.host == "use-this-domain" + assert s.flows[1].handshake_flow == f.handshake_flow + assert len(s.flows[1].messages) == len(f.messages) + def test_replay(self): - fm = master.Master(None, DummyServer()) + opts = options.Options() + fm = master.Master(opts) f = tflow.tflow(resp=True) f.request.content = None with pytest.raises(ReplayException, match="missing"): @@ -117,9 +142,17 @@ class TestFlowMaster: with pytest.raises(ReplayException, match="live"): fm.replay_request(f) + req = tutils.treq(headers=net_http.Headers(((b":authority", b"foo"), (b"header", b"qvalue"), (b"content-length", b"7")))) + f = tflow.tflow(req=req) + f.request.http_version = "HTTP/2.0" + with mock.patch('mitmproxy.proxy.protocol.http_replay.RequestReplayThread.run'): + rt = fm.replay_request(f) + assert rt.f.request.http_version == "HTTP/1.1" + assert ":authority" not in rt.f.request.headers + def test_all(self): s = tservers.TestState() - fm = master.Master(None, DummyServer()) + fm = master.Master(None) fm.addons.add(s) f = tflow.tflow(req=None) fm.addons.handle_lifecycle("clientconnect", f.client_conn) diff --git a/test/mitmproxy/test_flowfilter.py b/test/mitmproxy/test_flowfilter.py index fe9b2408..4eb37d81 100644 --- a/test/mitmproxy/test_flowfilter.py +++ b/test/mitmproxy/test_flowfilter.py @@ -225,11 +225,11 @@ class TestMatchingHTTPFlow: def test_src(self): q = self.req() - assert self.q("~src address", q) + assert self.q("~src 127.0.0.1", q) assert not self.q("~src foobar", q) assert self.q("~src :22", q) assert not self.q("~src :99", q) - assert self.q("~src address:22", q) + assert self.q("~src 127.0.0.1:22", q) q.client_conn.address = None assert not self.q('~src address:22', q) @@ -315,11 +315,11 @@ class TestMatchingTCPFlow: def test_src(self): f = self.flow() - assert self.q("~src address", f) + assert self.q("~src 127.0.0.1", f) assert not self.q("~src foobar", f) assert self.q("~src :22", f) assert not self.q("~src :99", f) - assert self.q("~src address:22", f) + assert self.q("~src 127.0.0.1:22", f) def test_dst(self): f = self.flow() @@ -420,6 +420,20 @@ class TestMatchingWebSocketFlow: e = self.err() assert self.q("~e", e) + def test_domain(self): + q = self.flow() + assert self.q("~d example.com", q) + assert not self.q("~d none", q) + + def test_url(self): + q = self.flow() + assert self.q("~u example.com", q) + assert self.q("~u example.com/ws", q) + assert not self.q("~u moo/path", q) + + q.handshake_flow = None + assert not self.q("~u example.com", q) + def test_body(self): f = self.flow() @@ -440,11 +454,11 @@ class TestMatchingWebSocketFlow: def test_src(self): f = self.flow() - assert self.q("~src address", f) + assert self.q("~src 127.0.0.1", f) assert not self.q("~src foobar", f) assert self.q("~src :22", f) assert not self.q("~src :99", f) - assert self.q("~src address:22", f) + assert self.q("~src 127.0.0.1:22", f) def test_dst(self): f = self.flow() @@ -524,7 +538,7 @@ class TestMatchingDummyFlow: assert not self.q("~s", f) - assert self.q("~src address", f) + assert self.q("~src 127.0.0.1", f) assert not self.q("~src nonexistent", f) assert not self.q("~tcp", f) diff --git a/test/mitmproxy/test_http.py b/test/mitmproxy/test_http.py index 4463961a..49e61e25 100644 --- a/test/mitmproxy/test_http.py +++ b/test/mitmproxy/test_http.py @@ -203,6 +203,15 @@ class TestHTTPFlow: f.resume() assert f.reply.state == "committed" + def test_resume_duplicated(self): + f = tflow.tflow() + f.intercept() + f2 = f.copy() + assert f.intercepted is f2.intercepted is True + f.resume() + f2.resume() + assert f.intercepted is f2.intercepted is False + def test_replace_unicode(self): f = tflow.tflow(resp=True) f.response.content = b"\xc2foo" diff --git a/test/mitmproxy/test_log.py b/test/mitmproxy/test_log.py index cde679ed..349e3ac8 100644 --- a/test/mitmproxy/test_log.py +++ b/test/mitmproxy/test_log.py @@ -4,3 +4,8 @@ from mitmproxy import log def test_logentry(): e = log.LogEntry("foo", "info") assert repr(e) == "LogEntry(foo, info)" + + f = log.LogEntry("foo", "warning") + assert e == e + assert e != f + assert e != 42 diff --git a/test/mitmproxy/test_optmanager.py b/test/mitmproxy/test_optmanager.py index 04ec7ded..d9b93227 100644 --- a/test/mitmproxy/test_optmanager.py +++ b/test/mitmproxy/test_optmanager.py @@ -73,6 +73,11 @@ def test_required_int(): o.parse_setval("required_int", None) +def test_deepcopy(): + o = TD() + copy.deepcopy(o) + + def test_options(): o = TO() assert o.keys() == {"bool", "one", "two", "required_int"} @@ -229,6 +234,10 @@ def test_simple(): assert "one" in TO() +def test_items(): + assert TO().items() + + def test_serialize(): o = TD2() o.three = "set" @@ -240,6 +249,7 @@ def test_serialize(): o2 = TD2() optmanager.load(o2, data) assert o2 == o + assert not o == 42 t = """ unknown: foo @@ -257,6 +267,10 @@ def test_serialize(): with pytest.raises(Exception, match="Config error"): optmanager.load(o2, t) + t = "# a comment" + optmanager.load(o2, t) + assert optmanager.load(o2, "foobar: '123'") == {"foobar": "123"} + t = "" optmanager.load(o2, t) assert optmanager.load(o2, "foobar: '123'") == {"foobar": "123"} @@ -334,6 +348,12 @@ def test_dump_defaults(): assert optmanager.dump_defaults(o) +def test_dump_dicts(): + o = options.Options() + assert optmanager.dump_dicts(o) + assert optmanager.dump_dicts(o, ['http2', 'anticomp']) + + class TTypes(optmanager.OptManager): def __init__(self): super().__init__() diff --git a/test/mitmproxy/test_typemanager.py b/test/mitmproxy/test_typemanager.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/mitmproxy/test_typemanager.py diff --git a/test/mitmproxy/test_types.py b/test/mitmproxy/test_types.py new file mode 100644 index 00000000..72492fa9 --- /dev/null +++ b/test/mitmproxy/test_types.py @@ -0,0 +1,237 @@ +import pytest +import os +import typing +import contextlib + +from mitmproxy.test import tutils +import mitmproxy.exceptions +import mitmproxy.types +from mitmproxy.test import taddons +from mitmproxy.test import tflow +from mitmproxy import command +from mitmproxy import flow + +from . import test_command + + +@contextlib.contextmanager +def chdir(path: str): + old_dir = os.getcwd() + os.chdir(path) + yield + os.chdir(old_dir) + + +def test_bool(): + with taddons.context() as tctx: + b = mitmproxy.types._BoolType() + assert b.completion(tctx.master.commands, bool, "b") == ["false", "true"] + assert b.parse(tctx.master.commands, bool, "true") is True + assert b.parse(tctx.master.commands, bool, "false") is False + assert b.is_valid(tctx.master.commands, bool, True) is True + assert b.is_valid(tctx.master.commands, bool, "foo") is False + with pytest.raises(mitmproxy.exceptions.TypeError): + b.parse(tctx.master.commands, bool, "foo") + + +def test_str(): + with taddons.context() as tctx: + b = mitmproxy.types._StrType() + assert b.is_valid(tctx.master.commands, str, "foo") is True + assert b.is_valid(tctx.master.commands, str, 1) is False + assert b.completion(tctx.master.commands, str, "") == [] + assert b.parse(tctx.master.commands, str, "foo") == "foo" + + +def test_unknown(): + with taddons.context() as tctx: + b = mitmproxy.types._UnknownType() + assert b.is_valid(tctx.master.commands, mitmproxy.types.Unknown, "foo") is False + assert b.is_valid(tctx.master.commands, mitmproxy.types.Unknown, 1) is False + assert b.completion(tctx.master.commands, mitmproxy.types.Unknown, "") == [] + assert b.parse(tctx.master.commands, mitmproxy.types.Unknown, "foo") == "foo" + + +def test_int(): + with taddons.context() as tctx: + b = mitmproxy.types._IntType() + assert b.is_valid(tctx.master.commands, int, "foo") is False + assert b.is_valid(tctx.master.commands, int, 1) is True + assert b.completion(tctx.master.commands, int, "b") == [] + assert b.parse(tctx.master.commands, int, "1") == 1 + assert b.parse(tctx.master.commands, int, "999") == 999 + with pytest.raises(mitmproxy.exceptions.TypeError): + b.parse(tctx.master.commands, int, "foo") + + +def test_path(): + with taddons.context() as tctx: + b = mitmproxy.types._PathType() + assert b.parse(tctx.master.commands, mitmproxy.types.Path, "/foo") == "/foo" + assert b.parse(tctx.master.commands, mitmproxy.types.Path, "/bar") == "/bar" + assert b.is_valid(tctx.master.commands, mitmproxy.types.Path, "foo") is True + assert b.is_valid(tctx.master.commands, mitmproxy.types.Path, 3) is False + + def normPathOpts(prefix, match): + ret = [] + for s in b.completion(tctx.master.commands, mitmproxy.types.Path, match): + s = s[len(prefix):] + s = s.replace(os.sep, "/") + ret.append(s) + return ret + + cd = os.path.normpath(tutils.test_data.path("mitmproxy/completion")) + assert normPathOpts(cd, cd) == ['/aaa', '/aab', '/aac', '/bbb/'] + assert normPathOpts(cd, os.path.join(cd, "a")) == ['/aaa', '/aab', '/aac'] + with chdir(cd): + assert normPathOpts("", "./") == ['./aaa', './aab', './aac', './bbb/'] + assert normPathOpts("", "") == ['./aaa', './aab', './aac', './bbb/'] + assert b.completion( + tctx.master.commands, mitmproxy.types.Path, "nonexistent" + ) == ["nonexistent"] + + +def test_cmd(): + with taddons.context() as tctx: + tctx.master.addons.add(test_command.TAddon()) + b = mitmproxy.types._CmdType() + assert b.is_valid(tctx.master.commands, mitmproxy.types.Cmd, "foo") is False + assert b.is_valid(tctx.master.commands, mitmproxy.types.Cmd, "cmd1") is True + assert b.parse(tctx.master.commands, mitmproxy.types.Cmd, "cmd1") == "cmd1" + with pytest.raises(mitmproxy.exceptions.TypeError): + assert b.parse(tctx.master.commands, mitmproxy.types.Cmd, "foo") + assert len( + b.completion(tctx.master.commands, mitmproxy.types.Cmd, "") + ) == len(tctx.master.commands.commands.keys()) + + +def test_cutspec(): + with taddons.context() as tctx: + b = mitmproxy.types._CutSpecType() + b.parse(tctx.master.commands, mitmproxy.types.CutSpec, "foo,bar") == ["foo", "bar"] + assert b.is_valid(tctx.master.commands, mitmproxy.types.CutSpec, 1) is False + assert b.is_valid(tctx.master.commands, mitmproxy.types.CutSpec, "foo") is False + assert b.is_valid(tctx.master.commands, mitmproxy.types.CutSpec, "request.path") is True + + assert b.completion( + tctx.master.commands, mitmproxy.types.CutSpec, "request.p" + ) == b.valid_prefixes + ret = b.completion(tctx.master.commands, mitmproxy.types.CutSpec, "request.port,f") + assert ret[0].startswith("request.port,") + assert len(ret) == len(b.valid_prefixes) + + +def test_arg(): + with taddons.context() as tctx: + b = mitmproxy.types._ArgType() + assert b.completion(tctx.master.commands, mitmproxy.types.Arg, "") == [] + assert b.parse(tctx.master.commands, mitmproxy.types.Arg, "foo") == "foo" + assert b.is_valid(tctx.master.commands, mitmproxy.types.Arg, "foo") is True + assert b.is_valid(tctx.master.commands, mitmproxy.types.Arg, 1) is False + + +def test_strseq(): + with taddons.context() as tctx: + b = mitmproxy.types._StrSeqType() + assert b.completion(tctx.master.commands, typing.Sequence[str], "") == [] + assert b.parse(tctx.master.commands, typing.Sequence[str], "foo") == ["foo"] + assert b.parse(tctx.master.commands, typing.Sequence[str], "foo,bar") == ["foo", "bar"] + assert b.is_valid(tctx.master.commands, typing.Sequence[str], ["foo"]) is True + assert b.is_valid(tctx.master.commands, typing.Sequence[str], ["a", "b", 3]) is False + assert b.is_valid(tctx.master.commands, typing.Sequence[str], 1) is False + assert b.is_valid(tctx.master.commands, typing.Sequence[str], "foo") is False + + +class DummyConsole: + @command.command("view.resolve") + def resolve(self, spec: str) -> typing.Sequence[flow.Flow]: + if spec == "err": + raise mitmproxy.exceptions.CommandError() + n = int(spec) + return [tflow.tflow(resp=True)] * n + + @command.command("cut") + def cut(self, spec: str) -> mitmproxy.types.Data: + return [["test"]] + + @command.command("options") + def options(self) -> typing.Sequence[str]: + return ["one", "two", "three"] + + +def test_flow(): + with taddons.context() as tctx: + tctx.master.addons.add(DummyConsole()) + b = mitmproxy.types._FlowType() + assert len(b.completion(tctx.master.commands, flow.Flow, "")) == len(b.valid_prefixes) + assert b.parse(tctx.master.commands, flow.Flow, "1") + assert b.is_valid(tctx.master.commands, flow.Flow, tflow.tflow()) is True + assert b.is_valid(tctx.master.commands, flow.Flow, "xx") is False + with pytest.raises(mitmproxy.exceptions.TypeError): + b.parse(tctx.master.commands, flow.Flow, "0") + with pytest.raises(mitmproxy.exceptions.TypeError): + b.parse(tctx.master.commands, flow.Flow, "2") + with pytest.raises(mitmproxy.exceptions.TypeError): + b.parse(tctx.master.commands, flow.Flow, "err") + + +def test_flows(): + with taddons.context() as tctx: + tctx.master.addons.add(DummyConsole()) + b = mitmproxy.types._FlowsType() + assert len( + b.completion(tctx.master.commands, typing.Sequence[flow.Flow], "") + ) == len(b.valid_prefixes) + assert b.is_valid(tctx.master.commands, typing.Sequence[flow.Flow], [tflow.tflow()]) is True + assert b.is_valid(tctx.master.commands, typing.Sequence[flow.Flow], "xx") is False + assert b.is_valid(tctx.master.commands, typing.Sequence[flow.Flow], 0) is False + assert len(b.parse(tctx.master.commands, typing.Sequence[flow.Flow], "0")) == 0 + assert len(b.parse(tctx.master.commands, typing.Sequence[flow.Flow], "1")) == 1 + assert len(b.parse(tctx.master.commands, typing.Sequence[flow.Flow], "2")) == 2 + with pytest.raises(mitmproxy.exceptions.TypeError): + b.parse(tctx.master.commands, typing.Sequence[flow.Flow], "err") + + +def test_data(): + with taddons.context() as tctx: + b = mitmproxy.types._DataType() + assert b.is_valid(tctx.master.commands, mitmproxy.types.Data, 0) is False + assert b.is_valid(tctx.master.commands, mitmproxy.types.Data, []) is True + assert b.is_valid(tctx.master.commands, mitmproxy.types.Data, [["x"]]) is True + assert b.is_valid(tctx.master.commands, mitmproxy.types.Data, [[b"x"]]) is True + assert b.is_valid(tctx.master.commands, mitmproxy.types.Data, [[1]]) is False + with pytest.raises(mitmproxy.exceptions.TypeError): + b.parse(tctx.master.commands, mitmproxy.types.Data, "foo") + with pytest.raises(mitmproxy.exceptions.TypeError): + b.parse(tctx.master.commands, mitmproxy.types.Data, "foo") + + +def test_choice(): + with taddons.context() as tctx: + tctx.master.addons.add(DummyConsole()) + b = mitmproxy.types._ChoiceType() + assert b.is_valid( + tctx.master.commands, + mitmproxy.types.Choice("options"), + "one", + ) is True + assert b.is_valid( + tctx.master.commands, + mitmproxy.types.Choice("options"), + "invalid", + ) is False + assert b.is_valid( + tctx.master.commands, + mitmproxy.types.Choice("nonexistent"), + "invalid", + ) is False + comp = b.completion(tctx.master.commands, mitmproxy.types.Choice("options"), "") + assert comp == ["one", "two", "three"] + assert b.parse(tctx.master.commands, mitmproxy.types.Choice("options"), "one") == "one" + with pytest.raises(mitmproxy.exceptions.TypeError): + b.parse(tctx.master.commands, mitmproxy.types.Choice("options"), "invalid") + + +def test_typemanager(): + assert mitmproxy.types.CommandTypes.get(bool, None) + assert mitmproxy.types.CommandTypes.get(mitmproxy.types.Choice("choide"), None) diff --git a/test/mitmproxy/test_version.py b/test/mitmproxy/test_version.py index f87b0851..8c176542 100644 --- a/test/mitmproxy/test_version.py +++ b/test/mitmproxy/test_version.py @@ -1,10 +1,36 @@ +import pathlib import runpy +import subprocess +from unittest import mock from mitmproxy import version def test_version(capsys): - runpy.run_module('mitmproxy.version', run_name='__main__') + here = pathlib.Path(__file__).absolute().parent + version_file = here / ".." / ".." / "mitmproxy" / "version.py" + runpy.run_path(str(version_file), run_name='__main__') stdout, stderr = capsys.readouterr() assert len(stdout) > 0 assert stdout.strip() == version.VERSION + + +def test_get_version_hardcoded(): + version.VERSION = "3.0.0.dev123-0xcafebabe" + assert version.get_version() == "3.0.0" + assert version.get_version(True) == "3.0.0.dev123" + assert version.get_version(True, True) == "3.0.0.dev123-0xcafebabe" + + +def test_get_version(): + version.VERSION = "3.0.0" + + with mock.patch('subprocess.check_output') as m: + m.return_value = b"tag-0-cafecafe" + assert version.get_version(True, True) == "3.0.0" + + m.return_value = b"tag-2-cafecafe" + assert version.get_version(True, True) == "3.0.0.dev2-0xcafecaf" + + m.side_effect = subprocess.CalledProcessError(-1, 'git describe --long') + assert version.get_version(True, True) == "3.0.0" diff --git a/test/mitmproxy/test_websocket.py b/test/mitmproxy/test_websocket.py index 7c53a4b0..fcacec36 100644 --- a/test/mitmproxy/test_websocket.py +++ b/test/mitmproxy/test_websocket.py @@ -3,6 +3,7 @@ import pytest from mitmproxy.io import tnetstring from mitmproxy import flowfilter +from mitmproxy.exceptions import Kill, ControlException from mitmproxy.test import tflow @@ -42,6 +43,20 @@ class TestWebSocketFlow: assert f.error.get_state() == f2.error.get_state() assert f.error is not f2.error + def test_kill(self): + f = tflow.twebsocketflow() + with pytest.raises(ControlException): + f.intercept() + f.resume() + f.kill() + + f = tflow.twebsocketflow() + f.intercept() + assert f.killable + f.kill() + assert not f.killable + assert f.reply.value == Kill + def test_match(self): f = tflow.twebsocketflow() assert not flowfilter.match("~b nonexistent", f) @@ -71,3 +86,9 @@ class TestWebSocketFlow: d = tflow.twebsocketflow().handshake_flow.get_state() tnetstring.dump(d, b) assert b.getvalue() + + def test_message_kill(self): + f = tflow.twebsocketflow() + assert not f.messages[-1].killed + f.messages[-1].kill() + assert f.messages[-1].killed diff --git a/test/mitmproxy/tools/console/test_commander.py b/test/mitmproxy/tools/console/test_commander.py new file mode 100644 index 00000000..2a96995d --- /dev/null +++ b/test/mitmproxy/tools/console/test_commander.py @@ -0,0 +1,98 @@ + +from mitmproxy.tools.console.commander import commander +from mitmproxy.test import taddons + + +class TestListCompleter: + def test_cycle(self): + tests = [ + [ + "", + ["a", "b", "c"], + ["a", "b", "c", "a"] + ], + [ + "xxx", + ["a", "b", "c"], + ["xxx", "xxx", "xxx"] + ], + [ + "b", + ["a", "b", "ba", "bb", "c"], + ["b", "ba", "bb", "b"] + ], + ] + for start, options, cycle in tests: + c = commander.ListCompleter(start, options) + for expected in cycle: + assert c.cycle() == expected + + +class TestCommandBuffer: + + def test_backspace(self): + tests = [ + [("", 0), ("", 0)], + [("1", 0), ("1", 0)], + [("1", 1), ("", 0)], + [("123", 3), ("12", 2)], + [("123", 2), ("13", 1)], + [("123", 0), ("123", 0)], + ] + with taddons.context() as tctx: + for start, output in tests: + cb = commander.CommandBuffer(tctx.master) + cb.text, cb.cursor = start[0], start[1] + cb.backspace() + assert cb.text == output[0] + assert cb.cursor == output[1] + + def test_left(self): + cursors = [3, 2, 1, 0, 0] + with taddons.context() as tctx: + cb = commander.CommandBuffer(tctx.master) + cb.text, cb.cursor = "abcd", 4 + for c in cursors: + cb.left() + assert cb.cursor == c + + def test_right(self): + cursors = [1, 2, 3, 4, 4] + with taddons.context() as tctx: + cb = commander.CommandBuffer(tctx.master) + cb.text, cb.cursor = "abcd", 0 + for c in cursors: + cb.right() + assert cb.cursor == c + + def test_insert(self): + tests = [ + [("", 0), ("x", 1)], + [("a", 0), ("xa", 1)], + [("xa", 2), ("xax", 3)], + ] + with taddons.context() as tctx: + for start, output in tests: + cb = commander.CommandBuffer(tctx.master) + cb.text, cb.cursor = start[0], start[1] + cb.insert("x") + assert cb.text == output[0] + assert cb.cursor == output[1] + + def test_cycle_completion(self): + with taddons.context() as tctx: + cb = commander.CommandBuffer(tctx.master) + cb.text = "foo bar" + cb.cursor = len(cb.text) + cb.cycle_completion() + + def test_render(self): + with taddons.context() as tctx: + cb = commander.CommandBuffer(tctx.master) + cb.text = "foo" + assert cb.render() + + def test_flatten(self): + with taddons.context() as tctx: + cb = commander.CommandBuffer(tctx.master) + assert cb.flatten("foo bar") == "foo bar" diff --git a/test/mitmproxy/tools/console/test_common.py b/test/mitmproxy/tools/console/test_common.py index 3ab4fd67..72438c49 100644 --- a/test/mitmproxy/tools/console/test_common.py +++ b/test/mitmproxy/tools/console/test_common.py @@ -1,12 +1,34 @@ +import urwid + from mitmproxy.test import tflow from mitmproxy.tools.console import common -from ....conftest import skip_appveyor - -@skip_appveyor def test_format_flow(): f = tflow.tflow(resp=True) assert common.format_flow(f, True) assert common.format_flow(f, True, hostheader=True) assert common.format_flow(f, True, extended=True) + + +def test_format_keyvals(): + assert common.format_keyvals( + [ + ("aa", "bb"), + ("cc", "dd"), + ("ee", None), + ] + ) + wrapped = urwid.BoxAdapter( + urwid.ListBox( + urwid.SimpleFocusListWalker( + common.format_keyvals([("foo", "bar")]) + ) + ), 1 + ) + assert wrapped.render((30, )) + assert common.format_keyvals( + [ + ("aa", wrapped) + ] + ) diff --git a/test/mitmproxy/tools/console/test_defaultkeys.py b/test/mitmproxy/tools/console/test_defaultkeys.py new file mode 100644 index 00000000..1f17c888 --- /dev/null +++ b/test/mitmproxy/tools/console/test_defaultkeys.py @@ -0,0 +1,23 @@ +from mitmproxy.test.tflow import tflow +from mitmproxy.tools.console import defaultkeys +from mitmproxy.tools.console import keymap +from mitmproxy.tools.console import master +from mitmproxy import command + + +def test_commands_exist(): + km = keymap.Keymap(None) + defaultkeys.map(km) + assert km.bindings + m = master.ConsoleMaster(None) + m.load_flow(tflow()) + + for binding in km.bindings: + cmd, *args = command.lexer(binding.command) + assert cmd in m.commands.commands + + cmd_obj = m.commands.commands[cmd] + try: + cmd_obj.prepare_args(args) + except Exception as e: + raise ValueError("Invalid command: {}".format(binding.command)) from e diff --git a/test/mitmproxy/tools/console/test_help.py b/test/mitmproxy/tools/console/test_help.py deleted file mode 100644 index 0ebc2d6a..00000000 --- a/test/mitmproxy/tools/console/test_help.py +++ /dev/null @@ -1,11 +0,0 @@ -import mitmproxy.tools.console.help as help - -from ....conftest import skip_appveyor - - -@skip_appveyor -class TestHelp: - - def test_helptext(self): - h = help.HelpView(None) - assert h.helptext() diff --git a/test/mitmproxy/tools/console/test_keymap.py b/test/mitmproxy/tools/console/test_keymap.py index 6a75800e..00e64991 100644 --- a/test/mitmproxy/tools/console/test_keymap.py +++ b/test/mitmproxy/tools/console/test_keymap.py @@ -4,26 +4,69 @@ from unittest import mock import pytest +def test_binding(): + b = keymap.Binding("space", "cmd", ["options"], "") + assert b.keyspec() == " " + + def test_bind(): - with taddons.context() as tctx: - km = keymap.Keymap(tctx.master) - km.executor = mock.Mock() + with taddons.context() as tctx: + km = keymap.Keymap(tctx.master) + km.executor = mock.Mock() + + with pytest.raises(ValueError): + km.add("foo", "bar", ["unsupported"]) + + km.add("key", "str", ["options", "commands"]) + assert km.get("options", "key") + assert km.get("commands", "key") + assert not km.get("flowlist", "key") + assert len((km.list("commands"))) == 1 + + km.handle("unknown", "unknown") + assert not km.executor.called + + km.handle("options", "key") + assert km.executor.called + + km.add("glob", "str", ["global"]) + km.executor = mock.Mock() + km.handle("options", "glob") + assert km.executor.called + + assert len((km.list("global"))) == 1 + + +def test_join(): + with taddons.context() as tctx: + km = keymap.Keymap(tctx.master) + km.add("key", "str", ["options"], "help1") + km.add("key", "str", ["commands"]) + return + assert len(km.bindings) == 1 + assert len(km.bindings[0].contexts) == 2 + assert km.bindings[0].help == "help1" + km.add("key", "str", ["commands"], "help2") + assert len(km.bindings) == 1 + assert len(km.bindings[0].contexts) == 2 + assert km.bindings[0].help == "help2" - with pytest.raises(ValueError): - km.add("foo", "bar", ["unsupported"]) + assert km.get("commands", "key") + km.unbind(km.bindings[0]) + assert len(km.bindings) == 0 + assert not km.get("commands", "key") - km.add("key", "str", ["options", "commands"]) - assert km.get("options", "key") - assert km.get("commands", "key") - assert not km.get("flowlist", "key") - km.handle("unknown", "unknown") - assert not km.executor.called +def test_remove(): + with taddons.context() as tctx: + km = keymap.Keymap(tctx.master) + km.add("key", "str", ["options", "commands"], "help1") + assert len(km.bindings) == 1 + assert "options" in km.bindings[0].contexts - km.handle("options", "key") - assert km.executor.called + km.remove("key", ["options"]) + assert len(km.bindings) == 1 + assert "options" not in km.bindings[0].contexts - km.add("glob", "str", ["global"]) - km.executor = mock.Mock() - km.handle("options", "glob") - assert km.executor.called + km.remove("key", ["commands"]) + assert len(km.bindings) == 0 diff --git a/test/mitmproxy/tools/console/test_master.py b/test/mitmproxy/tools/console/test_master.py index c87c9e83..9779a482 100644 --- a/test/mitmproxy/tools/console/test_master.py +++ b/test/mitmproxy/tools/console/test_master.py @@ -1,23 +1,10 @@ +import urwid + +from mitmproxy import options from mitmproxy.test import tflow from mitmproxy.test import tutils from mitmproxy.tools import console -from mitmproxy import proxy -from mitmproxy import options -from mitmproxy.tools.console import common from ... import tservers -import urwid - - -def test_format_keyvals(): - assert common.format_keyvals( - [ - ("aa", "bb"), - None, - ("cc", "dd"), - (None, "dd"), - (None, "dd"), - ] - ) def test_options(): @@ -27,9 +14,9 @@ def test_options(): class TestMaster(tservers.MasterTest): def mkmaster(self, **opts): if "verbosity" not in opts: - opts["verbosity"] = 1 + opts["verbosity"] = 'warn' o = options.Options(**opts) - m = console.master.ConsoleMaster(o, proxy.DummyServer()) + m = console.master.ConsoleMaster(o) m.addons.trigger("configure", o.keys()) return m diff --git a/test/mitmproxy/tools/console/test_pathedit.py b/test/mitmproxy/tools/console/test_pathedit.py deleted file mode 100644 index b9f51f5a..00000000 --- a/test/mitmproxy/tools/console/test_pathedit.py +++ /dev/null @@ -1,72 +0,0 @@ -import os -from os.path import normpath -from unittest import mock - -from mitmproxy.tools.console import pathedit -from mitmproxy.test import tutils - - -class TestPathCompleter: - - def test_lookup_construction(self): - c = pathedit._PathCompleter() - - cd = os.path.normpath(tutils.test_data.path("mitmproxy/completion")) - ca = os.path.join(cd, "a") - assert c.complete(ca).endswith(normpath("/completion/aaa")) - assert c.complete(ca).endswith(normpath("/completion/aab")) - c.reset() - ca = os.path.join(cd, "aaa") - assert c.complete(ca).endswith(normpath("/completion/aaa")) - assert c.complete(ca).endswith(normpath("/completion/aaa")) - c.reset() - assert c.complete(cd).endswith(normpath("/completion/aaa")) - - def test_completion(self): - c = pathedit._PathCompleter(True) - c.reset() - c.lookup = [ - ("a", "x/a"), - ("aa", "x/aa"), - ] - assert c.complete("a") == "a" - assert c.final == "x/a" - assert c.complete("a") == "aa" - assert c.complete("a") == "a" - - c = pathedit._PathCompleter(True) - r = c.complete("l") - assert c.final.endswith(r) - - c.reset() - assert c.complete("/nonexistent") == "/nonexistent" - assert c.final == "/nonexistent" - c.reset() - assert c.complete("~") != "~" - - c.reset() - s = "thisisatotallynonexistantpathforsure" - assert c.complete(s) == s - assert c.final == s - - -class TestPathEdit: - - def test_keypress(self): - - pe = pathedit.PathEdit("", "") - - with mock.patch('urwid.widget.Edit.get_edit_text') as get_text, \ - mock.patch('urwid.widget.Edit.set_edit_text') as set_text: - - cd = os.path.normpath(tutils.test_data.path("mitmproxy/completion")) - get_text.return_value = os.path.join(cd, "a") - - # Pressing tab should set completed path - pe.keypress((1,), "tab") - set_text_called_with = set_text.call_args[0][0] - assert set_text_called_with.endswith(normpath("/completion/aaa")) - - # Pressing any other key should reset - pe.keypress((1,), "a") - assert pe.lookup is None diff --git a/test/mitmproxy/tools/console/test_statusbar.py b/test/mitmproxy/tools/console/test_statusbar.py new file mode 100644 index 00000000..b131a5d3 --- /dev/null +++ b/test/mitmproxy/tools/console/test_statusbar.py @@ -0,0 +1,33 @@ +from mitmproxy import options +from mitmproxy.tools.console import statusbar, master + + +def test_statusbar(monkeypatch): + o = options.Options( + setheaders=[":~q:foo:bar"], + replacements=[":~q:foo:bar"], + ignore_hosts=["example.com", "example.org"], + tcp_hosts=["example.tcp"], + intercept="~q", + view_filter="~dst example.com", + stickycookie="~dst example.com", + stickyauth="~dst example.com", + default_contentview="javascript", + anticache=True, + anticomp=True, + showhost=True, + refresh_server_playback=False, + replay_kill_extra=True, + upstream_cert=False, + stream_large_bodies="3m", + mode="transparent", + scripts=["nonexistent"], + save_stream_file="foo", + ) + m = master.ConsoleMaster(o) + m.options.update(view_order='url', console_focus_follow=True) + monkeypatch.setattr(m.addons.get("clientplayback"), "count", lambda: 42) + monkeypatch.setattr(m.addons.get("serverplayback"), "count", lambda: 42) + + bar = statusbar.StatusBar(m) # this already causes a redraw + assert bar.ib._w diff --git a/test/mitmproxy/tools/test_cmdline.py b/test/mitmproxy/tools/test_cmdline.py index 65cfeb07..e247dc1d 100644 --- a/test/mitmproxy/tools/test_cmdline.py +++ b/test/mitmproxy/tools/test_cmdline.py @@ -1,7 +1,8 @@ import argparse -from mitmproxy.tools import cmdline -from mitmproxy.tools import main + from mitmproxy import options +from mitmproxy.tools import cmdline, web, dump, console +from mitmproxy.tools import main def test_common(): @@ -14,17 +15,20 @@ def test_common(): def test_mitmproxy(): opts = options.Options() + console.master.ConsoleMaster(opts) ap = cmdline.mitmproxy(opts) assert ap def test_mitmdump(): opts = options.Options() + dump.DumpMaster(opts) ap = cmdline.mitmdump(opts) assert ap def test_mitmweb(): opts = options.Options() + web.master.WebMaster(opts) ap = cmdline.mitmweb(opts) assert ap diff --git a/test/mitmproxy/tools/test_dump.py b/test/mitmproxy/tools/test_dump.py index 69a76d2e..952c3f4f 100644 --- a/test/mitmproxy/tools/test_dump.py +++ b/test/mitmproxy/tools/test_dump.py @@ -1,7 +1,6 @@ import pytest from unittest import mock -from mitmproxy import proxy from mitmproxy import log from mitmproxy import controller from mitmproxy import options @@ -12,8 +11,8 @@ from .. import tservers class TestDumpMaster(tservers.MasterTest): def mkmaster(self, flt, **opts): - o = options.Options(view_filter=flt, verbosity=-1, flow_detail=0, **opts) - m = dump.DumpMaster(o, proxy.DummyServer(), with_termlog=False, with_dumper=False) + o = options.Options(view_filter=flt, verbosity='error', flow_detail=0, **opts) + m = dump.DumpMaster(o, with_termlog=False, with_dumper=False) return m def test_has_error(self): @@ -27,12 +26,12 @@ class TestDumpMaster(tservers.MasterTest): def test_addons_termlog(self, termlog): with mock.patch('sys.stdout'): o = options.Options() - m = dump.DumpMaster(o, proxy.DummyServer(), with_termlog=termlog) + m = dump.DumpMaster(o, with_termlog=termlog) assert (m.addons.get('termlog') is not None) == termlog @pytest.mark.parametrize("dumper", [False, True]) def test_addons_dumper(self, dumper): with mock.patch('sys.stdout'): o = options.Options() - m = dump.DumpMaster(o, proxy.DummyServer(), with_dumper=dumper) + m = dump.DumpMaster(o, with_dumper=dumper) assert (m.addons.get('dumper') is not None) == dumper diff --git a/test/mitmproxy/tools/test_main.py b/test/mitmproxy/tools/test_main.py new file mode 100644 index 00000000..88e2fe86 --- /dev/null +++ b/test/mitmproxy/tools/test_main.py @@ -0,0 +1,19 @@ +from mitmproxy.test import tutils +from mitmproxy.tools import main + +shutdown_script = tutils.test_data.path("mitmproxy/data/addonscripts/shutdown.py") + + +def test_mitmweb(): + main.mitmweb([ + "--no-web-open-browser", + "-q", + "-s", shutdown_script + ]) + + +def test_mitmdump(): + main.mitmdump([ + "-q", + "-s", shutdown_script + ]) diff --git a/test/mitmproxy/tools/web/test_app.py b/test/mitmproxy/tools/web/test_app.py index 5427b995..5afc0bca 100644 --- a/test/mitmproxy/tools/web/test_app.py +++ b/test/mitmproxy/tools/web/test_app.py @@ -1,27 +1,40 @@ import json as _json +import logging from unittest import mock import os +import pytest import tornado.testing from tornado import httpclient from tornado import websocket from mitmproxy import exceptions -from mitmproxy import proxy from mitmproxy import options from mitmproxy.test import tflow from mitmproxy.tools.web import app from mitmproxy.tools.web import master as webmaster +@pytest.fixture(scope="module") +def no_tornado_logging(): + logging.getLogger('tornado.access').disabled = True + logging.getLogger('tornado.application').disabled = True + logging.getLogger('tornado.general').disabled = True + yield + logging.getLogger('tornado.access').disabled = False + logging.getLogger('tornado.application').disabled = False + logging.getLogger('tornado.general').disabled = False + + def json(resp: httpclient.HTTPResponse): return _json.loads(resp.body.decode()) +@pytest.mark.usefixtures("no_tornado_logging") class TestApp(tornado.testing.AsyncHTTPTestCase): def get_app(self): o = options.Options(http2=False) - m = webmaster.WebMaster(o, proxy.DummyServer(), with_termlog=False) + m = webmaster.WebMaster(o, with_termlog=False) f = tflow.tflow(resp=True) f.id = "42" m.view.add([f]) @@ -187,7 +200,7 @@ class TestApp(tornado.testing.AsyncHTTPTestCase): f.response.headers["Content-Encoding"] = "ran\x00dom" f.response.headers["Content-Disposition"] = 'inline; filename="filename.jpg"' - r = self.fetch("/flows/42/response/content") + r = self.fetch("/flows/42/response/content.data") assert r.body == b"message" assert r.headers["Content-Encoding"] == "random" assert r.headers["Content-Disposition"] == 'attachment; filename="filename.jpg"' @@ -195,17 +208,17 @@ class TestApp(tornado.testing.AsyncHTTPTestCase): del f.response.headers["Content-Disposition"] f.request.path = "/foo/bar.jpg" assert self.fetch( - "/flows/42/response/content" + "/flows/42/response/content.data" ).headers["Content-Disposition"] == 'attachment; filename=bar.jpg' f.response.content = b"" - assert self.fetch("/flows/42/response/content").code == 400 + assert self.fetch("/flows/42/response/content.data").code == 400 f.revert() def test_update_flow_content(self): assert self.fetch( - "/flows/42/request/content", + "/flows/42/request/content.data", method="POST", body="new" ).code == 200 @@ -223,7 +236,7 @@ class TestApp(tornado.testing.AsyncHTTPTestCase): b'--somefancyboundary--\r\n' ) assert self.fetch( - "/flows/42/request/content", + "/flows/42/request/content.data", method="POST", headers={"Content-Type": 'multipart/form-data; boundary="somefancyboundary"'}, body=body @@ -253,6 +266,19 @@ class TestApp(tornado.testing.AsyncHTTPTestCase): assert self.put_json("/settings", {"anticache": True}).code == 200 assert self.put_json("/settings", {"wtf": True}).code == 400 + def test_options(self): + j = json(self.fetch("/options")) + assert type(j) == dict + assert type(j['anticache']) == dict + + def test_option_update(self): + assert self.put_json("/options", {"anticache": True}).code == 200 + assert self.put_json("/options", {"wtf": True}).code == 400 + assert self.put_json("/options", {"anticache": "foo"}).code == 400 + + def test_option_save(self): + assert self.fetch("/options/save", method="POST").code == 200 + def test_err(self): with mock.patch("mitmproxy.tools.web.app.IndexHandler.get") as f: f.side_effect = RuntimeError @@ -265,19 +291,38 @@ class TestApp(tornado.testing.AsyncHTTPTestCase): ws_client = yield websocket.websocket_connect(ws_url) self.master.options.anticomp = True - response = yield ws_client.read_message() - assert _json.loads(response) == { + r1 = yield ws_client.read_message() + r2 = yield ws_client.read_message() + j1 = _json.loads(r1) + j2 = _json.loads(r2) + response = dict() + response[j1['resource']] = j1 + response[j2['resource']] = j2 + assert response['settings'] == { "resource": "settings", "cmd": "update", "data": {"anticomp": True}, } + assert response['options'] == { + "resource": "options", + "cmd": "update", + "data": { + "anticomp": { + "value": True, + "choices": None, + "default": False, + "help": "Try to convince servers to send us un-compressed data.", + "type": "bool", + } + } + } ws_client.close() # trigger on_close by opening a second connection. ws_client2 = yield websocket.websocket_connect(ws_url) ws_client2.close() - def test_generate_tflow_js(self): + def _test_generate_tflow_js(self): _tflow = app.flow_to_json(tflow.tflow(resp=True, err=True)) # Set some value as constant, so that _tflow.js would not change every time. _tflow['client_conn']['id'] = "4a18d1a0-50a1-48dd-9aa6-d45d74282939" @@ -291,5 +336,5 @@ class TestApp(tornado.testing.AsyncHTTPTestCase): web_root = os.path.join(here, os.pardir, os.pardir, os.pardir, os.pardir, 'web') tflow_path = os.path.join(web_root, 'src/js/__tests__/ducks/_tflow.js') content = """export default function(){{\n return {tflow_json}\n}}""".format(tflow_json=tflow_json) - with open(tflow_path, 'w') as f: + with open(tflow_path, 'w', newline="\n") as f: f.write(content) diff --git a/test/mitmproxy/tools/web/test_master.py b/test/mitmproxy/tools/web/test_master.py index 27f99a18..2bceb5ca 100644 --- a/test/mitmproxy/tools/web/test_master.py +++ b/test/mitmproxy/tools/web/test_master.py @@ -1,7 +1,5 @@ from mitmproxy.tools.web import master -from mitmproxy import proxy from mitmproxy import options -from mitmproxy.proxy.config import ProxyConfig from ... import tservers @@ -9,8 +7,7 @@ from ... import tservers class TestWebMaster(tservers.MasterTest): def mkmaster(self, **opts): o = options.Options(**opts) - c = ProxyConfig(o) - return master.WebMaster(o, proxy.DummyServer(c)) + return master.WebMaster(o) def test_basic(self): m = self.mkmaster() diff --git a/test/mitmproxy/tools/web/test_static_viewer.py b/test/mitmproxy/tools/web/test_static_viewer.py new file mode 100644 index 00000000..cfe6cd7f --- /dev/null +++ b/test/mitmproxy/tools/web/test_static_viewer.py @@ -0,0 +1,69 @@ +import json +from unittest import mock + +from mitmproxy.test import taddons +from mitmproxy.test import tflow + +from mitmproxy import flowfilter +from mitmproxy.tools.web.app import flow_to_json + +from mitmproxy.tools.web import static_viewer +from mitmproxy.addons import save + + +def test_save_static(tmpdir): + tmpdir.mkdir('static') + static_viewer.save_static(tmpdir) + assert len(tmpdir.listdir()) == 2 + assert tmpdir.join('index.html').check(file=1) + assert tmpdir.join('static/static.js').read() == 'MITMWEB_STATIC = true;' + + +def test_save_filter_help(tmpdir): + static_viewer.save_filter_help(tmpdir) + f = tmpdir.join('/filter-help.json') + assert f.check(file=1) + assert f.read() == json.dumps(dict(commands=flowfilter.help)) + + +def test_save_settings(tmpdir): + static_viewer.save_settings(tmpdir) + f = tmpdir.join('/settings.json') + assert f.check(file=1) + + +def test_save_flows(tmpdir): + flows = [tflow.tflow(req=True, resp=None), tflow.tflow(req=True, resp=True)] + static_viewer.save_flows(tmpdir, flows) + assert tmpdir.join('flows.json').check(file=1) + assert tmpdir.join('flows.json').read() == json.dumps([flow_to_json(f) for f in flows]) + + +@mock.patch('mitmproxy.ctx.log') +def test_save_flows_content(ctx, tmpdir): + flows = [tflow.tflow(req=True, resp=None), tflow.tflow(req=True, resp=True)] + with mock.patch('time.time', mock.Mock(side_effect=[1, 2, 2] * 4)): + static_viewer.save_flows_content(tmpdir, flows) + flows_path = tmpdir.join('flows') + assert len(flows_path.listdir()) == len(flows) + for p in flows_path.listdir(): + assert p.join('request').check(dir=1) + assert p.join('response').check(dir=1) + assert p.join('request/content.data').check(file=1) + assert p.join('request/content').check(dir=1) + assert p.join('response/content.data').check(file=1) + assert p.join('response/content').check(dir=1) + assert p.join('request/content/Auto.json').check(file=1) + assert p.join('response/content/Auto.json').check(file=1) + + +def test_static_viewer(tmpdir): + s = static_viewer.StaticViewer() + sa = save.Save() + with taddons.context() as tctx: + sa.save([tflow.tflow(resp=True)], str(tmpdir.join('foo'))) + tctx.master.addons.add(s) + tctx.configure(s, web_static_viewer=str(tmpdir), rfile=str(tmpdir.join('foo'))) + assert tmpdir.join('index.html').check(file=1) + assert tmpdir.join('static').check(dir=1) + assert tmpdir.join('flows').check(dir=1) diff --git a/test/mitmproxy/tservers.py b/test/mitmproxy/tservers.py index 3a2050e1..dd5bb327 100644 --- a/test/mitmproxy/tservers.py +++ b/test/mitmproxy/tservers.py @@ -2,6 +2,7 @@ import os.path import threading import tempfile import sys +from unittest import mock import mitmproxy.platform from mitmproxy.proxy.config import ProxyConfig @@ -23,10 +24,13 @@ class MasterTest: def cycle(self, master, content): f = tflow.tflow(req=tutils.treq(content=content)) - master.addons.handle_lifecycle("clientconnect", f.client_conn) + layer = mock.Mock("mitmproxy.proxy.protocol.base.Layer") + layer.client_conn = f.client_conn + layer.reply = controller.DummyReply() + master.addons.handle_lifecycle("clientconnect", layer) for i in eventsequence.iterate(f): master.addons.handle_lifecycle(*i) - master.addons.handle_lifecycle("clientdisconnect", f.client_conn) + master.addons.handle_lifecycle("clientdisconnect", layer) return f def dummy_cycle(self, master, n, content): @@ -65,9 +69,10 @@ class TestState: class TestMaster(taddons.RecordingMaster): - def __init__(self, opts, config): - s = ProxyServer(config) - super().__init__(opts, s) + def __init__(self, opts): + super().__init__(opts) + config = ProxyConfig(opts) + self.server = ProxyServer(config) def clear_addons(self, addons): self.addons.clear() @@ -125,9 +130,8 @@ class ProxyTestBase: ssl=cls.ssl, ssloptions=cls.ssloptions) - opts = cls.get_options() - cls.config = ProxyConfig(opts) - tmaster = cls.masterclass(opts, cls.config) + cls.options = cls.get_options() + tmaster = cls.masterclass(cls.options) cls.proxy = ProxyThread(tmaster) cls.proxy.start() @@ -334,19 +338,16 @@ class ChainProxyTest(ProxyTestBase): @classmethod def setup_class(cls): + # We need to initialize the chain first so that the normal server gets a correct config. cls.chain = [] - super().setup_class() for _ in range(cls.n): opts = cls.get_options() - config = ProxyConfig(opts) - tmaster = cls.masterclass(opts, config) + tmaster = cls.masterclass(opts) proxy = ProxyThread(tmaster) proxy.start() cls.chain.insert(0, proxy) - # Patch the orginal proxy to upstream mode - opts = cls.get_options() - cls.config = cls.proxy.tmaster.config = cls.proxy.tmaster.server.config = ProxyConfig(opts) + super().setup_class() @classmethod def teardown_class(cls): diff --git a/test/mitmproxy/utils/test_arg_check.py b/test/mitmproxy/utils/test_arg_check.py new file mode 100644 index 00000000..72913955 --- /dev/null +++ b/test/mitmproxy/utils/test_arg_check.py @@ -0,0 +1,36 @@ +import io +import contextlib +from unittest import mock + +import pytest + +from mitmproxy.utils import arg_check + + +@pytest.mark.parametrize('arg, output', [ + (["-T"], "-T is deprecated, please use --mode transparent instead"), + (["-U"], "-U is deprecated, please use --mode upstream:SPEC instead"), + (["--cadir"], "--cadir is deprecated.\n" + "Please use `--set cadir=value` instead.\n" + "To show all options and their default values use --options"), + (["--palette"], "--palette is deprecated.\n" + "Please use `--set console_palette=value` instead.\n" + "To show all options and their default values use --options"), + (["--wfile"], "--wfile is deprecated.\n" + "Please use `--save-stream-file` instead."), + (["--eventlog"], "--eventlog has been removed."), + (["--nonanonymous"], '--nonanonymous is deprecated.\n' + 'Please use `--proxyauth SPEC` instead.\n' + 'SPEC Format: "username:pass", "any" to accept any user/pass combination,\n' + '"@path" to use an Apache htpasswd file, or\n' + '"ldap[s]:url_server_ldap:dn_auth:password:dn_subtree" ' + 'for LDAP authentication.') + +]) +def test_check_args(arg, output): + f = io.StringIO() + with contextlib.redirect_stdout(f): + with mock.patch('sys.argv') as m: + m.__getitem__.return_value = arg + arg_check.check() + assert f.getvalue().strip() == output diff --git a/test/mitmproxy/utils/test_debug.py b/test/mitmproxy/utils/test_debug.py index 4371ef70..0ca6ead0 100644 --- a/test/mitmproxy/utils/test_debug.py +++ b/test/mitmproxy/utils/test_debug.py @@ -1,5 +1,4 @@ import io -import subprocess import sys from unittest import mock import pytest @@ -11,13 +10,7 @@ from mitmproxy.utils import debug def test_dump_system_info_precompiled(precompiled): sys.frozen = None with mock.patch.object(sys, 'frozen', precompiled): - assert ("Precompiled Binary" in debug.dump_system_info()) == precompiled - - -def test_dump_system_info_version(): - with mock.patch('subprocess.check_output') as m: - m.side_effect = subprocess.CalledProcessError(-1, 'git describe --tags --long') - assert 'release version' in debug.dump_system_info() + assert ("binary" in debug.dump_system_info()) == precompiled def test_dump_info(): diff --git a/test/mitmproxy/utils/test_human.py b/test/mitmproxy/utils/test_human.py index 76dc2f88..947cfa4a 100644 --- a/test/mitmproxy/utils/test_human.py +++ b/test/mitmproxy/utils/test_human.py @@ -22,6 +22,7 @@ def test_parse_size(): human.parse_size("1f") with pytest.raises(ValueError): human.parse_size("ak") + assert human.parse_size(None) is None def test_pretty_size(): @@ -53,3 +54,5 @@ def test_format_address(): assert human.format_address(("::ffff:127.0.0.1", "54010", "0", "0")) == "127.0.0.1:54010" assert human.format_address(("127.0.0.1", "54010")) == "127.0.0.1:54010" assert human.format_address(("example.com", "54010")) == "example.com:54010" + assert human.format_address(("::", "8080")) == "*:8080" + assert human.format_address(("0.0.0.0", "8080")) == "*:8080" diff --git a/test/mitmproxy/utils/test_strutils.py b/test/mitmproxy/utils/test_strutils.py index bacd7f62..dfe2c620 100644 --- a/test/mitmproxy/utils/test_strutils.py +++ b/test/mitmproxy/utils/test_strutils.py @@ -96,3 +96,33 @@ def test_clean_hanging_newline(): def test_hexdump(): assert list(strutils.hexdump(b"one\0" * 10)) + + +ESCAPE_QUOTES = [ + "'" + strutils.SINGLELINE_CONTENT + strutils.NO_ESCAPE + "'", + '"' + strutils.SINGLELINE_CONTENT + strutils.NO_ESCAPE + '"' +] + + +def test_split_special_areas(): + assert strutils.split_special_areas("foo", ESCAPE_QUOTES) == ["foo"] + assert strutils.split_special_areas("foo 'bar' baz", ESCAPE_QUOTES) == ["foo ", "'bar'", " baz"] + assert strutils.split_special_areas( + """foo 'b\\'a"r' baz""", + ESCAPE_QUOTES + ) == ["foo ", "'b\\'a\"r'", " baz"] + assert strutils.split_special_areas( + "foo\n/*bar\nbaz*/\nqux", + [r'/\*[\s\S]+?\*/'] + ) == ["foo\n", "/*bar\nbaz*/", "\nqux"] + assert strutils.split_special_areas( + "foo\n//bar\nbaz", + [r'//.+$'] + ) == ["foo\n", "//bar", "\nbaz"] + + +def test_escape_special_areas(): + assert strutils.escape_special_areas('foo "bar" baz', ESCAPE_QUOTES, "*") == 'foo "bar" baz' + esc = strutils.escape_special_areas('foo "b*r" b*z', ESCAPE_QUOTES, "*") + assert esc == 'foo "b\ue02ar" b*z' + assert strutils.unescape_special_areas(esc) == 'foo "b*r" b*z' diff --git a/test/mitmproxy/utils/test_typecheck.py b/test/mitmproxy/utils/test_typecheck.py index fe33070e..5295fff5 100644 --- a/test/mitmproxy/utils/test_typecheck.py +++ b/test/mitmproxy/utils/test_typecheck.py @@ -4,7 +4,6 @@ from unittest import mock import pytest from mitmproxy.utils import typecheck -from mitmproxy import command class TBase: @@ -88,26 +87,9 @@ def test_check_any(): typecheck.check_option_type("foo", None, typing.Any) -def test_check_command_type(): - assert(typecheck.check_command_type("foo", str)) - assert(typecheck.check_command_type(["foo"], typing.Sequence[str])) - assert(not typecheck.check_command_type(["foo", 1], typing.Sequence[str])) - assert(typecheck.check_command_type(None, None)) - assert(not typecheck.check_command_type(["foo"], typing.Sequence[int])) - assert(not typecheck.check_command_type("foo", typing.Sequence[int])) - assert(typecheck.check_command_type([["foo", b"bar"]], command.Cuts)) - assert(not typecheck.check_command_type(["foo", b"bar"], command.Cuts)) - assert(not typecheck.check_command_type([["foo", 22]], command.Cuts)) - - # Python 3.5 only defines __parameters__ - m = mock.Mock() - m.__str__ = lambda self: "typing.Sequence" - m.__parameters__ = (int,) - - typecheck.check_command_type([10], m) - - # Python 3.5 only defines __union_params__ - m = mock.Mock() - m.__str__ = lambda self: "typing.Union" - m.__union_params__ = (int,) - assert not typecheck.check_command_type([22], m) +def test_typesec_to_str(): + assert(typecheck.typespec_to_str(str)) == "str" + assert(typecheck.typespec_to_str(typing.Sequence[str])) == "sequence of str" + assert(typecheck.typespec_to_str(typing.Optional[str])) == "optional str" + with pytest.raises(NotImplementedError): + typecheck.typespec_to_str(dict) diff --git a/test/mitmproxy/utils/test_version_check.py b/test/mitmproxy/utils/test_version_check.py deleted file mode 100644 index d7929378..00000000 --- a/test/mitmproxy/utils/test_version_check.py +++ /dev/null @@ -1,25 +0,0 @@ -import io -from unittest import mock -from mitmproxy.utils import version_check - - -@mock.patch("sys.exit") -def test_check_pyopenssl_version(sexit): - fp = io.StringIO() - version_check.check_pyopenssl_version(fp=fp) - assert not fp.getvalue() - assert not sexit.called - - version_check.check_pyopenssl_version((9999,), fp=fp) - assert "outdated" in fp.getvalue() - assert sexit.called - - -@mock.patch("sys.exit") -@mock.patch("OpenSSL.__version__") -def test_unparseable_pyopenssl_version(version, sexit): - version.split.return_value = ["foo", "bar"] - fp = io.StringIO() - version_check.check_pyopenssl_version(fp=fp) - assert "Cannot parse" in fp.getvalue() - assert not sexit.called diff --git a/test/pathod/protocols/test_http2.py b/test/pathod/protocols/test_http2.py index c16a6d40..95965cee 100644 --- a/test/pathod/protocols/test_http2.py +++ b/test/pathod/protocols/test_http2.py @@ -11,8 +11,6 @@ from ...mitmproxy.net import tservers as net_tservers from pathod.protocols.http2 import HTTP2StateProtocol, TCPHandler -from ...conftest import requires_alpn - class TestTCPHandlerWrapper: def test_wrapped(self): @@ -68,7 +66,6 @@ class TestProtocol: assert mock_server_method.called -@requires_alpn class TestCheckALPNMatch(net_tservers.ServerTestBase): handler = EchoHandler ssl = dict( @@ -78,12 +75,11 @@ class TestCheckALPNMatch(net_tservers.ServerTestBase): def test_check_alpn(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl(alpn_protos=[b'h2']) + c.convert_to_tls(alpn_protos=[b'h2']) protocol = HTTP2StateProtocol(c) assert protocol.check_alpn() -@requires_alpn class TestCheckALPNMismatch(net_tservers.ServerTestBase): handler = EchoHandler ssl = dict( @@ -93,7 +89,7 @@ class TestCheckALPNMismatch(net_tservers.ServerTestBase): def test_check_alpn(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl(alpn_protos=[b'h2']) + c.convert_to_tls(alpn_protos=[b'h2']) protocol = HTTP2StateProtocol(c) with pytest.raises(NotImplementedError): protocol.check_alpn() @@ -211,7 +207,7 @@ class TestApplySettings(net_tservers.ServerTestBase): def test_apply_settings(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl() + c.convert_to_tls() protocol = HTTP2StateProtocol(c) protocol._apply_settings({ @@ -306,7 +302,7 @@ class TestReadRequest(net_tservers.ServerTestBase): def test_read_request(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl() + c.convert_to_tls() protocol = HTTP2StateProtocol(c, is_server=True) protocol.connection_preface_performed = True @@ -332,7 +328,7 @@ class TestReadRequestRelative(net_tservers.ServerTestBase): def test_asterisk_form(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl() + c.convert_to_tls() protocol = HTTP2StateProtocol(c, is_server=True) protocol.connection_preface_performed = True @@ -355,7 +351,7 @@ class TestReadRequestAbsolute(net_tservers.ServerTestBase): def test_absolute_form(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl() + c.convert_to_tls() protocol = HTTP2StateProtocol(c, is_server=True) protocol.connection_preface_performed = True @@ -382,7 +378,7 @@ class TestReadResponse(net_tservers.ServerTestBase): def test_read_response(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl() + c.convert_to_tls() protocol = HTTP2StateProtocol(c) protocol.connection_preface_performed = True @@ -408,7 +404,7 @@ class TestReadEmptyResponse(net_tservers.ServerTestBase): def test_read_empty_response(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl() + c.convert_to_tls() protocol = HTTP2StateProtocol(c) protocol.connection_preface_performed = True diff --git a/test/pathod/test_pathoc.py b/test/pathod/test_pathoc.py index 2dd29e20..297b54d4 100644 --- a/test/pathod/test_pathoc.py +++ b/test/pathod/test_pathoc.py @@ -11,7 +11,6 @@ from pathod.protocols.http2 import HTTP2StateProtocol from mitmproxy.test import tutils from . import tservers -from ..conftest import requires_alpn def test_response(): @@ -216,7 +215,6 @@ class TestDaemonHTTP2(PathocTestDaemon): ssl = True explain = False - @requires_alpn def test_http2(self): c = pathoc.Pathoc( ("127.0.0.1", self.d.port), @@ -231,7 +229,6 @@ class TestDaemonHTTP2(PathocTestDaemon): ) assert c.protocol == http1 - @requires_alpn def test_http2_alpn(self): c = pathoc.Pathoc( ("127.0.0.1", self.d.port), @@ -241,14 +238,13 @@ class TestDaemonHTTP2(PathocTestDaemon): http2_skip_connection_preface=True, ) - tmp_convert_to_ssl = c.convert_to_ssl - c.convert_to_ssl = Mock() - c.convert_to_ssl.side_effect = tmp_convert_to_ssl + tmp_convert_to_tls = c.convert_to_tls + c.convert_to_tls = Mock() + c.convert_to_tls.side_effect = tmp_convert_to_tls with c.connect(): - _, kwargs = c.convert_to_ssl.call_args + _, kwargs = c.convert_to_tls.call_args assert set(kwargs['alpn_protos']) == set([b'http/1.1', b'h2']) - @requires_alpn def test_request(self): c = pathoc.Pathoc( ("127.0.0.1", self.d.port), @@ -259,14 +255,3 @@ class TestDaemonHTTP2(PathocTestDaemon): with c.connect(): resp = c.request("get:/p/200") assert resp.status_code == 200 - - def test_failing_request(self, disable_alpn): - c = pathoc.Pathoc( - ("127.0.0.1", self.d.port), - fp=None, - ssl=True, - use_http2=True, - ) - with pytest.raises(NotImplementedError): - with c.connect(): - c.request("get:/p/200") diff --git a/test/pathod/test_pathod.py b/test/pathod/test_pathod.py index 88480a59..d6522cb6 100644 --- a/test/pathod/test_pathod.py +++ b/test/pathod/test_pathod.py @@ -8,7 +8,6 @@ from mitmproxy import exceptions from mitmproxy.test import tutils from . import tservers -from ..conftest import requires_alpn class TestPathod: @@ -58,7 +57,7 @@ class TestNotAfterConnect(tservers.DaemonTests): class TestCustomCert(tservers.DaemonTests): ssl = True ssloptions = dict( - certs=[(b"*", tutils.test_data.path("pathod/data/testkey.pem"))], + certs=[("*", tutils.test_data.path("pathod/data/testkey.pem"))], ) def test_connect(self): @@ -154,7 +153,7 @@ class CommonTests(tservers.DaemonTests): c = tcp.TCPClient(("localhost", self.d.port)) with c.connect(): if self.ssl: - c.convert_to_ssl() + c.convert_to_tls() c.wfile.write(b"foo\n\n\n") c.wfile.flush() l = self.d.last_log() @@ -242,7 +241,7 @@ class TestDaemonSSL(CommonTests): with c.connect(): c.wfile.write(b"\0\0\0\0") with pytest.raises(exceptions.TlsException): - c.convert_to_ssl() + c.convert_to_tls() l = self.d.last_log() assert l["type"] == "error" assert "SSL" in l["msg"] @@ -257,11 +256,6 @@ class TestHTTP2(tservers.DaemonTests): ssl = True nohang = True - @requires_alpn def test_http2(self): r, _ = self.pathoc(["GET:/"], ssl=True, use_http2=True) assert r[0].status_code == 800 - - def test_no_http2(self, disable_alpn): - with pytest.raises(NotImplementedError): - r, _ = self.pathoc(["GET:/"], ssl=True, use_http2=True) |