Repository: mitmproxy/mitmproxy

Issue:
Make Travis Great Again (Master Edition)
Working on improving the speed and accuracy of Travis's testing.


Discussion:
I actually expected the NO_ALPN test to fail; oh well, next up is trying to get that test to run without updated openssl, which likely will fail a lot more easily.
Aborted OS X build because it's going to be a very long time before it gets run, due to a large backlog of OS X jobs.
I broke the build and it still passed...
Once the OS X build finishes (probably tomorrow), I think this'll no longer be a WIP.
I removed 3.5 minutes from OS X build times, just based on time spent dealing with Homebrew
This is now ready for merge.

Resolving patch:
diff --git a/mitmproxy/net/tcp.py b/mitmproxy/net/tcp.py
--- a/mitmproxy/net/tcp.py
+++ b/mitmproxy/net/tcp.py
@@ -30,10 +30,7 @@
 socket_fileobject = socket.SocketIO
 
 EINTR = 4
-if os.environ.get("NO_ALPN"):
-    HAS_ALPN = False
-else:
-    HAS_ALPN = SSL._lib.Cryptography_HAS_ALPN
+HAS_ALPN = SSL._lib.Cryptography_HAS_ALPN
 
 # To enable all SSL methods use: SSLv23
 # then add options to disable certain methods


Test patch:
diff --git a/test/conftest.py b/test/conftest.py
new file mode 100644
--- /dev/null
+++ b/test/conftest.py
@@ -0,0 +1,14 @@
+import pytest
+import OpenSSL
+import mitmproxy.net.tcp
+
+
+requires_alpn = pytest.mark.skipif(
+    not mitmproxy.net.tcp.HAS_ALPN,
+    reason='requires OpenSSL with ALPN support')
+
+
+@pytest.fixture()
+def disable_alpn(monkeypatch):
+    monkeypatch.setattr(mitmproxy.net.tcp, 'HAS_ALPN', False)
+    monkeypatch.setattr(OpenSSL.SSL._lib, 'Cryptography_HAS_ALPN', False)
diff --git a/test/mitmproxy/net/test_tcp.py b/test/mitmproxy/net/test_tcp.py
--- a/test/mitmproxy/net/test_tcp.py
+++ b/test/mitmproxy/net/test_tcp.py
@@ -6,7 +6,7 @@
 import os
 import threading
 import mock
-
+import pytest
 from OpenSSL import SSL
 
 from mitmproxy import certs
@@ -15,6 +15,7 @@
 from mitmproxy import exceptions
 
 from . import tservers
+from ...conftest import requires_alpn
 
 
 class EchoHandler(tcp.BaseHandler):
@@ -526,40 +527,47 @@ def test_timeout(self):
             tutils.raises(exceptions.TcpTimeout, c.rfile.read, 10)
 
 
+class TestCryptographyALPN:
+
+    def test_has_alpn(self):
+        if 'OPENSSL_ALPN' in os.environ:
+            assert tcp.HAS_ALPN
+            assert SSL._lib.Cryptography_HAS_ALPN
+        elif 'OPENSSL_OLD' in os.environ:
+            assert not tcp.HAS_ALPN
+            assert not SSL._lib.Cryptography_HAS_ALPN
+
+
 class TestALPNClient(tservers.ServerTestBase):
     handler = ALPNHandler
     ssl = dict(
         alpn_select=b"bar"
     )
 
-    if tcp.HAS_ALPN:
-        def test_alpn(self):
-            c = tcp.TCPClient(("127.0.0.1", self.port))
-            with c.connect():
-                c.convert_to_ssl(alpn_protos=[b"foo", b"bar", b"fasel"])
-                assert c.get_alpn_proto_negotiated() == b"bar"
-                assert c.rfile.readline().strip() == b"bar"
-
-        def test_no_alpn(self):
-            c = tcp.TCPClient(("127.0.0.1", self.port))
-            with c.connect():
-                c.convert_to_ssl()
-                assert c.get_alpn_proto_negotiated() == b""
-                assert c.rfile.readline().strip() == b"NONE"
+    @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'),
+    ])
+    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)
 
-    else:
-        def test_none_alpn(self):
-            c = tcp.TCPClient(("127.0.0.1", self.port))
-            with c.connect():
-                c.convert_to_ssl(alpn_protos=[b"foo", b"bar", b"fasel"])
-                assert c.get_alpn_proto_negotiated() == b""
-                assert c.rfile.readline() == b"NONE"
+        c = tcp.TCPClient(("127.0.0.1", self.port))
+        with c.connect():
+            c.convert_to_ssl(alpn_protos=alpn_protos)
+            assert c.get_alpn_proto_negotiated() == expected_negotiated
+            assert c.rfile.readline().strip() == expected_response
 
 
 class TestNoSSLNoALPNClient(tservers.ServerTestBase):
     handler = ALPNHandler
 
