Repository: mitmproxy/mitmproxy

Issue:
HTTP/2 does not handle killed requests
##### Steps to reproduce the problem:
1. `mitmdump -S test_replay --kill`
2. Open a website that is not contained in the replay file.
##### What went wrong?

```
127.0.0.1:50907: clientconnect
127.0.0.1:50906: clientconnect
127.0.0.1 GET https://news.ycombinator.com/ HTTP/2.0
 << Connection killed
Exception in thread Thread-2:
Traceback (most recent call last):
  File "c:\python27\Lib\threading.py", line 810, in __bootstrap_inner
    self.run()
  File "c:\users\user\git\mitmproxy\mitmproxy\protocol\http2.py", line 422, in run
    layer()
  File "c:\users\user\git\mitmproxy\mitmproxy\protocol\http.py", line 188, in __call__
    self.process_request_hook(flow)
  File "c:\users\user\git\mitmproxy\mitmproxy\protocol\http.py", line 356, in process_request_hook
    raise Kill()
Kill
```



Discussion:
Do you have a test for this?

https://github.com/mitmproxy/mitmproxy/blob/423c076c61140c2e953313793263a4cac71e33ca/test/mitmproxy/test_server.py#L770-L783 is pretty much the same for HTTP/1.


Resolving patch:
diff --git a/mitmproxy/exceptions.py b/mitmproxy/exceptions.py
--- a/mitmproxy/exceptions.py
+++ b/mitmproxy/exceptions.py
@@ -50,6 +50,10 @@ class HttpProtocolException(ProtocolException):
     pass
 
 
+class Http2ProtocolException(ProtocolException):
+    pass
+
+
 class ServerException(ProxyException):
     pass
 
@@ -83,4 +87,4 @@ def from_exception_context(cls, cut_tb=1):
 
 
 class FlowReadException(ProxyException):
-    pass
\ No newline at end of file
+    pass
diff --git a/mitmproxy/protocol/http.py b/mitmproxy/protocol/http.py
--- a/mitmproxy/protocol/http.py
+++ b/mitmproxy/protocol/http.py
@@ -11,7 +11,7 @@
 from h2.exceptions import H2Error
 
 from .. import utils
-from ..exceptions import HttpProtocolException, ProtocolException
+from ..exceptions import HttpProtocolException, Http2ProtocolException, ProtocolException
 from ..models import (
     HTTPFlow,
     HTTPResponse,
@@ -243,7 +243,7 @@ def send_error_response(self, code, message):
         try:
             response = make_error_response(code, message)
             self.send_response(response)
-        except (NetlibException, H2Error):
+        except (NetlibException, H2Error, Http2ProtocolException):
             self.log(traceback.format_exc(), "debug")
 
     def change_upstream_proxy_server(self, address):
@@ -288,9 +288,9 @@ def get_response():
 
         try:
             get_response()
-        except NetlibException as v:
+        except NetlibException as e:
             self.log(
-                "server communication error: %s" % repr(v),
+                "server communication error: %s" % repr(e),
                 level="debug"
             )
             # In any case, we try to reconnect at least once. This is
@@ -304,6 +304,11 @@ def get_response():
             # > server detects timeout, disconnects
             # > read (100-n)% of large request
             # > send large request upstream
+
+            if isinstance(e, Http2ProtocolException):
+                # do not try to reconnect for HTTP2
+                raise ProtocolException("First and only attempt to get response via HTTP2 failed.")
+
             self.disconnect()
             self.connect()
             get_response()
diff --git a/mitmproxy/protocol/http2.py b/mitmproxy/protocol/http2.py
--- a/mitmproxy/protocol/http2.py
+++ b/mitmproxy/protocol/http2.py
@@ -4,6 +4,7 @@
 import time
 from six.moves import queue
 
+import traceback
 import h2
 import six
 from h2.connection import H2Connection
@@ -15,6 +16,7 @@
 
 from .base import Layer
 from .http import _HttpTransmissionLayer, HttpLayer
+from ..exceptions import ProtocolException, Http2ProtocolException
 from .. import utils
 from ..models import HTTPRequest, HTTPResponse
 
@@ -55,7 +57,7 @@ def safe_update_settings(self, new_settings):
     def safe_send_headers(self, is_zombie, stream_id, headers):
         with self.lock:
             if is_zombie():  # pragma: no cover
-                return
+                raise Http2ProtocolException("Zombie Stream")
             self.send_headers(stream_id, headers.fields)
             self.conn.send(self.data_to_send())
 
@@ -66,7 +68,7 @@ def safe_send_body(self, is_zombie, stream_id, chunks):
                 self.lock.acquire()
                 if is_zombie():  # pragma: no cover
                     self.lock.release()
-                    return
+                    raise Http2ProtocolException("Zombie Stream")
                 max_outbound_frame_size = self.max_outbound_frame_size
                 frame_chunk = chunk[position:position + max_outbound_frame_size]
                 if self.local_flow_control_window(stream_id) < len(frame_chunk):
@@ -79,7 +81,7 @@ def safe_send_body(self, is_zombie, stream_id, chunks):
                 position += max_outbound_frame_size
         with self.lock:
             if is_zombie():  # pragma: no cover
-                return
+                raise Http2ProtocolException("Zombie Stream")
             self.end_stream(stream_id)
             self.conn.send(self.data_to_send())
 
@@ -104,7 +106,7 @@ def _initiate_server_conn(self):
         self.active_conns.append(self.server_conn.connection)
 
     def connect(self):  # pragma: no cover
-        raise ValueError("CONNECT inside an HTTP2 stream is not supported.")
+        raise Http2ProtocolException("HTTP2 layer should already have a connection.")
 
     def set_server(self):  # pragma: no cover
         raise NotImplementedError("Cannot change server for HTTP2 connections.")
@@ -215,6 +217,7 @@ def __call__(self):
                         raw_frame = b''.join(http2_read_raw_frame(source_conn.rfile))
                     except:
                         # read frame failed: connection closed
+                        # kill all streams
                         for stream in self.streams.values():
                             stream.zombie = time.time()
                         return
@@ -232,7 +235,7 @@ def __call__(self):
 class Http2SingleStreamLayer(_HttpTransmissionLayer, threading.Thread):
 
     def __init__(self, ctx, stream_id, request_headers):
-        super(Http2SingleStreamLayer, self).__init__(ctx)
+        super(Http2SingleStreamLayer, self).__init__(ctx, name="Thread-Http2SingleStreamLayer-{}".format(stream_id))
         self.zombie = None
         self.client_stream_id = stream_id
         self.server_stream_id = None
@@ -335,7 +338,7 @@ def send_request(self, message):
         with self.server_conn.h2.lock:
             # We must not assign a stream id if we are already a zombie.
             if self.zombie:  # pragma: no cover
-                return
+                raise Http2ProtocolException("Zombie Stream")
 
             self.server_stream_id = self.server_conn.h2.get_next_available_stream_id()
             self.server_to_client_stream_ids[self.server_stream_id] = self.client_stream_id
@@ -350,6 +353,8 @@ def send_request(self, message):
             self.server_stream_id,
             message.body
         )
