diff --git a/CHANGELOG b/CHANGELOG index 0a67d00e0..a8d6b565f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,7 +16,7 @@ Unreleased: mitmproxy next * Fix file unlinking before external viewer finishes loading (@wchasekelley) * Add --cert-passphrase command line argument (@mirosyn) * Add interactive tutorials to the documentation (@mplattner) - * Add support for sending (but not parsing) HTTP Trailers to the HTTP/1 protocol (@bburky) + * Add support for sending (but not parsing) HTTP Trailers to the HTTP/1.1 protocol (@bburky) * --- TODO: add new PRs above this line --- diff --git a/docs/src/content/concepts-protocols.md b/docs/src/content/concepts-protocols.md index f985e81bf..9a3dd6e48 100644 --- a/docs/src/content/concepts-protocols.md +++ b/docs/src/content/concepts-protocols.md @@ -16,7 +16,7 @@ menu: HTTP/1.0 and HTTP/1.1 support in mitmproxy is based on our custom HTTP stack, which takes care of all semantics and on-the-wire parsing/serialization tasks. -mitmproxy currently does not parsing HTTP trailers - but if you want to send +mitmproxy currently does not support parsing HTTP trailers - but if you want to send us a PR, we promise to take look! ## HTTP/2 diff --git a/examples/addons/http-trailers.py b/examples/addons/http-trailers.py index 01a13ff7a..ba0732ba1 100644 --- a/examples/addons/http-trailers.py +++ b/examples/addons/http-trailers.py @@ -16,9 +16,17 @@ def request(flow: http.HTTPFlow): print("HTTP Trailers detected! Request contains:", flow.request.trailers) if flow.request.path == "/inject_trailers": - if not flow.request.is_http2: - # HTTP 1.0 requires transfer-encoding: chunked to send trailers + if flow.request.is_http10: + # HTTP/1.0 doesn't support trailers + return + elif flow.request.is_http11: + if not flow.request.content: + # Avoid sending a body on GET requests or a 0 byte chunked body with trailers. + # Otherwise some servers return 400 Bad Request. + return + # HTTP 1.1 requires transfer-encoding: chunked to send trailers flow.request.headers["transfer-encoding"] = "chunked" + # HTTP 2+ supports trailers on all requests/responses flow.request.headers["trailer"] = "x-my-injected-trailer-header" flow.request.trailers = Headers([ @@ -32,8 +40,11 @@ def response(flow: http.HTTPFlow): print("HTTP Trailers detected! Response contains:", flow.response.trailers) if flow.request.path == "/inject_trailers": - if not flow.response.is_http2: - # HTTP 1.0 requires transfer-encoding: chunked to send trailers + if flow.request.is_http10: + return + elif flow.request.is_http11: + if not flow.response.content: + return flow.response.headers["transfer-encoding"] = "chunked" flow.response.headers["trailer"] = "x-my-injected-trailer-header" diff --git a/mitmproxy/net/http/http1/assemble.py b/mitmproxy/net/http/http1/assemble.py index 2975fa404..ac72fab89 100644 --- a/mitmproxy/net/http/http1/assemble.py +++ b/mitmproxy/net/http/http1/assemble.py @@ -35,12 +35,12 @@ def assemble_body(headers, body_chunks, trailers): if chunk: yield b"%x\r\n%s\r\n" % (len(chunk), chunk) if trailers: - yield b"0\r\n%s\r\n" % (trailers,) + yield b"0\r\n%s\r\n" % trailers else: yield b"0\r\n\r\n" else: if trailers: - raise exceptions.HttpException("Sending HTTP/1 trailer headers requires transfer-encoding: chunked") + raise exceptions.HttpException("Sending HTTP/1.1 trailer headers requires transfer-encoding: chunked") for chunk in body_chunks: yield chunk diff --git a/mitmproxy/net/http/message.py b/mitmproxy/net/http/message.py index ba3269aa0..3a1d0577b 100644 --- a/mitmproxy/net/http/message.py +++ b/mitmproxy/net/http/message.py @@ -70,6 +70,14 @@ class Message(serializable.Serializable): def http_version(self, http_version: Union[str, bytes]) -> None: self.data.http_version = strutils.always_bytes(http_version, "utf-8", "surrogateescape") + @property + def is_http10(self) -> bool: + return self.data.http_version == b"HTTP/1.0" + + @property + def is_http11(self) -> bool: + return self.data.http_version == b"HTTP/1.1" + @property def is_http2(self) -> bool: return self.data.http_version == b"HTTP/2.0" diff --git a/mitmproxy/proxy/protocol/http1.py b/mitmproxy/proxy/protocol/http1.py index 158b9c23e..dbd567f2d 100644 --- a/mitmproxy/proxy/protocol/http1.py +++ b/mitmproxy/proxy/protocol/http1.py @@ -23,7 +23,7 @@ class Http1Layer(httpbase._HttpTransmissionLayer): def read_request_trailers(self, request): if "Trailer" in request.headers: # TODO: not implemented yet - self.log("HTTP/1 request trailer headers are not implemented yet!", "warn") + self.log("HTTP/1.1 request trailer headers are not implemented yet!", "warn") return None def send_request_headers(self, request): @@ -37,7 +37,7 @@ class Http1Layer(httpbase._HttpTransmissionLayer): self.server_conn.wfile.flush() def send_request_trailers(self, request): - # HTTP/1 request trailer headers are sent in the body + # HTTP/1.1 request trailer headers are sent in the body pass def send_request(self, request): @@ -59,7 +59,7 @@ class Http1Layer(httpbase._HttpTransmissionLayer): # Trailers should actually be parsed unconditionally, the "Trailer" header is optional if "Trailer" in response.headers: # TODO: not implemented yet - self.log("HTTP/1 trailer headers are not implemented yet!", "warn") + self.log("HTTP/1.1 trailer headers are not implemented yet!", "warn") return None def send_response_headers(self, response): @@ -73,7 +73,7 @@ class Http1Layer(httpbase._HttpTransmissionLayer): self.client_conn.wfile.flush() def send_response_trailers(self, response): - # HTTP/1 response trailer headers are sent in the body + # HTTP/1.1 response trailer headers are sent in the body pass def check_close_connection(self, flow): diff --git a/test/mitmproxy/net/http/http1/test_assemble.py b/test/mitmproxy/net/http/http1/test_assemble.py index 6269e8143..4fea192fc 100644 --- a/test/mitmproxy/net/http/http1/test_assemble.py +++ b/test/mitmproxy/net/http/http1/test_assemble.py @@ -64,6 +64,9 @@ def test_assemble_body(): c = list(assemble_body(Headers(transfer_encoding="chunked"), [b"123456789a"], Headers(trailer="trailer"))) assert c == [b"a\r\n123456789a\r\n", b"0\r\ntrailer: trailer\r\n\r\n"] + with pytest.raises(exceptions.HttpException): + list(assemble_body(Headers(), [b"body"], Headers(trailer="trailer"))) + def test_assemble_request_line(): assert _assemble_request_line(treq().data) == b"GET /path HTTP/1.1" diff --git a/test/mitmproxy/net/http/test_message.py b/test/mitmproxy/net/http/test_message.py index bd42c30c5..16c7ac30c 100644 --- a/test/mitmproxy/net/http/test_message.py +++ b/test/mitmproxy/net/http/test_message.py @@ -99,6 +99,9 @@ class TestMessage: def test_http_version(self): _test_decoded_attr(tutils.tresp(), "http_version") + assert tutils.tresp(http_version=b"HTTP/1.0").is_http10 + assert tutils.tresp(http_version=b"HTTP/1.1").is_http11 + assert tutils.tresp(http_version=b"HTTP/2.0").is_http2 class TestMessageContentEncoding: