Browse Source

Handle HTTP/2 timeouts in the state machine

Loïc Hoguin 6 years ago
parent
commit
27dbac2ac8
1 changed files with 53 additions and 6 deletions
  1. 53 6
      src/cow_http2_machine.erl

+ 53 - 6
src/cow_http2_machine.erl

@@ -18,6 +18,7 @@
 -export([init_upgrade_stream/2]).
 -export([frame/2]).
 -export([ignored_frame/1]).
+-export([timeout/3]).
 -export([prepare_headers/5]).
 -export([prepare_push_promise/4]).
 -export([prepare_trailers/3]).
@@ -37,7 +38,9 @@
 	max_decode_table_size => non_neg_integer(),
 	max_encode_table_size => non_neg_integer(),
 	max_frame_size_received => 16384..16777215,
-	max_frame_size_sent => 16384..16777215 | infinity
+	max_frame_size_sent => 16384..16777215 | infinity,
+	preface_timeout => timeout(),
+	settings_timeout => timeout()
 }.
 -export_type([opts/0]).
 
@@ -97,6 +100,12 @@
 	state = settings :: settings | normal
 		| {continuation, request | response | trailers | push_promise, continued_frame()},
 
+	%% Timer for the connection preface.
+	preface_timer = undefined :: undefined | reference(),
+
+	%% Timer for the ack for a SETTINGS frame we sent.
+	settings_timer = undefined :: undefined | reference(),
+
 	%% Settings are separate for each endpoint. In addition, settings
 	%% must be acknowledged before they can be expected to be applied.
 	local_settings = #{
@@ -171,6 +180,8 @@ init(client, Opts) ->
 	client_preface(#http2_machine{
 		mode=client,
 		opts=only_keep_relevant_opts(Opts),
+		preface_timer=start_timer(preface_timeout, Opts),
+		settings_timer=start_timer(settings_timeout, Opts),
 		next_settings=NextSettings,
 		local_streamid=1
 	});
@@ -179,6 +190,8 @@ init(server, Opts) ->
 	common_preface(#http2_machine{
 		mode=server,
 		opts=only_keep_relevant_opts(Opts),
+		preface_timer=start_timer(preface_timeout, Opts),
+		settings_timer=start_timer(settings_timeout, Opts),
 		next_settings=NextSettings,
 		local_streamid=2
 	}).
@@ -188,9 +201,16 @@ only_keep_relevant_opts(Opts) ->
 	maps:with([
 		initial_connection_window_size,
 		max_encode_table_size,
-		max_frame_size_sent
+		max_frame_size_sent,
+		settings_timeout
 	], Opts).
 
+start_timer(Name, Opts) ->
+	case maps:get(Name, Opts, 5000) of
+		infinity -> undefined;
+		Timeout -> erlang:start_timer(Timeout, self(), {?MODULE, Name})
+	end.
+
 client_preface(State0) ->
 	{ok, CommonPreface, State} = common_preface(State0),
 	{ok, [
@@ -257,8 +277,12 @@ init_upgrade_stream(Method, State=#http2_machine{mode=server, remote_streamid=0,
 	| {error, {stream_error, cow_http2:streamid(), cow_http2:error(), atom()}, State}
 	| {error, {connection_error, cow_http2:error(), atom()}, State}
 	when State::http2_machine().
-frame(Frame, State=#http2_machine{state=settings}) ->
-	settings_frame(Frame, State#http2_machine{state=normal});
+frame(Frame, State=#http2_machine{state=settings, preface_timer=TRef}) ->
+	ok = case TRef of
+		undefined -> ok;
+		_ -> erlang:cancel_timer(TRef, [{async, true}, {info, false}])
+	end,
+	settings_frame(Frame, State#http2_machine{state=normal, preface_timer=undefined});
 frame(Frame, State=#http2_machine{state={continuation, _, _}}) ->
 	continuation_frame(Frame, State);
 frame(settings_ack, State=#http2_machine{state=normal}) ->
@@ -803,9 +827,15 @@ streams_update_local_window(State=#http2_machine{streams=Streams0}, Increment) -
 
 %% Ack for a previously sent SETTINGS frame.
 
-settings_ack_frame(State0=#http2_machine{local_settings=Local0, next_settings=NextSettings}) ->
+settings_ack_frame(State0=#http2_machine{settings_timer=TRef,
+		local_settings=Local0, next_settings=NextSettings}) ->
+	ok = case TRef of
+		undefined -> ok;
+		_ -> erlang:cancel_timer(TRef, [{async, true}, {info, false}])
+	end,
 	Local = maps:merge(Local0, NextSettings),
-	State1 = State0#http2_machine{local_settings=Local, next_settings=#{}},
+	State1 = State0#http2_machine{settings_timer=undefined,
+		local_settings=Local, next_settings=#{}},
 	{ok, maps:fold(fun
 		(header_table_size, MaxSize, State=#http2_machine{decode_state=DecodeState0}) ->
 			DecodeState = cow_hpack:set_max_size(MaxSize, DecodeState0),
@@ -992,6 +1022,23 @@ ignored_frame(State=#http2_machine{state={continuation, _, _}}) ->
 ignored_frame(State) ->
 	{ok, State}.
 
+%% Timeouts.
+
+-spec timeout(preface_timeout | settings_timeout, reference(), State)
+	-> {ok, State}
+	| {error, {connection_error, cow_http2:error(), atom()}, State}
+	when State::http2_machine().
+timeout(preface_timeout, TRef, State=#http2_machine{preface_timer=TRef}) ->
+	{error, {connection_error, protocol_error,
+		'The preface was not received in a reasonable amount of time.'},
+		State};
+timeout(settings_timeout, TRef, State=#http2_machine{settings_timer=TRef}) ->
+	{error, {connection_error, settings_timeout,
+		'The SETTINGS ack was not received within the configured time. (RFC7540 6.5.3)'},
+		State};
+timeout(_, _, State) ->
+	{ok, State}.
+
 %% Functions for sending a message header or body. Note that
 %% this module does not send data directly, instead it returns
 %% a value that can then be used to send the frames.