Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 31 additions & 9 deletions lib/mint/http2.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2080,6 +2080,13 @@ defmodule Mint.HTTP2 do
# https://httpwg.org/specs/rfc9113.html#HttpFraming
[{":status", <<?1, _, _>> = status} | headers] ->
cond do
# RFC 9113 8.6: HTTP/2 does not support the 101 status code.
status == "101" ->
conn = close_stream!(conn, stream.id, :protocol_error)
debug_data = "the 101 (Switching Protocols) status code is not supported in HTTP/2"
error = wrap_error({:protocol_error, debug_data})
{conn, [{:error, stream.ref, error} | responses]}

end_stream? ->
conn = close_stream!(conn, stream.id, :protocol_error)
debug_data = "informational response (1xx) must not have the END_STREAM flag set"
Expand Down Expand Up @@ -2217,14 +2224,32 @@ defmodule Mint.HTTP2 do
cond do
not valid_field_name?(name) -> {:error, {:invalid_header_name, name}}
not valid_field_value?(value) -> {:error, {:invalid_header_value, name, value}}
connection_specific?(name) -> {:error, connection_specific_error(name)}
true -> validate_response_headers(rest, trailers?, status?, true)
end
end

# RFC 9110 15: status-code = 3DIGIT
defp valid_status?(<<a, b, c>>) when a in ?0..?9 and b in ?0..?9 and c in ?0..?9, do: true
# RFC 9110 15: status-code = 3DIGIT, with values in the range 100-999.
defp valid_status?(<<a, b, c>>) when a in ?1..?9 and b in ?0..?9 and c in ?0..?9, do: true
defp valid_status?(_other), do: false

# RFC 9113 8.2.2: a message with connection-specific header fields is malformed.
# "te" is only allowed in requests, with the "trailers" value.
@connection_specific_headers [
"connection",
"keep-alive",
"proxy-connection",
"te",
"transfer-encoding",
"upgrade"
]

defp connection_specific?(name), do: name in @connection_specific_headers

defp connection_specific_error(name) do
{:protocol_error, "connection-specific header #{inspect(name)} is not allowed in HTTP/2"}
end

# RFC 9113 8.2.1: a field name must not contain characters in 0x00-0x20, 0x41-0x5A
# (uppercase letters) or 0x7F-0xFF, and only a pseudo-header field can contain a colon.
defp valid_field_name?(<<>>), do: false
Expand Down Expand Up @@ -2333,14 +2358,11 @@ defmodule Mint.HTTP2 do
# If we receive RST_STREAM then the stream is definitely closed.
# We won't send anything else on the stream so we can simply delete
# it, so that if we get things like DATA on that stream we error out.
# Streams are removed as soon as the server ends them, so a RST_STREAM on a
# stream we still track means the response is incomplete, whatever the code.
conn = delete_stream(conn, stream)

if error_code == :no_error do
{conn, [{:done, stream.ref} | responses]}
else
error = wrap_error({:server_closed_request, error_code})
{conn, [{:error, stream.ref, error} | responses]}
end
error = wrap_error({:server_closed_request, error_code})
{conn, [{:error, stream.ref, error} | responses]}

:error ->
{conn, responses}
Expand Down
117 changes: 115 additions & 2 deletions test/mint/http2/conn_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,36 @@ defmodule Mint.HTTP2Test do
end

describe "closed streams" do
for phase <- [:before_the_headers, :during_the_body] do
test "RST_STREAM with NO_ERROR #{phase} is an error", %{conn: conn} do
{conn, ref} = open_request(conn)

assert_recv_frames [headers(stream_id: stream_id)]

conn =
if unquote(phase) == :during_the_body do
assert {:ok, %HTTP2{} = conn,
[{:status, ^ref, 200}, {:headers, ^ref, _}, {:data, ^ref, "x"}]} =
stream_frames(conn, [
{:headers, stream_id, [{":status", "200"}, {"content-length", "5"}],
[:end_headers]},
data(stream_id: stream_id, data: "x")
])

conn
else
conn
end

assert {:ok, %HTTP2{} = conn, [{:error, ^ref, error}]} =
stream_frames(conn, [rst_stream(stream_id: stream_id, error_code: :no_error)])

assert_http2_error error, {:server_closed_request, :no_error}
refute Map.has_key?(conn.streams, stream_id)
assert HTTP2.open?(conn)
end
end

