Repository: mitmproxy/mitmproxy

Issue:
AttributeError: 'NoneType' object has no attribute 'alpn_protocols'
##### Steps to reproduce the problem:

I am following http://docs.mitmproxy.org/en/stable/features/setheaders.html to set header for a page in mitmdump.
1. I want to set "Host" request header of page https://tinhte.vn to "127.0.0.1., so I run the mitmdump command with --setheader, this in my proxy computer.
   mitmdump -R https://tinhte.vn --setheader :~q:Host:127.0.0.1
2. In my victim computer, I browse to https://tinhte.vn (page above)
3. I got the error `AttributeError: 'NoneType' object has no attribute 'alpn_protocols'`
##### What is the expected behavior?

The page will load successful in victim browser with HOST request header is set to "127.0.0.1"
##### What went wrong?

Mitmdump crash with this message:

```
192.168.6.179:49564: clientconnect
Traceback (most recent call last):
  File "/home/hynguyen/mitmproxy/mitmproxy/proxy/server.py", line 119, in handle
    root_layer()
  File "/home/hynguyen/mitmproxy/mitmproxy/proxy/modes/reverse_proxy.py", line 15, in __call__
    layer()
  File "/home/hynguyen/mitmproxy/mitmproxy/protocol/tls.py", line 372, in __call__
    self._client_hello.alpn_protocols or
AttributeError: 'NoneType' object has no attribute 'alpn_protocols'

mitmproxy has crashed!
Please lodge a bug report at: https://github.com/mitmproxy/mitmproxy
192.168.6.179:49564: Traceback (most recent call last):
  File "/home/myname/mitmproxy/mitmproxy/proxy/server.py", line 119, in handle
    root_layer()
  File "/home/myname/mitmproxy/mitmproxy/proxy/modes/reverse_proxy.py", line 15, in __call__
    layer()
  File "/home/hynguyen/mitmproxy/mitmproxy/protocol/tls.py", line 372, in __call__
    self._client_hello.alpn_protocols or
AttributeError: 'NoneType' object has no attribute 'alpn_protocols'
```
##### Any other comments? What have you tried so far?

Nope

---

Mitmproxy Version: 0.18
Operating System: LinuxMint 18