-    def test_no_ssl_no_alpn(self):
+    def test_no_ssl_no_alpn(self, disable_alpn):
         c = tcp.TCPClient(("127.0.0.1", self.port))
         with c.connect():
             assert c.get_alpn_proto_negotiated() == b""
diff --git a/test/mitmproxy/protocol/test_http2.py b/test/mitmproxy/protocol/test_http2.py
--- a/test/mitmproxy/protocol/test_http2.py
+++ b/test/mitmproxy/protocol/test_http2.py
@@ -1,7 +1,6 @@
 # coding=utf-8
 
 
-import pytest
 import os
 import tempfile
 import traceback
@@ -17,6 +16,7 @@
 from mitmproxy.net.http import http1, http2
 
 from .. import tservers
+from ...conftest import requires_alpn
 
 import logging
 logging.getLogger("hyper.packages.hpack.hpack").setLevel(logging.WARNING)
@@ -27,11 +27,6 @@
 logging.getLogger("PIL.PngImagePlugin").setLevel(logging.WARNING)
 
 
-requires_alpn = pytest.mark.skipif(
-    not mitmproxy.net.tcp.HAS_ALPN,
-    reason='requires OpenSSL with ALPN support')
-
-
 # inspect the log:
 #   for msg in self.proxy.tmaster.tlog:
 #       print(msg)
diff --git a/test/mitmproxy/test_dump.py b/test/mitmproxy/test_dump.py
--- a/test/mitmproxy/test_dump.py
+++ b/test/mitmproxy/test_dump.py
@@ -51,14 +51,14 @@ def test_error(self):
         assert "error" in o.tfile.getvalue()
 
     def test_replay(self):
-        o = dump.Options(server_replay=["nonexistent"], replay_kill_extra=True)
+        o = dump.Options(http2=False, server_replay=["nonexistent"], replay_kill_extra=True)
         tutils.raises(exceptions.OptionsError, dump.DumpMaster, o, proxy.DummyServer())
 
         with tutils.tmpdir() as t:
             p = os.path.join(t, "rep")
             self.flowfile(p)
 
-            o = dump.Options(server_replay=[p], replay_kill_extra=True)
+            o = dump.Options(http2=False, server_replay=[p], replay_kill_extra=True)
             o.verbosity = 0
             o.flow_detail = 0
             m = dump.DumpMaster(o, proxy.DummyServer())
@@ -66,13 +66,13 @@ def test_replay(self):
             self.cycle(m, b"content")
             self.cycle(m, b"content")
 
-            o = dump.Options(server_replay=[p], replay_kill_extra=False)
+            o = dump.Options(http2=False, server_replay=[p], replay_kill_extra=False)
             o.verbosity = 0
             o.flow_detail = 0
             m = dump.DumpMaster(o, proxy.DummyServer())
             self.cycle(m, b"nonexistent")
 
-            o = dump.Options(client_replay=[p], replay_kill_extra=False)
+            o = dump.Options(http2=False, client_replay=[p], replay_kill_extra=False)
             o.verbosity = 0
             o.flow_detail = 0
             m = dump.DumpMaster(o, proxy.DummyServer())
diff --git a/test/pathod/test_pathoc.py b/test/pathod/test_pathoc.py
--- a/test/pathod/test_pathoc.py
+++ b/test/pathod/test_pathoc.py
@@ -1,8 +1,8 @@
 import io
 from mock import Mock
+import pytest
 
 from mitmproxy.net import http
-from mitmproxy.net import tcp
 from mitmproxy.net.http import http1
 from mitmproxy import exceptions
 
@@ -11,6 +11,7 @@
 
 from mitmproxy.test import tutils
 from . import tservers
+from ..conftest import requires_alpn
 
 
 def test_response():
@@ -211,45 +212,57 @@ class TestDaemonHTTP2(PathocTestDaemon):
     ssl = True
     explain = False
 
