From 1bdc4fdb8f8634075944671ae00d14dadeba89df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Thu, 27 Feb 2020 12:19:07 +0100 Subject: Detect invalid HTTP/2 preface errors And make sure all HTTP/2 connection_error(s) result in a gun_down message containing the error. In the preface case we do not send a gun_error message (because there's no stream open yet) and gun_down was always saying normal. Also make sure the human readable reason is included in the gun_error message, if any. --- src/gun_http2.erl | 32 ++++++++++++++++++----- test/rfc7540_SUITE.erl | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/src/gun_http2.erl b/src/gun_http2.erl index b0397f0..3587466 100644 --- a/src/gun_http2.erl +++ b/src/gun_http2.erl @@ -58,8 +58,9 @@ buffer = <<>> :: binary(), %% Current status of the connection. We use this to ensure we are - %% not sending the GOAWAY frame more than once. - status = connected :: connected | goaway | closing, + %% not sending the GOAWAY frame more than once, and to validate + %% the server connection preface. + status = preface :: preface | connected | goaway | closing, %% HTTP/2 state machine. http2_machine :: cow_http2_machine:http2_machine(), @@ -147,6 +148,24 @@ handle(Data, State=#http2_state{buffer=Buffer}, EvHandler, EvHandlerState) -> parse(<< Buffer/binary, Data/binary >>, State#http2_state{buffer= <<>>}, EvHandler, EvHandlerState). +parse(Data, State0=#http2_state{status=preface, http2_machine=HTTP2Machine}, + EvHandler, EvHandlerState0) -> + MaxFrameSize = cow_http2_machine:get_local_setting(max_frame_size, HTTP2Machine), + case cow_http2:parse(Data, MaxFrameSize) of + {ok, Frame, Rest} when element(1, Frame) =:= settings -> + case frame(State0#http2_state{status=connected}, Frame, EvHandler, EvHandlerState0) of + Close = {close, _} -> Close; + Error = {{error, _}, _} -> Error; + {State, EvHandlerState} -> parse(Rest, State, EvHandler, EvHandlerState) + end; + more -> + {{state, State0#http2_state{buffer=Data}}, EvHandlerState0}; + %% Any error in the preface is converted to this specific error + %% to make debugging the problem easier (it's the server's fault). + _ -> + {connection_error(State0, {connection_error, protocol_error, + 'Invalid connection preface received. (RFC7540 3.5)'}), EvHandlerState0} + end; parse(Data, State0=#http2_state{status=Status, http2_machine=HTTP2Machine, streams=Streams}, EvHandler, EvHandlerState0) -> MaxFrameSize = cow_http2_machine:get_local_setting(max_frame_size, HTTP2Machine), @@ -154,11 +173,12 @@ parse(Data, State0=#http2_state{status=Status, http2_machine=HTTP2Machine, strea {ok, Frame, Rest} -> case frame(State0, Frame, EvHandler, EvHandlerState0) of Close = {close, _} -> Close; + Error = {{error, _}, _} -> Error; {State, EvHandlerState} -> parse(Rest, State, EvHandler, EvHandlerState) end; {ignore, Rest} -> case ignored_frame(State0) of - close -> {close, EvHandlerState0}; + Error = {error, _} -> {Error, EvHandlerState0}; State -> parse(Rest, State, EvHandler, EvHandlerState0) end; {stream_error, StreamID, Reason, Human, Rest} -> @@ -735,15 +755,15 @@ down(#http2_state{stream_refs=Refs}) -> connection_error(#http2_state{socket=Socket, transport=Transport, http2_machine=HTTP2Machine, streams=Streams}, - {connection_error, Reason, _}) -> + Error={connection_error, Reason, HumanReadable}) -> Pids = lists:usort(maps:fold( fun(_, #stream{reply_to=ReplyTo}, Acc) -> [ReplyTo|Acc] end, [], Streams)), - _ = [Pid ! {gun_error, self(), Reason} || Pid <- Pids], + _ = [Pid ! {gun_error, self(), {Reason, HumanReadable}} || Pid <- Pids], Transport:send(Socket, cow_http2:goaway( cow_http2_machine:get_last_streamid(HTTP2Machine), Reason, <<>>)), - close. + {error, Error}. %% Stream functions. diff --git a/test/rfc7540_SUITE.erl b/test/rfc7540_SUITE.erl index f19ce34..780f850 100644 --- a/test/rfc7540_SUITE.erl +++ b/test/rfc7540_SUITE.erl @@ -70,6 +70,77 @@ do_authority_port(Transport0, DefaultPort, AuthorityHeaderPort) -> AuthorityHeaderPort = Rest, gun:close(ConnPid). +prior_knowledge_preface_garbage(_) -> + doc("A PROTOCOL_ERROR connection error must result from the server sending " + "an invalid preface in the form of garbage when connecting " + "using the prior knowledge method. (RFC7540 3.4, RFC7540 3.5)"), + %% We use 'http' here because we are going to do the handshake manually. + {ok, OriginPid, Port} = init_origin(tcp, http, fun(_, Socket, Transport) -> + ok = Transport:send(Socket, <<0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15>>), + timer:sleep(100) + end), + {ok, ConnPid} = gun:open("localhost", Port, #{protocols => [http2]}), + {ok, http2} = gun:await_up(ConnPid), + handshake_completed = receive_from(OriginPid), + receive + {gun_down, ConnPid, http2, {error, {connection_error, protocol_error, + 'Invalid connection preface received. (RFC7540 3.5)'}}, []} -> + gun:close(ConnPid); + Msg -> + error({unexpected_msg, Msg}) + after 1000 -> + error(timeout) + end. + +prior_knowledge_preface_http1(_) -> + doc("A PROTOCOL_ERROR connection error must result from the server sending " + "an invalid preface in the form of an HTTP/1.1 response when connecting " + "using the prior knowledge method. (RFC7540 3.4, RFC7540 3.5)"), + %% We use 'http' here because we are going to do the handshake manually. + {ok, OriginPid, Port} = init_origin(tcp, http, fun(_, Socket, Transport) -> + ok = Transport:send(Socket, << + "HTTP/1.1 400 Bad Request\r\n" + "Connection: close\r\n" + "Content-Length: 0\r\n" + "Date: Thu, 27 Feb 2020 09:32:17 GMT\r\n" + "\r\n">>), + timer:sleep(100) + end), + {ok, ConnPid} = gun:open("localhost", Port, #{protocols => [http2]}), + {ok, http2} = gun:await_up(ConnPid), + handshake_completed = receive_from(OriginPid), + receive + {gun_down, ConnPid, http2, {error, {connection_error, protocol_error, + 'Invalid connection preface received. (RFC7540 3.5)'}}, []} -> + gun:close(ConnPid); + Msg -> + error({unexpected_msg, Msg}) + after 1000 -> + error(timeout) + end. + +prior_knowledge_preface_other_frame(_) -> + doc("A PROTOCOL_ERROR connection error must result from the server sending " + "an invalid preface in the form of a non-SETTINGS frame when connecting " + "using the prior knowledge method. (RFC7540 3.4, RFC7540 3.5)"), + %% We use 'http' here because we are going to do the handshake manually. + {ok, OriginPid, Port} = init_origin(tcp, http, fun(_, Socket, Transport) -> + ok = Transport:send(Socket, cow_http2:window_update(1)), + timer:sleep(100) + end), + {ok, ConnPid} = gun:open("localhost", Port, #{protocols => [http2]}), + {ok, http2} = gun:await_up(ConnPid), + handshake_completed = receive_from(OriginPid), + receive + {gun_down, ConnPid, http2, {error, {connection_error, protocol_error, + 'Invalid connection preface received. (RFC7540 3.5)'}}, []} -> + gun:close(ConnPid); + Msg -> + error({unexpected_msg, Msg}) + after 1000 -> + error(timeout) + end. + lingering_data_counts_toward_connection_window(_) -> doc("DATA frames received after sending RST_STREAM must be counted " "toward the connection flow-control window. (RFC7540 5.1)"), -- cgit v1.2.3