test "server closes a stream with RST_STREAM", %{conn: conn} do
{conn, ref} = open_request(conn)

Expand Down Expand Up @@ -1231,7 +1261,7 @@ defmodule Mint.HTTP2Test do
end

describe "response header validation" do
for status <- ["abc", "", "+200", "2000", "20", "200 ", " 200", "1ab"] do
for status <- ["abc", "", "+200", "2000", "20", "200 ", " 200", "1ab", "000", "099"] do
test "an invalid :status of #{inspect(status)} is a stream error", %{conn: conn} do
{conn, ref} = open_request(conn)

Expand All @@ -1250,6 +1280,89 @@ defmodule Mint.HTTP2Test do
end
end

for {name, value} <- [
{"connection", "close"},
{"keep-alive", "timeout=5"},
{"proxy-connection", "keep-alive"},
{"transfer-encoding", "chunked"},
{"upgrade", "websocket"},
{"te", "gzip"},
{"te", "trailers"}
] do
test "the connection-specific header #{name}: #{value} is a stream error", %{conn: conn} do
{conn, ref} = open_request(conn)

assert_recv_frames [headers(stream_id: stream_id)]

assert {:ok, %HTTP2{} = conn, responses} =
stream_frames(conn, [
{:headers, stream_id, [{":status", "200"}, {unquote(name), unquote(value)}],
[:end_headers]}
])

assert [{:error, ^ref, error}] = responses
assert_http2_error error, {:protocol_error, debug_data}
assert debug_data =~ "connection-specific header #{inspect(unquote(name))}"

assert_recv_frames [rst_stream(stream_id: ^stream_id, error_code: :protocol_error)]
assert HTTP2.open?(conn)
end
end

test "a connection-specific header in an informational response is a stream error",
%{conn: conn} do
{conn, ref} = open_request(conn)

assert_recv_frames [headers(stream_id: stream_id)]

assert {:ok, %HTTP2{} = conn, responses} =
stream_frames(conn, [
{:headers, stream_id, [{":status", "103"}, {"te", "trailers"}], [:end_headers]}
])

assert [{:error, ^ref, error}] = responses
assert_http2_error error, {:protocol_error, debug_data}
assert debug_data =~ "connection-specific header \"te\""

assert_recv_frames [rst_stream(stream_id: ^stream_id, error_code: :protocol_error)]
assert HTTP2.open?(conn)
end

test "a connection-specific header in trailers is a stream error", %{conn: conn} do
{conn, ref} = open_request(conn)

assert_recv_frames [headers(stream_id: stream_id)]

assert {:ok, %HTTP2{} = conn, responses} =
stream_frames(conn, [
{:headers, stream_id, [{":status", "200"}], [:end_headers]},
{:headers, stream_id, [{"te", "trailers"}], [:end_headers, :end_stream]}
])

assert [{:status, ^ref, 200}, {:headers, ^ref, []}, {:error, ^ref, error}] = responses
assert_http2_error error, {:protocol_error, debug_data}
assert debug_data =~ "connection-specific header \"te\""

assert_recv_frames [rst_stream(stream_id: ^stream_id, error_code: :protocol_error)]
assert HTTP2.open?(conn)
end

test "a 101 status is a stream error", %{conn: conn} do
{conn, ref} = open_request(conn)

assert_recv_frames [headers(stream_id: stream_id)]

assert {:ok, %HTTP2{} = conn, responses} =
stream_frames(conn, [{:headers, stream_id, [{":status", "101"}], [:end_headers]}])

assert [{:error, ^ref, error}] = responses
assert_http2_error error, {:protocol_error, debug_data}
assert debug_data =~ "the 101 (Switching Protocols) status code is not supported in HTTP/2"

assert_recv_frames [rst_stream(stream_id: ^stream_id, error_code: :protocol_error)]
assert HTTP2.open?(conn)
end

for name <- ["Foo", "fo o", "", "foo:bar", "f\x7Fo", "f\xC3\xA4"] do
test "an invalid header name #{inspect(name)} is a stream error", %{conn: conn} do
{conn, ref} = open_request(conn)
Expand Down Expand Up @@ -1694,7 +1807,7 @@ defmodule Mint.HTTP2Test do

info_hbf =
server_encode_headers([
{":status", "101"},
{":status", "102"},
{"x-info-header1", "this is an info"}
])

Expand Down
Loading