From 8185d356c596e3dda9c87786516c6e924a56617a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Fri, 16 Nov 2018 16:30:57 +0100 Subject: Add the idle_timeout option to HTTP/2 --- doc/src/manual/cowboy_http2.asciidoc | 5 ++++ src/cowboy_http2.erl | 41 ++++++++++++++++++--------- test/http2_SUITE.erl | 54 ++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 13 deletions(-) diff --git a/doc/src/manual/cowboy_http2.asciidoc b/doc/src/manual/cowboy_http2.asciidoc index 8a79846..2b33a71 100644 --- a/doc/src/manual/cowboy_http2.asciidoc +++ b/doc/src/manual/cowboy_http2.asciidoc @@ -20,6 +20,7 @@ opts() :: #{ connection_type => worker | supervisor, enable_connect_protocol => boolean(), env => cowboy_middleware:env(), + idle_timeout => timeout(), inactivity_timeout => timeout(), initial_connection_window_size => 65535..16#7fffffff, initial_stream_window_size => 0..16#7fffffff, @@ -63,6 +64,10 @@ env (#{}):: Middleware environment. +idle_timeout (60000):: + +Time in ms with no data received before Cowboy closes the connection. + inactivity_timeout (300000):: Time in ms with nothing received at all before Cowboy closes the connection. diff --git a/src/cowboy_http2.erl b/src/cowboy_http2.erl index b83c021..5070bd4 100644 --- a/src/cowboy_http2.erl +++ b/src/cowboy_http2.erl @@ -32,6 +32,7 @@ connection_type => worker | supervisor, enable_connect_protocol => boolean(), env => cowboy_middleware:env(), + idle_timeout => timeout(), inactivity_timeout => timeout(), initial_connection_window_size => 65535..16#7fffffff, initial_stream_window_size => 0..16#7fffffff, @@ -64,6 +65,9 @@ proxy_header :: undefined | ranch_proxy_header:proxy_info(), opts = #{} :: opts(), + %% Timer for idle_timeout. + timer = undefined :: undefined | reference(), + %% Remote address and port for the connection. peer = undefined :: {inet:ip_address(), inet:port_number()}, @@ -122,13 +126,13 @@ init(Parent, Ref, Socket, Transport, ProxyHeader, Opts) -> binary() | undefined, binary()) -> ok. init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, Buffer) -> {ok, Preface, HTTP2Machine} = cow_http2_machine:init(server, Opts), - State = #state{parent=Parent, ref=Ref, socket=Socket, + State = set_timeout(#state{parent=Parent, ref=Ref, socket=Socket, transport=Transport, proxy_header=ProxyHeader, opts=Opts, peer=Peer, sock=Sock, cert=Cert, - http2_init=sequence, http2_machine=HTTP2Machine}, + http2_init=sequence, http2_machine=HTTP2Machine}), Transport:send(Socket, Preface), case Buffer of - <<>> -> before_loop(State, Buffer); + <<>> -> loop(State, Buffer); _ -> parse(State, Buffer) end. @@ -154,26 +158,23 @@ init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, Buffer <<"connection">> => <<"Upgrade">>, <<"upgrade">> => <<"h2c">> }, ?MODULE, undefined}), %% @todo undefined or #{}? - State = State2#state{http2_init=sequence}, + State = set_timeout(State2#state{http2_init=sequence}), Transport:send(Socket, Preface), case Buffer of - <<>> -> before_loop(State, Buffer); + <<>> -> loop(State, Buffer); _ -> parse(State, Buffer) end. -%% @todo Add the timeout for last time since we heard of connection. -before_loop(State, Buffer) -> - loop(State, Buffer). - loop(State=#state{parent=Parent, socket=Socket, transport=Transport, - opts=Opts, children=Children}, Buffer) -> + opts=Opts, timer=TimerRef, children=Children}, Buffer) -> + %% @todo This should only be called when data was read. Transport:setopts(Socket, [{active, once}]), {OK, Closed, Error} = Transport:messages(), InactivityTimeout = maps:get(inactivity_timeout, Opts, 300000), receive %% Socket messages. {OK, Socket, Data} -> - parse(State, << Buffer/binary, Data/binary >>); + parse(set_timeout(State), << Buffer/binary, Data/binary >>); {Closed, Socket} -> terminate(State, {socket_error, closed, 'The socket has been closed.'}); {Error, Socket, Reason} -> @@ -184,6 +185,9 @@ loop(State=#state{parent=Parent, socket=Socket, transport=Transport, {system, From, Request} -> sys:handle_system_msg(Request, From, Parent, ?MODULE, [], {State, Buffer}); %% Timeouts. + {timeout, TimerRef, idle_timeout} -> + terminate(State, {stop, timeout, + 'Connection idle longer than configuration allows.'}); {timeout, Ref, {shutdown, Pid}} -> cowboy_children:shutdown_timeout(Children, Ref, Pid), loop(State, Buffer); @@ -206,6 +210,17 @@ loop(State=#state{parent=Parent, socket=Socket, transport=Transport, terminate(State, {internal_error, timeout, 'No message or data received before timeout.'}) end. +set_timeout(State=#state{opts=Opts, timer=TimerRef0}) -> + ok = case TimerRef0 of + undefined -> ok; + _ -> erlang:cancel_timer(TimerRef0, [{async, true}, {info, false}]) + end, + TimerRef = case maps:get(idle_timeout, Opts, 60000) of + infinity -> undefined; + Timeout -> erlang:start_timer(Timeout, self(), idle_timeout) + end, + State#state{timer=TimerRef}. + %% HTTP/2 protocol parsing. parse(State=#state{http2_init=sequence}, Data) -> @@ -213,7 +228,7 @@ parse(State=#state{http2_init=sequence}, Data) -> {ok, Rest} -> parse(State#state{http2_init=settings}, Rest); more -> - before_loop(State, Data); + loop(State, Data); Error = {connection_error, _, _} -> terminate(State, Error) end; @@ -229,7 +244,7 @@ parse(State=#state{http2_machine=HTTP2Machine}, Data) -> Error = {connection_error, _, _} -> terminate(State, Error); more -> - before_loop(State, Data) + loop(State, Data) end. %% Frames received. diff --git a/test/http2_SUITE.erl b/test/http2_SUITE.erl index ba95173..1fd33f9 100644 --- a/test/http2_SUITE.erl +++ b/test/http2_SUITE.erl @@ -51,6 +51,60 @@ do_handshake(Settings, Config) -> {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, Socket}. +idle_timeout(Config) -> + doc("Terminate when the idle timeout is reached."), + ProtoOpts = #{ + env => #{dispatch => cowboy_router:compile(init_routes(Config))}, + idle_timeout => 1000 + }, + {ok, _} = cowboy:start_clear(name(), [{port, 0}], ProtoOpts), + Port = ranch:get_port(name()), + {ok, Socket} = do_handshake([{port, Port}|Config]), + timer:sleep(1000), + %% Receive a GOAWAY frame back with NO_ERROR. + {ok, << _:24, 7:8, _:72, 0:32 >>} = gen_tcp:recv(Socket, 17, 1000), + ok. + +idle_timeout_infinity(Config) -> + doc("Ensure the idle_timeout option accepts the infinity value."), + ProtoOpts = #{ + env => #{dispatch => cowboy_router:compile(init_routes(Config))}, + idle_timeout => infinity + }, + {ok, _} = cowboy:start_clear(name(), [{port, 0}], ProtoOpts), + Port = ranch:get_port(name()), + {ok, Socket} = do_handshake([{port, Port}|Config]), + timer:sleep(1000), + %% Don't receive a GOAWAY frame. + {error, timeout} = gen_tcp:recv(Socket, 17, 1000), + ok. + +idle_timeout_reset_on_data(Config) -> + doc("Terminate when the idle timeout is reached."), + ProtoOpts = #{ + env => #{dispatch => cowboy_router:compile(init_routes(Config))}, + idle_timeout => 1000 + }, + {ok, _} = cowboy:start_clear(name(), [{port, 0}], ProtoOpts), + Port = ranch:get_port(name()), + {ok, Socket} = do_handshake([{port, Port}|Config]), + %% We wait a little, send a PING, receive a PING ack. + {error, timeout} = gen_tcp:recv(Socket, 17, 500), + ok = gen_tcp:send(Socket, cow_http2:ping(0)), + {ok, <<8:24, 6:8, 0:7, 1:1, 0:96>>} = gen_tcp:recv(Socket, 17, 1000), + %% Again. + {error, timeout} = gen_tcp:recv(Socket, 17, 500), + ok = gen_tcp:send(Socket, cow_http2:ping(0)), + {ok, <<8:24, 6:8, 0:7, 1:1, 0:96>>} = gen_tcp:recv(Socket, 17, 1000), + %% And one more time. + {error, timeout} = gen_tcp:recv(Socket, 17, 500), + ok = gen_tcp:send(Socket, cow_http2:ping(0)), + {ok, <<8:24, 6:8, 0:7, 1:1, 0:96>>} = gen_tcp:recv(Socket, 17, 1000), + %% The connection goes away soon after we stop sending data. + timer:sleep(1000), + {ok, << _:24, 7:8, _:72, 0:32 >>} = gen_tcp:recv(Socket, 17, 1000), + ok. + inactivity_timeout(Config) -> doc("Terminate when the inactivity timeout is reached."), ProtoOpts = #{ -- cgit v1.2.3