-    if tcp.HAS_ALPN:
-
-        def test_http2(self):
-            c = pathoc.Pathoc(
-                ("127.0.0.1", self.d.port),
-                fp=None,
-                ssl=True,
-                use_http2=True,
-            )
-            assert isinstance(c.protocol, HTTP2StateProtocol)
-
-            c = pathoc.Pathoc(
-                ("127.0.0.1", self.d.port),
-            )
-            assert c.protocol == http1
-
-        def test_http2_alpn(self):
-            c = pathoc.Pathoc(
-                ("127.0.0.1", self.d.port),
-                fp=None,
-                ssl=True,
-                use_http2=True,
-                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
-            with c.connect():
-                _, kwargs = c.convert_to_ssl.call_args
-                assert set(kwargs['alpn_protos']) == set([b'http/1.1', b'h2'])
-
-        def test_request(self):
-            c = pathoc.Pathoc(
-                ("127.0.0.1", self.d.port),
-                fp=None,
-                ssl=True,
-                use_http2=True,
-            )
+    @requires_alpn
+    def test_http2(self):
+        c = pathoc.Pathoc(
+            ("127.0.0.1", self.d.port),
+            fp=None,
+            ssl=True,
+            use_http2=True,
+        )
+        assert isinstance(c.protocol, HTTP2StateProtocol)
+
+        c = pathoc.Pathoc(
+            ("127.0.0.1", self.d.port),
+        )
+        assert c.protocol == http1
+
+    @requires_alpn
+    def test_http2_alpn(self):
+        c = pathoc.Pathoc(
+            ("127.0.0.1", self.d.port),
+            fp=None,
+            ssl=True,
+            use_http2=True,
+            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
+        with c.connect():
+            _, kwargs = c.convert_to_ssl.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),
+            fp=None,
+            ssl=True,
+            use_http2=True,
+        )
+        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():
-                resp = c.request("get:/p/200")
-            assert resp.status_code == 200
+                c.request("get:/p/200")
diff --git a/test/pathod/test_pathod.py b/test/pathod/test_pathod.py
--- a/test/pathod/test_pathod.py
+++ b/test/pathod/test_pathod.py
@@ -1,11 +1,14 @@
 import io
 
+import pytest
+
 from pathod import pathod
 from mitmproxy.net import tcp
 from mitmproxy import exceptions
 from mitmproxy.test import tutils
 
 from . import tservers
+from ..conftest import requires_alpn
 
 
 class TestPathod:
@@ -257,8 +260,11 @@ class TestHTTP2(tservers.DaemonTests):
     ssl = True
     nohang = True
 
-    if tcp.HAS_ALPN:
+    @requires_alpn
+    def test_http2(self):
+        r, _ = self.pathoc(["GET:/"], ssl=True, use_http2=True)
+        assert r[0].status_code == 800
 
-        def test_http2(self):
+    def test_no_http2(self, disable_alpn):
+        with pytest.raises(NotImplementedError):
             r, _ = self.pathoc(["GET:/"], ssl=True, use_http2=True)
-            assert r[0].status_code == 800
diff --git a/test/pathod/test_protocols_http2.py b/test/pathod/test_protocols_http2.py
--- a/test/pathod/test_protocols_http2.py
+++ b/test/pathod/test_protocols_http2.py
@@ -11,6 +11,8 @@
 
 from pathod.protocols.http2 import HTTP2StateProtocol, TCPHandler
 
+from ..conftest import requires_alpn
+
 
 class TestTCPHandlerWrapper:
     def test_wrapped(self):
@@ -66,37 +68,35 @@ def test_perform_connection_preface_server(self, mock_client_method, mock_server
         assert mock_server_method.called
 
 
+@requires_alpn
 class TestCheckALPNMatch(net_tservers.ServerTestBase):
     handler = EchoHandler
     ssl = dict(
         alpn_select=b'h2',
     )
 
-    if tcp.HAS_ALPN:
-
-        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'])
-                protocol = HTTP2StateProtocol(c)
-                assert protocol.check_alpn()
+    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'])
+            protocol = HTTP2StateProtocol(c)
+            assert protocol.check_alpn()
 
 
+@requires_alpn
 class TestCheckALPNMismatch(net_tservers.ServerTestBase):
     handler = EchoHandler
     ssl = dict(
         alpn_select=None,
     )
 
-    if tcp.HAS_ALPN:
-
-        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'])
-                protocol = HTTP2StateProtocol(c)
-                with raises(NotImplementedError):
-                    protocol.check_alpn()
+    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'])
+            protocol = HTTP2StateProtocol(c)
+            with raises(NotImplementedError):
+                protocol.check_alpn()
 
 
 class TestPerformServerConnectionPreface(net_tservers.ServerTestBase):