mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-22 15:37:45 +00:00
HTTP/2: Show human-readable error messages (#4462)
This commit is contained in:
parent
001cf6c10a
commit
593dd93cf8
@ -36,6 +36,7 @@ UNSUPPORTED_MEDIA_TYPE = 415
|
||||
REQUESTED_RANGE_NOT_SATISFIABLE = 416
|
||||
EXPECTATION_FAILED = 417
|
||||
IM_A_TEAPOT = 418
|
||||
NO_RESPONSE = 444
|
||||
CLIENT_CLOSED_REQUEST = 499
|
||||
|
||||
INTERNAL_SERVER_ERROR = 500
|
||||
|
@ -2,18 +2,18 @@ import collections
|
||||
import enum
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Tuple, Union, Dict, DefaultDict, List
|
||||
from typing import DefaultDict, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from mitmproxy import flow, http
|
||||
from mitmproxy.connection import Connection, ConnectionState, Server
|
||||
from mitmproxy.connection import Connection, Server
|
||||
from mitmproxy.net import server_spec
|
||||
from mitmproxy.net.http import url
|
||||
from mitmproxy.net.http import status_codes, url
|
||||
from mitmproxy.proxy import commands, events, layer, tunnel
|
||||
from mitmproxy.proxy.layers import tls, websocket, tcp
|
||||
from mitmproxy.proxy.layers import tcp, tls, websocket
|
||||
from mitmproxy.proxy.layers.http import _upstream_proxy
|
||||
from mitmproxy.proxy.utils import expect
|
||||
from mitmproxy.utils import human
|
||||
from ._base import HttpCommand, ReceiveHttp, StreamId, HttpConnection
|
||||
from ._base import HttpCommand, HttpConnection, ReceiveHttp, StreamId
|
||||
from ._events import HttpEvent, RequestData, RequestEndOfMessage, RequestHeaders, RequestProtocolError, ResponseData, \
|
||||
ResponseEndOfMessage, ResponseHeaders, ResponseProtocolError
|
||||
from ._hooks import HttpConnectHook, HttpErrorHook, HttpRequestHeadersHook, HttpRequestHook, HttpResponseHeadersHook, \
|
||||
@ -354,13 +354,11 @@ class HttpStream(layer.Layer):
|
||||
if killed_by_us or killed_by_remote:
|
||||
if emit_error_hook:
|
||||
yield HttpErrorHook(self.flow)
|
||||
# For HTTP/2 we only want to kill the specific stream, for HTTP/1 we want to kill the connection
|
||||
# *without* sending an HTTP response (that could be achieved by the user by setting flow.response).
|
||||
if self.context.client.alpn == b"h2":
|
||||
yield SendHttp(ResponseProtocolError(self.stream_id, "killed"), self.context.client)
|
||||
else:
|
||||
if self.context.client.state & ConnectionState.CAN_WRITE:
|
||||
yield commands.CloseConnection(self.context.client)
|
||||
# Use the special NO_RESPONSE status code to make sure that no error message is sent to the client.
|
||||
yield SendHttp(
|
||||
ResponseProtocolError(self.stream_id, "killed", status_codes.NO_RESPONSE),
|
||||
self.context.client
|
||||
)
|
||||
self._handle_event = self.state_errored
|
||||
return True
|
||||
return False
|
||||
|
@ -1,7 +1,10 @@
|
||||
import html
|
||||
import textwrap
|
||||
from dataclasses import dataclass
|
||||
|
||||
from mitmproxy.proxy import events, layer, commands
|
||||
from mitmproxy import http
|
||||
from mitmproxy.connection import Connection
|
||||
from mitmproxy.proxy import commands, events, layer
|
||||
from mitmproxy.proxy.context import Context
|
||||
|
||||
StreamId = int
|
||||
@ -35,8 +38,16 @@ class ReceiveHttp(HttpCommand):
|
||||
return f"Receive({self.event})"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"HttpConnection",
|
||||
"StreamId",
|
||||
"HttpEvent",
|
||||
]
|
||||
def format_error(status_code: int, message: str) -> bytes:
|
||||
reason = http.status_codes.RESPONSES.get(status_code, "Unknown")
|
||||
return textwrap.dedent(f"""
|
||||
<html>
|
||||
<head>
|
||||
<title>{status_code} {reason}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{status_code} {reason}</h1>
|
||||
<p>{html.escape(message)}</p>
|
||||
</body>
|
||||
</html>
|
||||
""").strip().encode("utf8", "replace")
|
||||
|
@ -1,19 +1,18 @@
|
||||
import abc
|
||||
import html
|
||||
from typing import Union, Optional, Callable, Type
|
||||
from typing import Callable, Optional, Type, Union
|
||||
|
||||
import h11
|
||||
from h11._readers import ChunkedReader, ContentLengthReader, Http10Reader
|
||||
from h11._receivebuffer import ReceiveBuffer
|
||||
|
||||
from mitmproxy import http, version
|
||||
from mitmproxy.connection import Connection, ConnectionState
|
||||
from mitmproxy.net.http import http1, status_codes
|
||||
from mitmproxy.proxy import commands, events, layer
|
||||
from mitmproxy.connection import Connection, ConnectionState
|
||||
from mitmproxy.proxy.layers.http._base import ReceiveHttp, StreamId
|
||||
from mitmproxy.proxy.utils import expect
|
||||
from mitmproxy.utils import human
|
||||
from ._base import HttpConnection
|
||||
from ._base import HttpConnection, format_error
|
||||
from ._events import HttpEvent, RequestData, RequestEndOfMessage, RequestHeaders, RequestProtocolError, ResponseData, \
|
||||
ResponseEndOfMessage, ResponseHeaders, ResponseProtocolError
|
||||
from ...context import Context
|
||||
@ -212,11 +211,20 @@ class Http1Server(Http1Connection):
|
||||
yield commands.SendData(self.conn, b"0\r\n\r\n")
|
||||
yield from self.mark_done(response=True)
|
||||
elif isinstance(event, ResponseProtocolError):
|
||||
if not self.response:
|
||||
resp = make_error_response(event.code, event.message)
|
||||
if not self.response and event.code != status_codes.NO_RESPONSE:
|
||||
resp = http.Response.make(
|
||||
event.code,
|
||||
format_error(event.code, event.message),
|
||||
http.Headers(
|
||||
Server=version.MITMPROXY,
|
||||
Connection="close",
|
||||
Content_Type="text/html",
|
||||
)
|
||||
)
|
||||
raw = http1.assemble_response(resp)
|
||||
yield commands.SendData(self.conn, raw)
|
||||
yield commands.CloseConnection(self.conn)
|
||||
if self.conn.state & ConnectionState.CAN_WRITE:
|
||||
yield commands.CloseConnection(self.conn)
|
||||
else:
|
||||
raise AssertionError(f"Unexpected event: {event}")
|
||||
|
||||
@ -368,25 +376,9 @@ def make_error_response(
|
||||
status_code: int,
|
||||
message: str = "",
|
||||
) -> http.Response:
|
||||
body: bytes = """
|
||||
<html>
|
||||
<head>
|
||||
<title>{status_code} {reason}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{status_code} {reason}</h1>
|
||||
<p>{message}</p>
|
||||
</body>
|
||||
</html>
|
||||
""".strip().format(
|
||||
status_code=status_code,
|
||||
reason=http.status_codes.RESPONSES.get(status_code, "Unknown"),
|
||||
message=html.escape(message),
|
||||
).encode("utf8", "replace")
|
||||
|
||||
return http.Response.make(
|
||||
status_code,
|
||||
body,
|
||||
format_error(status_code, message),
|
||||
http.Headers(
|
||||
Server=version.MITMPROXY,
|
||||
Connection="close",
|
||||
|
@ -1,7 +1,7 @@
|
||||
import collections
|
||||
import time
|
||||
from enum import Enum
|
||||
from typing import ClassVar, DefaultDict, Dict, List, Optional, Tuple, Type, Union, Sequence
|
||||
from typing import ClassVar, DefaultDict, Dict, List, Optional, Sequence, Tuple, Type, Union
|
||||
|
||||
import h2.config
|
||||
import h2.connection
|
||||
@ -12,13 +12,13 @@ import h2.settings
|
||||
import h2.stream
|
||||
import h2.utilities
|
||||
|
||||
from mitmproxy import http
|
||||
from mitmproxy import http, version
|
||||
from mitmproxy.connection import Connection
|
||||
from mitmproxy.net.http import url, status_codes
|
||||
from mitmproxy.net.http import status_codes, url
|
||||
from mitmproxy.utils import human
|
||||
from . import RequestData, RequestEndOfMessage, RequestHeaders, RequestProtocolError, ResponseData, \
|
||||
ResponseEndOfMessage, ResponseHeaders, ResponseProtocolError
|
||||
from ._base import HttpConnection, HttpEvent, ReceiveHttp
|
||||
from ._base import HttpConnection, HttpEvent, ReceiveHttp, format_error
|
||||
from ._http_h2 import BufferedH2Connection, H2ConnectionLogger
|
||||
from ...commands import CloseConnection, Log, SendData
|
||||
from ...context import Context
|
||||
@ -106,7 +106,25 @@ class Http2Connection(HttpConnection):
|
||||
code = {
|
||||
status_codes.CLIENT_CLOSED_REQUEST: h2.errors.ErrorCodes.CANCEL,
|
||||
}.get(event.code, h2.errors.ErrorCodes.INTERNAL_ERROR)
|
||||
self.h2_conn.reset_stream(event.stream_id, code)
|
||||
stream: h2.stream.H2Stream = self.h2_conn.streams[event.stream_id]
|
||||
send_error_message = (
|
||||
isinstance(event, ResponseProtocolError)
|
||||
and not stream.state_machine.headers_sent
|
||||
and event.code != status_codes.NO_RESPONSE
|
||||
)
|
||||
if send_error_message:
|
||||
self.h2_conn.send_headers(event.stream_id, [
|
||||
(b":status", b"%d" % event.code),
|
||||
(b"server", version.MITMPROXY.encode()),
|
||||
(b"content-type", b"text/html"),
|
||||
])
|
||||
self.h2_conn.send_data(
|
||||
event.stream_id,
|
||||
format_error(event.code, event.message),
|
||||
end_stream=True
|
||||
)
|
||||
else:
|
||||
self.h2_conn.reset_stream(event.stream_id, code)
|
||||
else:
|
||||
raise AssertionError(f"Unexpected event: {event}")
|
||||
data_to_send = self.h2_conn.data_to_send()
|
||||
|
@ -5,7 +5,6 @@ from mitmproxy.proxy.commands import SendData
|
||||
from mitmproxy.proxy.events import DataReceived
|
||||
from mitmproxy.proxy.layers.http import Http1Server, ReceiveHttp, RequestHeaders, RequestEndOfMessage, \
|
||||
ResponseHeaders, ResponseEndOfMessage, RequestData, Http1Client, ResponseData
|
||||
from mitmproxy.proxy.layers.http._http1 import make_error_response
|
||||
from test.mitmproxy.proxy.tutils import Placeholder, Playbook
|
||||
|
||||
|
||||
@ -200,7 +199,3 @@ class TestClient:
|
||||
>> RequestHeaders(3, req, True)
|
||||
<< SendData(tctx.server, Placeholder(bytes))
|
||||
)
|
||||
|
||||
|
||||
def test_make_error_response():
|
||||
assert make_error_response(543, 'foobar')
|
||||
|
@ -103,6 +103,36 @@ def test_simple(tctx):
|
||||
assert flow().response.text == "Hello, World!"
|
||||
|
||||
|
||||
def test_upstream_error(tctx):
|
||||
playbook, cff = start_h2_client(tctx)
|
||||
flow = Placeholder(HTTPFlow)
|
||||
server = Placeholder(Server)
|
||||
err = Placeholder(bytes)
|
||||
assert (
|
||||
playbook
|
||||
>> DataReceived(tctx.client,
|
||||
cff.build_headers_frame(example_request_headers, flags=["END_STREAM"]).serialize())
|
||||
<< http.HttpRequestHeadersHook(flow)
|
||||
>> reply()
|
||||
<< http.HttpRequestHook(flow)
|
||||
>> reply()
|
||||
<< OpenConnection(server)
|
||||
>> reply("oops server <> error")
|
||||
<< http.HttpErrorHook(flow)
|
||||
>> reply()
|
||||
<< SendData(tctx.client, err)
|
||||
)
|
||||
frames = decode_frames(err())
|
||||
assert [type(x) for x in frames] == [
|
||||
hyperframe.frame.HeadersFrame,
|
||||
hyperframe.frame.DataFrame,
|
||||
]
|
||||
d = frames[1]
|
||||
assert isinstance(d, hyperframe.frame.DataFrame)
|
||||
assert b"502 Bad Gateway" in d.data
|
||||
assert b"server <> error" in d.data
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stream", ["stream", ""])
|
||||
@pytest.mark.parametrize("when", ["request", "response"])
|
||||
@pytest.mark.parametrize("how", ["RST", "disconnect", "RST+disconnect"])
|
||||
|
Loading…
Reference in New Issue
Block a user