Browse Source

Data received after RST_STREAM counts toward window

Tony Han 6 years ago
parent
commit
7708fc77cd
3 changed files with 79 additions and 0 deletions
  1. 8 0
      src/cowboy_http2.erl
  2. 21 0
      test/handlers/loop_handler_abort_h.erl
  3. 50 0
      test/rfc7540_SUITE.erl

+ 8 - 0
src/cowboy_http2.erl

@@ -255,6 +255,8 @@ frame(State=#state{http2_machine=HTTP2Machine0}, Frame) ->
 			maybe_ack(State#state{http2_machine=HTTP2Machine}, Frame);
 		{ok, {data, StreamID, IsFin, Data}, HTTP2Machine} ->
 			data_frame(State#state{http2_machine=HTTP2Machine}, StreamID, IsFin, Data);
+		{ok, {lingering_data, _StreamID, DataLen}, HTTP2Machine} ->
+			lingering_data_frame(State#state{http2_machine=HTTP2Machine}, DataLen);
 		{ok, {headers, StreamID, IsFin, Headers, PseudoHeaders, BodyLen}, HTTP2Machine} ->
 			headers_frame(State#state{http2_machine=HTTP2Machine},
 				StreamID, IsFin, Headers, PseudoHeaders, BodyLen);
@@ -306,6 +308,12 @@ data_frame(State=#state{opts=Opts, streams=Streams}, StreamID, IsFin, Data) ->
 			State
 	end.
 
+lingering_data_frame(State=#state{socket=Socket, transport=Transport,
+		http2_machine=HTTP2Machine0}, DataLen) ->
+	Transport:send(Socket, cow_http2:window_update(DataLen)),
+	HTTP2Machine1 = cow_http2_machine:update_window(DataLen, HTTP2Machine0),
+	State#state{http2_machine=HTTP2Machine1}.
+
 headers_frame(State, StreamID, IsFin, Headers,
 		PseudoHeaders=#{method := <<"CONNECT">>}, _)
 		when map_size(PseudoHeaders) =:= 2 ->

+ 21 - 0
test/handlers/loop_handler_abort_h.erl

@@ -0,0 +1,21 @@
+%% This module implements a loop handler that reads
+%% 1000 bytes of the request body after sending itself
+%% a message, then terminates the stream.
+
+-module(loop_handler_abort_h).
+
+-export([init/2]).
+-export([info/3]).
+-export([terminate/3]).
+
+init(Req, _) ->
+	self() ! timeout,
+	{cowboy_loop, Req, undefined, hibernate}.
+
+info(timeout, Req0, State) ->
+	{_Status, Body, Req} = cowboy_req:read_body(Req0, #{length => 1000}),
+	1000 = byte_size(Body),
+	{stop, cowboy_req:reply(200, Req), State}.
+
+terminate(stop, _, _) ->
+	ok.

+ 50 - 0
test/rfc7540_SUITE.erl

@@ -49,6 +49,7 @@ init_routes(_) -> [
 		{"/", hello_h, []},
 		{"/echo/:key", echo_h, []},
 		{"/long_polling", long_polling_h, []},
+		{"/loop_handler_abort", loop_handler_abort_h, []},
 		{"/resp/:key[/:arg]", resp_h, []}
 	]}
 ].
@@ -3116,6 +3117,55 @@ data_reject_overflow_stream(Config0) ->
 		cowboy:stop_listener(?FUNCTION_NAME)
 	end.
 
+lingering_data_counts_toward_connection_window(Config0) ->
+	doc("DATA frames received after sending RST_STREAM must be counted "
+		"toward the connection flow-control window. (RFC7540 5.1)"),
+	Config = cowboy_test:init_http(?FUNCTION_NAME, #{
+		env => #{dispatch => cowboy_router:compile(init_routes(Config0))},
+		initial_connection_window_size => 100000
+	}, Config0),
+	try
+		%% We need to do the handshake manually because a WINDOW_UPDATE
+		%% frame will be sent to update the connection window.
+		{ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]),
+		%% Send a valid preface.
+		ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]),
+		%% Receive the server preface.
+		{ok, << Len1:24 >>} = gen_tcp:recv(Socket, 3, 1000),
+		{ok, << 4:8, 0:40, _:Len1/binary >>} = gen_tcp:recv(Socket, 6 + Len1, 1000),
+		%% Send the SETTINGS ack.
+		ok = gen_tcp:send(Socket, cow_http2:settings_ack()),
+		%% Receive the WINDOW_UPDATE for the connection.
+		{ok, << 4:24, 8:8, 0:40, _:32 >>} = gen_tcp:recv(Socket, 13, 1000),
+		%% Receive the SETTINGS ack.
+		{ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000),
+		Headers = [
+			{<<":method">>, <<"POST">>},
+			{<<":scheme">>, <<"http">>},
+			{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+			{<<":path">>, <<"/loop_handler_abort">>}
+		],
+		{HeadersBlock, _} = cow_hpack:encode(Headers),
+		ok = gen_tcp:send(Socket, [
+			cow_http2:headers(1, nofin, HeadersBlock),
+			cow_http2:data(1, nofin, <<0:1000/unit:8>>)
+		]),
+		% Make sure server send RST_STREAM.
+		timer:sleep(100),
+		ok = gen_tcp:send(Socket, [
+			cow_http2:data(1, fin, <<0:1000/unit:8>>)
+		]),
+		{ok, << SkipLen:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000),
+		% Skip the header.
+		{ok, _} = gen_tcp:recv(Socket, SkipLen, 1000),
+		% Skip RST_STREAM.
+		{ok, << 4:24, 3:8, 1:40, _:32 >>} = gen_tcp:recv(Socket, 13, 1000),
+		% Received a WINDOW_UPDATE frame after we got RST_STREAM.
+		{ok, << 4:24, 8:8, 0:40, 1000:32 >>} = gen_tcp:recv(Socket, 13, 1000)
+	after
+		cowboy:stop_listener(?FUNCTION_NAME)
+	end.
+
 %% (RFC7540 6.9.1)
 %   Frames with zero length with the END_STREAM flag set (that
 %   is, an empty DATA frame) MAY be sent if there is no available space