+        if self.zombie:  # pragma: no cover
+            raise Http2ProtocolException("Zombie Stream")
 
     def read_response_headers(self):
         self.response_arrived.wait()
@@ -377,7 +382,7 @@ def read_response_body(self, request, response):
                     yield self.response_data_queue.get()
                 return
             if self.zombie:  # pragma: no cover
-                return
+                raise Http2ProtocolException("Zombie Stream")
 
     def send_response_headers(self, response):
         self.client_conn.h2.safe_send_headers(
@@ -385,6 +390,8 @@ def send_response_headers(self, response):
             self.client_stream_id,
             response.headers
         )
+        if self.zombie:  # pragma: no cover
+            raise Http2ProtocolException("Zombie Stream")
 
     def send_response_body(self, _response, chunks):
         self.client_conn.h2.safe_send_body(
@@ -392,6 +399,8 @@ def send_response_body(self, _response, chunks):
             self.client_stream_id,
             chunks
         )
+        if self.zombie:  # pragma: no cover
+            raise Http2ProtocolException("Zombie Stream")
 
     def check_close_connection(self, flow):
         # This layer only handles a single stream.
@@ -404,5 +413,11 @@ def set_server(self, *args, **kwargs):  # pragma: no cover
 
     def run(self):
         layer = HttpLayer(self, self.mode)
-        layer()
+
+        try:
+            layer()
+        except ProtocolException as e:
+            self.log(repr(e), "info")
+            self.log(traceback.format_exc(), "debug")
+
         self.zombie = time.time()


Test patch:
diff --git a/test/mitmproxy/test_protocol_http2.py b/test/mitmproxy/test_protocol_http2.py
--- a/test/mitmproxy/test_protocol_http2.py
+++ b/test/mitmproxy/test_protocol_http2.py
@@ -434,3 +434,51 @@ def test_push_promise_reset(self):
         assert len(bodies) >= 1
         assert b'regular_stream' in bodies
         # the other two bodies might not be transmitted before the reset
+
+@requires_alpn
+class TestConnectionLost(_Http2TestBase, _Http2ServerBase):
+
+    @classmethod
+    def setup_class(self):
+        _Http2TestBase.setup_class()
+        _Http2ServerBase.setup_class()
+
+    @classmethod
+    def teardown_class(self):
+        _Http2TestBase.teardown_class()
+        _Http2ServerBase.teardown_class()
+
+    @classmethod
+    def handle_server_event(self, event, h2_conn, rfile, wfile):
+        if isinstance(event, h2.events.RequestReceived):
+            h2_conn.send_headers(1, [(':status', '200')])
+            wfile.write(h2_conn.data_to_send())
+            wfile.flush()
+            return False
+
+    def test_connection_lost(self):
+        client, h2_conn = self._setup_connection()
+
+        self._send_request(client.wfile, h2_conn, stream_id=1, headers=[
+            (':authority', "127.0.0.1:%s" % self.server.server.address.port),
+            (':method', 'GET'),
+            (':scheme', 'https'),
+            (':path', '/'),
+            ('foo', 'bar')
+        ])
+
+        done = False
+        ended_streams = 0
+        pushed_streams = 0
+        responses = 0
+        while not done:
+            try:
+                raw = b''.join(http2_read_raw_frame(client.rfile))
+                events = h2_conn.receive_data(raw)
+            except:
+                break
+            client.wfile.write(h2_conn.data_to_send())
+            client.wfile.flush()
+
+        if len(self.master.state.flows) == 1:
+            assert self.master.state.flows[0].response is None