<!-- Please use the mitmproxy forums (https://discourse.mitmproxy.org/) for support/how-to questions. Thanks! :) -->

AttributeError: 'NoneType' object has no attribute 'alpn_protocols'
##### Steps to reproduce the problem:

I am following http://docs.mitmproxy.org/en/stable/features/setheaders.html to set header for a page in mitmdump.
1. I want to set "Host" request header of page https://tinhte.vn to "127.0.0.1., so I run the mitmdump command with --setheader, this in my proxy computer.
   mitmdump -R https://tinhte.vn --setheader :~q:Host:127.0.0.1
2. In my victim computer, I browse to https://tinhte.vn (page above)
3. I got the error `AttributeError: 'NoneType' object has no attribute 'alpn_protocols'`
##### What is the expected behavior?

The page will load successful in victim browser with HOST request header is set to "127.0.0.1"
##### What went wrong?

Mitmdump crash with this message:

```
192.168.6.179:49564: clientconnect
Traceback (most recent call last):
  File "/home/hynguyen/mitmproxy/mitmproxy/proxy/server.py", line 119, in handle
    root_layer()
  File "/home/hynguyen/mitmproxy/mitmproxy/proxy/modes/reverse_proxy.py", line 15, in __call__
    layer()
  File "/home/hynguyen/mitmproxy/mitmproxy/protocol/tls.py", line 372, in __call__
    self._client_hello.alpn_protocols or
AttributeError: 'NoneType' object has no attribute 'alpn_protocols'

mitmproxy has crashed!
Please lodge a bug report at: https://github.com/mitmproxy/mitmproxy
192.168.6.179:49564: Traceback (most recent call last):
  File "/home/myname/mitmproxy/mitmproxy/proxy/server.py", line 119, in handle
    root_layer()
  File "/home/myname/mitmproxy/mitmproxy/proxy/modes/reverse_proxy.py", line 15, in __call__
    layer()
  File "/home/hynguyen/mitmproxy/mitmproxy/protocol/tls.py", line 372, in __call__
    self._client_hello.alpn_protocols or
AttributeError: 'NoneType' object has no attribute 'alpn_protocols'
```
##### Any other comments? What have you tried so far?

Nope

---

Mitmproxy Version: 0.18
Operating System: LinuxMint 18

<!-- Please use the mitmproxy forums (https://discourse.mitmproxy.org/) for support/how-to questions. Thanks! :) -->



Discussion:
Thanks for the report!

Repro:

```
λ mitmdump -R https://example.com
λ curl localhost:8080
```

There are a couple of places in tls.py where we reference `_client_hello` that should be fixed. Also, this definitely needs a regression test.

Thanks for the report!

Repro:

```
λ mitmdump -R https://example.com
λ curl localhost:8080
```

There are a couple of places in tls.py where we reference `_client_hello` that should be fixed. Also, this definitely needs a regression test.


Resolving patch:
diff --git a/mitmproxy/protocol/tls.py b/mitmproxy/protocol/tls.py
--- a/mitmproxy/protocol/tls.py
+++ b/mitmproxy/protocol/tls.py
@@ -369,8 +369,10 @@ def __call__(self):
             not self.config.options.no_upstream_cert and
             (
                 self.config.options.add_upstream_certs_to_client_chain or
-                self._client_hello.alpn_protocols or
-                not self._client_hello.sni
+                self._client_tls and (
+                    self._client_hello.alpn_protocols or
+                    not self._client_hello.sni
+                )
             )
         )
         establish_server_tls_now = (
@@ -434,7 +436,7 @@ def server_sni(self):
         if self._custom_server_sni is False:
             return None
         else:
-            return self._custom_server_sni or self._client_hello.sni
+            return self._custom_server_sni or self._client_hello and self._client_hello.sni
 
     @property
     def alpn_for_client_connection(self):
@@ -509,21 +511,18 @@ def _establish_tls_with_client(self):
     def _establish_tls_with_server(self):
         self.log("Establish TLS with server", "debug")
         try:
-            # We only support http/1.1 and h2.
-            # If the server only supports spdy (next to http/1.1), it may select that
-            # and mitmproxy would enter TCP passthrough mode, which we want to avoid.
-            def deprecated_http2_variant(x):
-                return x.startswith(b"h2-") or x.startswith(b"spdy")
-
-            if self._client_hello.alpn_protocols:
-                alpn = [x for x in self._client_hello.alpn_protocols if not deprecated_http2_variant(x)]
-            else:
-                alpn = None
-            if alpn and b"h2" in alpn and not self.config.options.http2:
-                alpn.remove(b"h2")
+            alpn = None
+            if self._client_tls:
+                if self._client_hello.alpn_protocols:
+                    # We only support http/1.1 and h2.
+                    # If the server only supports spdy (next to http/1.1), it may select that
+                    # and mitmproxy would enter TCP passthrough mode, which we want to avoid.
+                    alpn = [x for x in self._client_hello.alpn_protocols if not (x.startswith(b"h2-") or x.startswith(b"spdy"))]
+                if alpn and b"h2" in alpn and not self.config.options.http2:
+                    alpn.remove(b"h2")
 
             ciphers_server = self.config.options.ciphers_server
-            if not ciphers_server:
+            if not ciphers_server and self._client_tls:
                 ciphers_server = []
                 for id in self._client_hello.cipher_suites:
                     if id in CIPHER_ID_NAME_MAP.keys():
@@ -562,7 +561,8 @@ def deprecated_http2_variant(x):
                 sys.exc_info()[2]
             )
 
-        self.log("ALPN selected by server: %s" % self.alpn_for_client_connection, "debug")
+        proto = self.alpn_for_client_connection.decode() if self.alpn_for_client_connection else '-'
+        self.log("ALPN selected by server: {}".format(proto), "debug")
 
     def _find_cert(self):
         """


Test patch:
diff --git a/test/mitmproxy/test_proxy.py b/test/mitmproxy/test_proxy.py
--- a/test/mitmproxy/test_proxy.py
+++ b/test/mitmproxy/test_proxy.py
@@ -85,22 +85,22 @@ def test_no_transparent(self):
 
     @mock.patch("mitmproxy.platform.resolver")
     def test_modes(self, _):
-        # self.assert_noerr("-R", "http://localhost")
-        # self.assert_err("expected one argument", "-R")
-        # self.assert_err("Invalid server specification", "-R", "reverse")
-        #
-        # self.assert_noerr("-T")
-        #
-        # self.assert_noerr("-U", "http://localhost")
-        # self.assert_err("expected one argument", "-U")
-        # self.assert_err("Invalid server specification", "-U", "upstream")
-        #
-        # self.assert_noerr("--upstream-auth", "test:test")
-        # self.assert_err("expected one argument", "--upstream-auth")
+        self.assert_noerr("-R", "http://localhost")
+        self.assert_err("expected one argument", "-R")
+        self.assert_err("Invalid server specification", "-R", "reverse")
+
+        self.assert_noerr("-T")
+
+        self.assert_noerr("-U", "http://localhost")
+        self.assert_err("expected one argument", "-U")
+        self.assert_err("Invalid server specification", "-U", "upstream")
+
+        self.assert_noerr("--upstream-auth", "test:test")
+        self.assert_err("expected one argument", "--upstream-auth")
         self.assert_err(
             "Invalid upstream auth specification", "--upstream-auth", "test"
         )
-        # self.assert_err("mutually exclusive", "-R", "http://localhost", "-T")
+        self.assert_err("mutually exclusive", "-R", "http://localhost", "-T")
 
     def test_socks_auth(self):
         self.assert_err(
diff --git a/test/mitmproxy/test_server.py b/test/mitmproxy/test_server.py
--- a/test/mitmproxy/test_server.py
+++ b/test/mitmproxy/test_server.py
@@ -472,6 +472,11 @@ class TestReverse(tservers.ReverseProxyTest, CommonMixin, TcpMixin):
     reverse = True
 
 
+class TestReverseSSL(tservers.ReverseProxyTest, CommonMixin, TcpMixin):
+    reverse = True
+    ssl = True
+
+
 class TestSocks5(tservers.SocksModeTest):
 
     def test_simple(self):