From d86cb76e5ba38c51b7d6017fb151e4946e3fb916 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Thu, 27 Oct 2016 17:44:41 -0700 Subject: [PATCH] http2: add push indicator --- mitmproxy/addons/dumper.py | 3 +- mitmproxy/flow.py | 5 +- mitmproxy/io_compat.py | 1 + mitmproxy/proxy/protocol/http.py | 191 ++++++++++++++++-------------- mitmproxy/proxy/protocol/http1.py | 2 +- mitmproxy/proxy/protocol/http2.py | 7 +- mitmproxy/tools/console/common.py | 9 +- 7 files changed, 119 insertions(+), 99 deletions(-) diff --git a/mitmproxy/addons/dumper.py b/mitmproxy/addons/dumper.py index d690c0003..fb92c629f 100644 --- a/mitmproxy/addons/dumper.py +++ b/mitmproxy/addons/dumper.py @@ -116,7 +116,8 @@ class Dumper: else: client = "" - method = flow.request.method + pushed = ' PUSH_PROMISE' if 'h2-pushed-stream' in flow.metadata else '' + method = flow.request.method + pushed method_color = dict( GET="green", DELETE="red" diff --git a/mitmproxy/flow.py b/mitmproxy/flow.py index ff7a2b4a1..18395be38 100644 --- a/mitmproxy/flow.py +++ b/mitmproxy/flow.py @@ -7,7 +7,7 @@ from mitmproxy import stateobject from mitmproxy import connections from mitmproxy import version -from typing import Optional # noqa +from typing import Optional, Dict # noqa class Error(stateobject.StateObject): @@ -83,6 +83,7 @@ class Flow(stateobject.StateObject): self._backup = None # type: Optional[Flow] self.reply = None # type: Optional[controller.Reply] self.marked = False # type: bool + self.metadata = dict() # type: Dict[str, str] _stateobject_attributes = dict( id=str, @@ -92,6 +93,7 @@ class Flow(stateobject.StateObject): type=str, intercepted=bool, marked=bool, + metadata=dict, ) def get_state(self): @@ -120,6 +122,7 @@ class Flow(stateobject.StateObject): f.live = False f.client_conn = self.client_conn.copy() f.server_conn = self.server_conn.copy() + f.metadata = self.metadata.copy() if self.error: f.error = self.error.copy() diff --git a/mitmproxy/io_compat.py b/mitmproxy/io_compat.py index e1ca27b23..b1b5a2961 100644 --- a/mitmproxy/io_compat.py +++ b/mitmproxy/io_compat.py @@ -69,6 +69,7 @@ def convert_018_019(data): data["client_conn"]["sni"] = None data["client_conn"]["cipher_name"] = None data["client_conn"]["tls_version"] = None + data["metadata"] = dict() return data diff --git a/mitmproxy/proxy/protocol/http.py b/mitmproxy/proxy/protocol/http.py index a47fb4554..542f6a94a 100644 --- a/mitmproxy/proxy/protocol/http.py +++ b/mitmproxy/proxy/protocol/http.py @@ -12,14 +12,14 @@ from mitmproxy.net import websockets class _HttpTransmissionLayer(base.Layer): - def read_request_headers(self): + def read_request_headers(self, flow): raise NotImplementedError() def read_request_body(self, request): raise NotImplementedError() - def read_request(self): - request = self.read_request_headers() + def read_request(self, f): + request = self.read_request_headers(f) request.data.content = b"".join( self.read_request_body(request) ) @@ -125,7 +125,7 @@ class HttpLayer(base.Layer): def __init__(self, ctx, mode): super().__init__(ctx) self.mode = mode - + self.flow = None # type: http.HTTPFlow self.__initial_server_conn = None "Contains the original destination in transparent mode, which needs to be restored" "if an inline script modified the target server for a single http request" @@ -140,105 +140,112 @@ class HttpLayer(base.Layer): self.__initial_server_tls = self.server_tls self.__initial_server_conn = self.server_conn while True: - f = http.HTTPFlow(self.client_conn, self.server_conn, live=self) - try: - request = self.get_request_from_client(f) - # Make sure that the incoming request matches our expectations - self.validate_request(request) - except exceptions.HttpReadDisconnect: - # don't throw an error for disconnects that happen before/between requests. + self.flow = http.HTTPFlow(self.client_conn, self.server_conn, live=self) + if not self._process_flow(self.flow): return - except exceptions.HttpException as e: - # We optimistically guess there might be an HTTP client on the - # other end - self.send_error_response(400, repr(e)) - raise exceptions.ProtocolException( - "HTTP protocol error in client request: {}".format(e) + + def _process_flow(self, f): + try: + request = self.get_request_from_client(f) + # Make sure that the incoming request matches our expectations + self.validate_request(request) + except exceptions.HttpReadDisconnect: + # don't throw an error for disconnects that happen before/between requests. + return False + except exceptions.HttpException as e: + # We optimistically guess there might be an HTTP client on the + # other end + self.send_error_response(400, repr(e)) + raise exceptions.ProtocolException( + "HTTP protocol error in client request: {}".format(e) + ) + + self.log("request", "debug", [repr(request)]) + + # Handle Proxy Authentication + # Proxy Authentication conceptually does not work in transparent mode. + # We catch this misconfiguration on startup. Here, we sort out requests + # after a successful CONNECT request (which do not need to be validated anymore) + if not (self.http_authenticated or self.authenticate(request)): + return False + + f.request = request + + try: + # Regular Proxy Mode: Handle CONNECT + if self.mode == "regular" and request.first_line_format == "authority": + self.handle_regular_mode_connect(request) + return False + except (exceptions.ProtocolException, exceptions.NetlibException) as e: + # HTTPS tasting means that ordinary errors like resolution and + # connection errors can happen here. + self.send_error_response(502, repr(e)) + f.error = flow.Error(str(e)) + self.channel.ask("error", f) + return False + + # update host header in reverse proxy mode + if self.config.options.mode == "reverse": + f.request.headers["Host"] = self.config.upstream_server.address.host + + # set upstream auth + if self.mode == "upstream" and self.config.upstream_auth is not None: + f.request.headers["Proxy-Authorization"] = self.config.upstream_auth + self.process_request_hook(f) + + try: + if websockets.check_handshake(request.headers) and websockets.check_client_version(request.headers): + # We only support RFC6455 with WebSockets version 13 + # allow inline scripts to manipulate the client handshake + self.channel.ask("websocket_handshake", f) + + if not f.response: + self.establish_server_connection( + f.request.host, + f.request.port, + f.request.scheme ) + self.get_response_from_server(f) + else: + # response was set by an inline script. + # we now need to emulate the responseheaders hook. + self.channel.ask("responseheaders", f) - self.log("request", "debug", [repr(request)]) + self.log("response", "debug", [repr(f.response)]) + self.channel.ask("response", f) + self.send_response_to_client(f) - # Handle Proxy Authentication - # Proxy Authentication conceptually does not work in transparent mode. - # We catch this misconfiguration on startup. Here, we sort out requests - # after a successful CONNECT request (which do not need to be validated anymore) - if not (self.http_authenticated or self.authenticate(request)): - return + if self.check_close_connection(f): + return False - f.request = request + # Handle 101 Switching Protocols + if f.response.status_code == 101: + self.handle_101_switching_protocols(f) + return False # should never be reached - try: - # Regular Proxy Mode: Handle CONNECT - if self.mode == "regular" and request.first_line_format == "authority": - self.handle_regular_mode_connect(request) - return - except (exceptions.ProtocolException, exceptions.NetlibException) as e: - # HTTPS tasting means that ordinary errors like resolution and - # connection errors can happen here. - self.send_error_response(502, repr(e)) + # Upstream Proxy Mode: Handle CONNECT + if f.request.first_line_format == "authority" and f.response.status_code == 200: + self.handle_upstream_mode_connect(f.request.copy()) + return False + + except (exceptions.ProtocolException, exceptions.NetlibException) as e: + self.send_error_response(502, repr(e)) + if not f.response: f.error = flow.Error(str(e)) self.channel.ask("error", f) - return + return False + else: + raise exceptions.ProtocolException( + "Error in HTTP connection: %s" % repr(e) + ) + finally: + if f: + f.live = False - # update host header in reverse proxy mode - if self.config.options.mode == "reverse": - f.request.headers["Host"] = self.config.upstream_server.address.host - - # set upstream auth - if self.mode == "upstream" and self.config.upstream_auth is not None: - f.request.headers["Proxy-Authorization"] = self.config.upstream_auth - self.process_request_hook(f) - - try: - if websockets.check_handshake(request.headers) and websockets.check_client_version(request.headers): - # We only support RFC6455 with WebSockets version 13 - # allow inline scripts to manipulate the client handshake - self.channel.ask("websocket_handshake", f) - - if not f.response: - self.establish_server_connection( - f.request.host, - f.request.port, - f.request.scheme - ) - self.get_response_from_server(f) - else: - # response was set by an inline script. - # we now need to emulate the responseheaders hook. - self.channel.ask("responseheaders", f) - - self.log("response", "debug", [repr(f.response)]) - self.channel.ask("response", f) - self.send_response_to_client(f) - - if self.check_close_connection(f): - return - - # Handle 101 Switching Protocols - if f.response.status_code == 101: - return self.handle_101_switching_protocols(f) - - # Upstream Proxy Mode: Handle CONNECT - if f.request.first_line_format == "authority" and f.response.status_code == 200: - self.handle_upstream_mode_connect(f.request.copy()) - return - - except (exceptions.ProtocolException, exceptions.NetlibException) as e: - self.send_error_response(502, repr(e)) - if not f.response: - f.error = flow.Error(str(e)) - self.channel.ask("error", f) - return - else: - raise exceptions.ProtocolException( - "Error in HTTP connection: %s" % repr(e) - ) - finally: - if f: - f.live = False + return True def get_request_from_client(self, f): - request = self.read_request() + request = self.read_request(f) f.request = request self.channel.ask("requestheaders", f) if request.headers.get("expect", "").lower() == "100-continue": diff --git a/mitmproxy/proxy/protocol/http1.py b/mitmproxy/proxy/protocol/http1.py index 713c48a76..b1fd0ecd5 100644 --- a/mitmproxy/proxy/protocol/http1.py +++ b/mitmproxy/proxy/protocol/http1.py @@ -9,7 +9,7 @@ class Http1Layer(httpbase._HttpTransmissionLayer): super().__init__(ctx) self.mode = mode - def read_request_headers(self): + def read_request_headers(self, flow): return http.HTTPRequest.wrap( http1.read_request_head(self.client_conn.rfile) ) diff --git a/mitmproxy/proxy/protocol/http2.py b/mitmproxy/proxy/protocol/http2.py index b1154fae3..15939e7dd 100644 --- a/mitmproxy/proxy/protocol/http2.py +++ b/mitmproxy/proxy/protocol/http2.py @@ -242,6 +242,7 @@ class Http2Layer(base.Layer): def _handle_pushed_stream_received(self, event, h2_connection): # pushed stream ids should be unique and not dependent on race conditions # only the parent stream id must be looked up first + parent_eid = self.server_to_client_stream_ids[event.parent_stream_id] with self.client_conn.h2.lock: self.client_conn.h2.push_stream(parent_eid, event.pushed_stream_id, event.headers) @@ -454,9 +455,13 @@ class Http2SingleStreamLayer(httpbase._HttpTransmissionLayer, basethread.BaseThr raise exceptions.Http2ZombieException("Connection already dead") @detect_zombie_stream - def read_request_headers(self): + def read_request_headers(self, flow): self.request_arrived.wait() self.raise_zombie() + + if self.pushed: + flow.metadata['h2-pushed-stream'] = True + first_line_format, method, scheme, host, port, path = http2.parse_headers(self.request_headers) return http.HTTPRequest( first_line_format, diff --git a/mitmproxy/tools/console/common.py b/mitmproxy/tools/console/common.py index 788607028..08bf0b67f 100644 --- a/mitmproxy/tools/console/common.py +++ b/mitmproxy/tools/console/common.py @@ -327,7 +327,7 @@ def export_to_clip_or_file(key, scope, flow, writer): @lru_cache(maxsize=800) -def raw_format_flow(f): +def raw_format_flow(f, flow): f = dict(f) pile = [] req = [] @@ -346,7 +346,9 @@ def raw_format_flow(f): if f["req_is_replay"]: req.append(fcol(SYMBOL_REPLAY, "replay")) - req.append(fcol(f["req_method"], "method")) + + pushed = ' PUSH_PROMISE' if 'h2-pushed-stream' in flow.metadata else '' + req.append(fcol(f["req_method"] + pushed, "method")) preamble = sum(i[1] for i in req) + len(req) - 1 @@ -451,10 +453,11 @@ def format_flow(f, focus, extended=False, hostheader=False, max_url_len=False): resp_clen = contentdesc, roundtrip = roundtrip, )) + t = f.response.headers.get("content-type") if t: d["resp_ctype"] = t.split(";")[0] else: d["resp_ctype"] = "" - return raw_format_flow(tuple(sorted(d.items()))) + return raw_format_flow(tuple(sorted(d.items())), f)