aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2020-02-27 12:19:07 +0100
committerLoïc Hoguin <[email protected]>2020-02-27 12:38:39 +0100
commit1bdc4fdb8f8634075944671ae00d14dadeba89df (patch)
treea8982ccb3d59a3d30650e60473199e97ae314ec3
parent1b7b5ca356609574dac30dbec846dbd75718624e (diff)
downloadgun-1bdc4fdb8f8634075944671ae00d14dadeba89df.tar.gz
gun-1bdc4fdb8f8634075944671ae00d14dadeba89df.tar.bz2
gun-1bdc4fdb8f8634075944671ae00d14dadeba89df.zip
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.
-rw-r--r--src/gun_http2.erl32
-rw-r--r--test/rfc7540_SUITE.erl71
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)"),