From 27dbac2ac8ba804c5698ea3d019265bdfda33cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Sat, 27 Oct 2018 00:15:15 +0200 Subject: Handle HTTP/2 timeouts in the state machine --- src/cow_http2_machine.erl | 59 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/src/cow_http2_machine.erl b/src/cow_http2_machine.erl index b1cf1da..033919e 100644 --- a/src/cow_http2_machine.erl +++ b/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. -- cgit v1.2.3