mirror of
https://github.com/ninenines/cowboy.git
synced 2025-07-14 12:20:24 +00:00
Add more rfc7230 tests and better handle bad chunk sizes
Bad chunk sizes used to be accepted and could result in a badly parsed body or a timeout. They are now properly rejected. Chunk extensions now have a hard limit of 129 characters. I haven't heard of anyone using them and Cowboy does not provide an interface for them, but we can always increase or make configurable if it ever becomes necessary (but I honestly doubt it). Also a test from the old http suite could be removed. Yay!
This commit is contained in:
parent
1af508c4cd
commit
c4e43ec26a
4 changed files with 296 additions and 109 deletions
|
@ -727,7 +727,7 @@ http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Tran
|
||||||
parse_body(Buffer, State=#state{in_streamid=StreamID, in_state=
|
parse_body(Buffer, State=#state{in_streamid=StreamID, in_state=
|
||||||
PS=#ps_body{transfer_decode_fun=TDecode, transfer_decode_state=TState0}}) ->
|
PS=#ps_body{transfer_decode_fun=TDecode, transfer_decode_state=TState0}}) ->
|
||||||
%% @todo Proper trailers.
|
%% @todo Proper trailers.
|
||||||
case TDecode(Buffer, TState0) of
|
try TDecode(Buffer, TState0) of
|
||||||
more ->
|
more ->
|
||||||
%% @todo Asks for 0 or more bytes.
|
%% @todo Asks for 0 or more bytes.
|
||||||
{more, State, Buffer};
|
{more, State, Buffer};
|
||||||
|
@ -749,6 +749,10 @@ parse_body(Buffer, State=#state{in_streamid=StreamID, in_state=
|
||||||
{done, Data, _HasTrailers, Rest} ->
|
{done, Data, _HasTrailers, Rest} ->
|
||||||
{data, StreamID, fin, Data, set_timeout(
|
{data, StreamID, fin, Data, set_timeout(
|
||||||
State#state{in_streamid=StreamID + 1, in_state=#ps_request_line{}}), Rest}
|
State#state{in_streamid=StreamID + 1, in_state=#ps_request_line{}}), Rest}
|
||||||
|
catch _:_ ->
|
||||||
|
Reason = {connection_error, protocol_error,
|
||||||
|
'Failure to decode the content. (RFC7230 4)'},
|
||||||
|
terminate(stream_terminate(State, StreamID, Reason), Reason)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Message handling.
|
%% Message handling.
|
||||||
|
@ -1031,6 +1035,8 @@ stream_terminate(State0=#state{out_streamid=OutStreamID, out_state=OutState,
|
||||||
State1 = #state{streams=Streams1} = case OutState of
|
State1 = #state{streams=Streams1} = case OutState of
|
||||||
wait when element(1, Reason) =:= internal_error ->
|
wait when element(1, Reason) =:= internal_error ->
|
||||||
info(State0, StreamID, {response, 500, #{<<"content-length">> => <<"0">>}, <<>>});
|
info(State0, StreamID, {response, 500, #{<<"content-length">> => <<"0">>}, <<>>});
|
||||||
|
wait when element(1, Reason) =:= connection_error ->
|
||||||
|
info(State0, StreamID, {response, 400, #{<<"content-length">> => <<"0">>}, <<>>});
|
||||||
wait ->
|
wait ->
|
||||||
info(State0, StreamID, {response, 204, #{}, <<>>});
|
info(State0, StreamID, {response, 204, #{}, <<>>});
|
||||||
chunked when Version =:= 'HTTP/1.1' ->
|
chunked when Version =:= 'HTTP/1.1' ->
|
||||||
|
|
|
@ -22,6 +22,10 @@ echo(<<"read_body">>, Req0, Opts) ->
|
||||||
cowboy_req:inform(100, Req0),
|
cowboy_req:inform(100, Req0),
|
||||||
cowboy_req:read_body(Req0);
|
cowboy_req:read_body(Req0);
|
||||||
<<"/full", _/bits>> -> read_body(Req0, <<>>);
|
<<"/full", _/bits>> -> read_body(Req0, <<>>);
|
||||||
|
<<"/length", _/bits>> ->
|
||||||
|
{_, _, Req1} = read_body(Req0, <<>>),
|
||||||
|
Length = cowboy_req:body_length(Req1),
|
||||||
|
{ok, integer_to_binary(Length), Req1};
|
||||||
<<"/opts", _/bits>> -> cowboy_req:read_body(Req0, Opts);
|
<<"/opts", _/bits>> -> cowboy_req:read_body(Req0, Opts);
|
||||||
_ -> cowboy_req:read_body(Req0)
|
_ -> cowboy_req:read_body(Req0)
|
||||||
end,
|
end,
|
||||||
|
|
|
@ -307,19 +307,6 @@ http10_keepalive_forced(Config) ->
|
||||||
_ -> ok
|
_ -> ok
|
||||||
end.
|
end.
|
||||||
|
|
||||||
keepalive_max(Config) ->
|
|
||||||
ConnPid = gun_open(Config),
|
|
||||||
Refs = [gun:get(ConnPid, "/", [{<<"connection">>, <<"keep-alive">>}])
|
|
||||||
|| _ <- lists:seq(1, 99)],
|
|
||||||
CloseRef = gun:get(ConnPid, "/", [{<<"connection">>, <<"keep-alive">>}]),
|
|
||||||
_ = [begin
|
|
||||||
{response, nofin, 200, Headers} = gun:await(ConnPid, Ref),
|
|
||||||
false = lists:keymember(<<"connection">>, 1, Headers)
|
|
||||||
end || Ref <- Refs],
|
|
||||||
{response, nofin, 200, Headers} = gun:await(ConnPid, CloseRef),
|
|
||||||
{_, <<"close">>} = lists:keyfind(<<"connection">>, 1, Headers),
|
|
||||||
gun_down(ConnPid).
|
|
||||||
|
|
||||||
keepalive_nl(Config) ->
|
keepalive_nl(Config) ->
|
||||||
ConnPid = gun_open(Config),
|
ConnPid = gun_open(Config),
|
||||||
Refs = [begin
|
Refs = [begin
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
-compile(export_all).
|
-compile(export_all).
|
||||||
|
|
||||||
-import(ct_helper, [doc/1]).
|
-import(ct_helper, [doc/1]).
|
||||||
|
-import(cowboy_test, [gun_open/1]).
|
||||||
-import(cowboy_test, [raw_open/1]).
|
-import(cowboy_test, [raw_open/1]).
|
||||||
-import(cowboy_test, [raw_send/2]).
|
-import(cowboy_test, [raw_send/2]).
|
||||||
-import(cowboy_test, [raw_recv_head/1]).
|
-import(cowboy_test, [raw_recv_head/1]).
|
||||||
|
@ -36,7 +37,8 @@ end_per_group(Name, _) ->
|
||||||
init_routes(_) -> [
|
init_routes(_) -> [
|
||||||
{"localhost", [
|
{"localhost", [
|
||||||
{"/", hello_h, []},
|
{"/", hello_h, []},
|
||||||
{"/echo/:key", echo_h, []}
|
{"/echo/:key[/:arg]", echo_h, []},
|
||||||
|
{"/length/echo/:key", echo_h, []}
|
||||||
%% @todo Something is clearly wrong about routing * right now.
|
%% @todo Something is clearly wrong about routing * right now.
|
||||||
%% {"*", asterisk_h, []}
|
%% {"*", asterisk_h, []}
|
||||||
]},
|
]},
|
||||||
|
@ -878,9 +880,9 @@ limit_headers(Config) ->
|
||||||
%
|
%
|
||||||
%@todo
|
%@todo
|
||||||
%The information in the via header is largely unreliable. (RFC7230 5.7.1)
|
%The information in the via header is largely unreliable. (RFC7230 5.7.1)
|
||||||
%
|
|
||||||
%%% Request body.
|
%% Request body.
|
||||||
%
|
|
||||||
%@todo
|
%@todo
|
||||||
%The message body is the octets after decoding any transfer
|
%The message body is the octets after decoding any transfer
|
||||||
%codings. (RFC7230 3.3)
|
%codings. (RFC7230 3.3)
|
||||||
|
@ -888,6 +890,10 @@ limit_headers(Config) ->
|
||||||
no_request_body(Config) ->
|
no_request_body(Config) ->
|
||||||
doc("A request has a message body only if it includes a transfer-encoding "
|
doc("A request has a message body only if it includes a transfer-encoding "
|
||||||
"header or a non-zero content-length header. (RFC7230 3.3)"),
|
"header or a non-zero content-length header. (RFC7230 3.3)"),
|
||||||
|
#{code := 200, body := <<"false">>} = do_raw(Config, [
|
||||||
|
"POST /echo/has_body HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"\r\n"]),
|
||||||
#{code := 200, body := <<>>} = do_raw(Config, [
|
#{code := 200, body := <<>>} = do_raw(Config, [
|
||||||
"POST /echo/read_body HTTP/1.1\r\n"
|
"POST /echo/read_body HTTP/1.1\r\n"
|
||||||
"Host: localhost\r\n"
|
"Host: localhost\r\n"
|
||||||
|
@ -897,6 +903,11 @@ no_request_body(Config) ->
|
||||||
no_request_body_content_length_zero(Config) ->
|
no_request_body_content_length_zero(Config) ->
|
||||||
doc("A request has a message body only if it includes a transfer-encoding "
|
doc("A request has a message body only if it includes a transfer-encoding "
|
||||||
"header or a non-zero content-length header. (RFC7230 3.3)"),
|
"header or a non-zero content-length header. (RFC7230 3.3)"),
|
||||||
|
#{code := 200, body := <<"false">>} = do_raw(Config, [
|
||||||
|
"POST /echo/has_body HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Content-length: 0\r\n"
|
||||||
|
"\r\n"]),
|
||||||
#{code := 200, body := <<>>} = do_raw(Config, [
|
#{code := 200, body := <<>>} = do_raw(Config, [
|
||||||
"POST /echo/read_body HTTP/1.1\r\n"
|
"POST /echo/read_body HTTP/1.1\r\n"
|
||||||
"Host: localhost\r\n"
|
"Host: localhost\r\n"
|
||||||
|
@ -907,6 +918,12 @@ no_request_body_content_length_zero(Config) ->
|
||||||
request_body_content_length(Config) ->
|
request_body_content_length(Config) ->
|
||||||
doc("A request has a message body only if it includes a transfer-encoding "
|
doc("A request has a message body only if it includes a transfer-encoding "
|
||||||
"header or a non-zero content-length header. (RFC7230 3.3)"),
|
"header or a non-zero content-length header. (RFC7230 3.3)"),
|
||||||
|
#{code := 200, body := <<"true">>} = do_raw(Config, [
|
||||||
|
"POST /echo/has_body HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Content-length: 12\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"Hello world!"]),
|
||||||
#{code := 200, body := <<"Hello world!">>} = do_raw(Config, [
|
#{code := 200, body := <<"Hello world!">>} = do_raw(Config, [
|
||||||
"POST /echo/read_body HTTP/1.1\r\n"
|
"POST /echo/read_body HTTP/1.1\r\n"
|
||||||
"Host: localhost\r\n"
|
"Host: localhost\r\n"
|
||||||
|
@ -918,6 +935,12 @@ request_body_content_length(Config) ->
|
||||||
request_body_transfer_encoding(Config) ->
|
request_body_transfer_encoding(Config) ->
|
||||||
doc("A request has a message body only if it includes a transfer-encoding "
|
doc("A request has a message body only if it includes a transfer-encoding "
|
||||||
"header or a non-zero content-length header. (RFC7230 3.3)"),
|
"header or a non-zero content-length header. (RFC7230 3.3)"),
|
||||||
|
#{code := 200, body := <<"true">>} = do_raw(Config, [
|
||||||
|
"POST /echo/has_body HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Transfer-encoding: chunked\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"6\r\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]),
|
||||||
#{code := 200, body := <<"Hello world!">>} = do_raw(Config, [
|
#{code := 200, body := <<"Hello world!">>} = do_raw(Config, [
|
||||||
"POST /echo/read_body HTTP/1.1\r\n"
|
"POST /echo/read_body HTTP/1.1\r\n"
|
||||||
"Host: localhost\r\n"
|
"Host: localhost\r\n"
|
||||||
|
@ -1052,29 +1075,64 @@ ignore_content_length_when_transfer_encoding(Config) ->
|
||||||
%timeout_while_reading_body(Config) ->
|
%timeout_while_reading_body(Config) ->
|
||||||
%If a timeout occurs while reading the body the server must
|
%If a timeout occurs while reading the body the server must
|
||||||
%send a 408 status code response and close the connection. (RFC7230 3.3.3, RFC7230 3.4)
|
%send a 408 status code response and close the connection. (RFC7230 3.3.3, RFC7230 3.4)
|
||||||
%
|
|
||||||
%%% Body length.
|
%% Body length.
|
||||||
%
|
|
||||||
%body_length_chunked_before(Config) ->
|
body_length_chunked_before(Config) ->
|
||||||
%The length of a message with a transfer-encoding header can
|
doc("The length of a message with a transfer-encoding header can "
|
||||||
%only be determined on decoding completion. (RFC7230 3.3.3)
|
"only be determined on decoding completion. (RFC7230 3.3.3)"),
|
||||||
%
|
#{code := 200, body := <<"undefined">>} = do_raw(Config, [
|
||||||
%body_length_chunked_after(Config) ->
|
"POST /echo/body_length HTTP/1.1\r\n"
|
||||||
%Upon completion of chunk decoding the server must add a content-length
|
"Host: localhost\r\n"
|
||||||
%header with the value set to the total length of data read. (RFC7230 4.1.3)
|
"Transfer-encoding: chunked\r\n"
|
||||||
%
|
"\r\n"
|
||||||
%body_length_content_length(Config) ->
|
"6\r\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]),
|
||||||
%The length of a message with a content-length header is
|
ok.
|
||||||
%the numeric value in octets found in the header. (RFC7230 3.3.3)
|
|
||||||
%
|
body_length_chunked_after(Config) ->
|
||||||
%body_length_zero(Config) ->
|
doc("Upon completion of chunk decoding the server must add a content-length "
|
||||||
%A message with no transfer-encoding or content-length header
|
"header with the value set to the total length of data read. (RFC7230 4.1.3)"),
|
||||||
%has a body length of 0. (RFC7230 3.3.3)
|
#{code := 200, body := <<"12">>} = do_raw(Config, [
|
||||||
%
|
"POST /length/echo/read_body HTTP/1.1\r\n"
|
||||||
%%% Chunked transfer-encoding.
|
"Host: localhost\r\n"
|
||||||
%
|
"Transfer-encoding: chunked\r\n"
|
||||||
%reject_invalid_chunk_size(Config) ->
|
"\r\n"
|
||||||
%
|
"6\r\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
body_length_content_length(Config) ->
|
||||||
|
doc("The length of a message with a content-length header is "
|
||||||
|
"the numeric value in octets found in the header. (RFC7230 3.3.3)"),
|
||||||
|
#{code := 200, body := <<"12">>} = do_raw(Config, [
|
||||||
|
"POST /echo/body_length HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Content-length: 12\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"Hello world!"]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
body_length_zero(Config) ->
|
||||||
|
doc("A message with no transfer-encoding or content-length header "
|
||||||
|
"has a body length of 0. (RFC7230 3.3.3)"),
|
||||||
|
#{code := 200, body := <<"0">>} = do_raw(Config, [
|
||||||
|
"POST /echo/body_length HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"\r\n"]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% Chunked transfer-encoding.
|
||||||
|
|
||||||
|
reject_invalid_chunk_size(Config) ->
|
||||||
|
doc("A request with an invalid chunk size must be rejected "
|
||||||
|
"with a 400 status code and the closing of the connection. (RFC7230 4.1)"),
|
||||||
|
#{code := 400, client := Client} = do_raw(Config, [
|
||||||
|
"POST /echo/read_body HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Transfer-encoding: chunked\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"6\r\nHello \r\nFIVE\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]),
|
||||||
|
{error, closed} = raw_recv(Client, 0, 1000).
|
||||||
|
|
||||||
%```
|
%```
|
||||||
%chunked-body = *chunk last-chunk trailer-part CRLF
|
%chunked-body = *chunk last-chunk trailer-part CRLF
|
||||||
%
|
%
|
||||||
|
@ -1093,52 +1151,148 @@ ignore_content_length_when_transfer_encoding(Config) ->
|
||||||
%chunk-ext-name = token
|
%chunk-ext-name = token
|
||||||
%chunk-ext-val = token / quoted-string
|
%chunk-ext-val = token / quoted-string
|
||||||
%```
|
%```
|
||||||
%
|
|
||||||
%ignore_unknown_chunk_extensions(Config) ->
|
ignore_unknown_chunk_extensions(Config) ->
|
||||||
%Unknown chunk extensions must be ignored. (RFC7230 4.1.1)
|
doc("Unknown chunk extensions must be ignored. (RFC7230 4.1.1)"),
|
||||||
%
|
#{code := 200, body := <<"Hello world!">>} = do_raw(Config, [
|
||||||
%reject_invalid_chunk_extensions(Config) ->
|
"POST /echo/read_body HTTP/1.1\r\n"
|
||||||
%
|
"Host: localhost\r\n"
|
||||||
%limit_chunk_size_line(Config) ->
|
"Transfer-encoding: chunked\r\n"
|
||||||
%The chunk-size line length must be subject to configuration.
|
"\r\n"
|
||||||
%There are no recommended defaults, although 100 octets should work.
|
"6; hello=\"cool world\"\r\nHello \r\n"
|
||||||
%Requests with a too long line must be rejected with a 400 status
|
"5 ; one ; two ; three;four;five\r\nworld"
|
||||||
%code and the closing of the connection.
|
"\r\n1;ok\r\n!\r\n0\r\n\r\n"]),
|
||||||
%
|
ok.
|
||||||
%reject_invalid_chunk_line_crlf(Config) ->
|
|
||||||
%reject_invalid_chunk_data_crlf(Config) ->
|
%% Since we skip everything right now, the only reason
|
||||||
%
|
%% we might reject chunk extensions is if they are too large.
|
||||||
|
limit_chunk_size_line(Config) ->
|
||||||
|
doc("A request with chunk extensions larger than the server allows must be rejected "
|
||||||
|
"with a 400 status code and the closing of the connection. (RFC7230 4.1.1)"),
|
||||||
|
#{code := 200, body := <<"Hello world!">>} = do_raw(Config, [
|
||||||
|
"POST /echo/read_body HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Transfer-encoding: chunked\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"6; hello=\"cool world\"\r\nHello \r\n"
|
||||||
|
"5;", lists:duplicate(128, $a), "\r\nworld"
|
||||||
|
"\r\n1;ok\r\n!\r\n0\r\n\r\n"]),
|
||||||
|
#{code := 400, client := Client} = do_raw(Config, [
|
||||||
|
"POST /echo/read_body HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Transfer-encoding: chunked\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"6; hello=\"cool world\"\r\nHello \r\n"
|
||||||
|
"5;", lists:duplicate(129, $a), "\r\nworld"
|
||||||
|
"\r\n1;ok\r\n!\r\n0\r\n\r\n"]),
|
||||||
|
{error, closed} = raw_recv(Client, 0, 1000).
|
||||||
|
|
||||||
|
reject_invalid_chunk_size_crlf(Config) ->
|
||||||
|
doc("A request with an invalid line break after the chunk size must be rejected "
|
||||||
|
"with a 400 status code and the closing of the connection. (RFC7230 4.1)"),
|
||||||
|
#{code := 400, client := Client1} = do_raw(Config, [
|
||||||
|
"POST /echo/read_body HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Transfer-encoding: chunked\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"6\rHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]),
|
||||||
|
{error, closed} = raw_recv(Client1, 0, 1000),
|
||||||
|
#{code := 400, client := Client2} = do_raw(Config, [
|
||||||
|
"POST /echo/read_body HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Transfer-encoding: chunked\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"6\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]),
|
||||||
|
{error, closed} = raw_recv(Client2, 0, 1000),
|
||||||
|
#{code := 400, client := Client3} = do_raw(Config, [
|
||||||
|
"POST /echo/read_body HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Transfer-encoding: chunked\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"6Hello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]),
|
||||||
|
{error, closed} = raw_recv(Client3, 0, 1000).
|
||||||
|
|
||||||
|
reject_invalid_chunk_ext_crlf(Config) ->
|
||||||
|
doc("A request with an invalid line break after chunk extensions must be rejected "
|
||||||
|
"with a 400 status code and the closing of the connection. (RFC7230 4.1)"),
|
||||||
|
#{code := 400, client := Client1} = do_raw(Config, [
|
||||||
|
"POST /echo/read_body HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Transfer-encoding: chunked\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"6; extensions\rHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]),
|
||||||
|
{error, closed} = raw_recv(Client1, 0, 1000),
|
||||||
|
#{code := 400, client := Client2} = do_raw(Config, [
|
||||||
|
"POST /echo/read_body HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Transfer-encoding: chunked\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"6; extensions\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]),
|
||||||
|
{error, closed} = raw_recv(Client2, 0, 1000),
|
||||||
|
#{code := 400, client := Client3} = do_raw(Config, [
|
||||||
|
"POST /echo/read_body HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Transfer-encoding: chunked\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"6; extensionsHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]),
|
||||||
|
{error, closed} = raw_recv(Client3, 0, 1000).
|
||||||
|
|
||||||
|
reject_invalid_chunk_data_crlf(Config) ->
|
||||||
|
doc("A request with an invalid line break after the chunk data must be rejected "
|
||||||
|
"with a 400 status code and the closing of the connection. (RFC7230 4.1)"),
|
||||||
|
#{code := 400, client := Client1} = do_raw(Config, [
|
||||||
|
"POST /echo/read_body HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Transfer-encoding: chunked\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"6\r\nHello \r5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]),
|
||||||
|
{error, closed} = raw_recv(Client1, 0, 1000),
|
||||||
|
#{code := 400, client := Client2} = do_raw(Config, [
|
||||||
|
"POST /echo/read_body HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Transfer-encoding: chunked\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"6\r\nHello \n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]),
|
||||||
|
{error, closed} = raw_recv(Client2, 0, 1000),
|
||||||
|
#{code := 400, client := Client3} = do_raw(Config, [
|
||||||
|
"POST /echo/read_body HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Transfer-encoding: chunked\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"6\r\nHello 5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]),
|
||||||
|
{error, closed} = raw_recv(Client3, 0, 1000).
|
||||||
|
|
||||||
%```
|
%```
|
||||||
%trailer-part = *( header-field CRLF )
|
%trailer-part = *( header-field CRLF )
|
||||||
%```
|
%```
|
||||||
%
|
%
|
||||||
%%% @todo see headers above and reject the same way, space etc.
|
%%% @todo see headers above and reject the same way, space etc.
|
||||||
%reject_invalid_trailer_part(Config) ->
|
%reject_invalid_request_trailer(Config) ->
|
||||||
%
|
%
|
||||||
%ignore_trailer_transfer_encoding(Config) ->
|
%ignore_request_trailer_transfer_encoding(Config) ->
|
||||||
%ignore_trailer_content_length(Config) ->
|
%ignore_request_trailer_content_length(Config) ->
|
||||||
%ignore_trailer_host(Config) ->
|
%ignore_request_trailer_host(Config) ->
|
||||||
%ignore_trailer_cache_control(Config) ->
|
%ignore_request_trailer_cache_control(Config) ->
|
||||||
%ignore_trailer_expect(Config) ->
|
%ignore_request_trailer_expect(Config) ->
|
||||||
%ignore_trailer_max_forwards(Config) ->
|
%ignore_request_trailer_max_forwards(Config) ->
|
||||||
%ignore_trailer_pragma(Config) ->
|
%ignore_request_trailer_pragma(Config) ->
|
||||||
%ignore_trailer_range(Config) ->
|
%ignore_request_trailer_range(Config) ->
|
||||||
%ignore_trailer_te(Config) ->
|
%ignore_request_trailer_te(Config) ->
|
||||||
%ignore_trailer_if_match(Config) ->
|
%ignore_request_trailer_if_match(Config) ->
|
||||||
%ignore_trailer_if_none_match(Config) ->
|
%ignore_request_trailer_if_none_match(Config) ->
|
||||||
%ignore_trailer_if_modified_since(Config) ->
|
%ignore_request_trailer_if_modified_since(Config) ->
|
||||||
%ignore_trailer_if_unmodified_since(Config) ->
|
%ignore_request_trailer_if_unmodified_since(Config) ->
|
||||||
%ignore_trailer_if_range(Config) ->
|
%ignore_request_trailer_if_range(Config) ->
|
||||||
%ignore_trailer_www_authenticate(Config) ->
|
%ignore_request_trailer_www_authenticate(Config) ->
|
||||||
%ignore_trailer_authorization(Config) ->
|
%ignore_request_trailer_authorization(Config) ->
|
||||||
%ignore_trailer_proxy_authenticate(Config) ->
|
%ignore_request_trailer_proxy_authenticate(Config) ->
|
||||||
%ignore_trailer_proxy_authorization(Config) ->
|
%ignore_request_trailer_proxy_authorization(Config) ->
|
||||||
%ignore_trailer_content_encoding(Config) ->
|
%ignore_request_trailer_content_encoding(Config) ->
|
||||||
%ignore_trailer_content_type(Config) ->
|
%ignore_request_trailer_content_type(Config) ->
|
||||||
%ignore_trailer_content_range(Config) ->
|
%ignore_request_trailer_content_range(Config) ->
|
||||||
%ignore_trailer_trailer(Config) ->
|
%ignore_request_trailer_trailer(Config) ->
|
||||||
%
|
%
|
||||||
%ignore_trailer_header(Config, Header) ->
|
%ignore_response_trailer_header(Config, Header) ->
|
||||||
%Trailing headers must not include transfer-encoding, content-length,
|
%Trailing headers must not include transfer-encoding, content-length,
|
||||||
%host, cache-control, expect, max-forwards, pragma, range, te,
|
%host, cache-control, expect, max-forwards, pragma, range, te,
|
||||||
%if-match, if-none-match, if-modified-since, if-unmodified-since,
|
%if-match, if-none-match, if-modified-since, if-unmodified-since,
|
||||||
|
@ -1147,23 +1301,33 @@ ignore_content_length_when_transfer_encoding(Config) ->
|
||||||
%retry-after, vary, warning, content-encoding, content-type,
|
%retry-after, vary, warning, content-encoding, content-type,
|
||||||
%content-range, or trailer. (RFC7230 4.1.2)
|
%content-range, or trailer. (RFC7230 4.1.2)
|
||||||
%
|
%
|
||||||
%Trailer headers can be ignored safely. (RFC7230 4.1.2)
|
|
||||||
%
|
|
||||||
%When trailer headers are processed, invalid headers must be ignored.
|
%When trailer headers are processed, invalid headers must be ignored.
|
||||||
%Valid headers must be added to the list of headers of the request. (RFC7230 4.1.2)
|
%Valid headers must be added to the list of headers of the request. (RFC7230 4.1.2)
|
||||||
%
|
%
|
||||||
%limit_trailer_headers(Config) ->
|
%ignore_request_trailers(Config) ->
|
||||||
|
%Trailer headers can be ignored safely. (RFC7230 4.1.2)
|
||||||
|
%
|
||||||
|
%limit_request_trailer_headers(Config) ->
|
||||||
%The number of trailer headers must be subject to configuration.
|
%The number of trailer headers must be subject to configuration.
|
||||||
%There is no known recommendations for the default. A value of 10
|
%There is no known recommendations for the default. A value of 10
|
||||||
%should cover most cases. Requests with too many trailer headers
|
%should cover most cases. Requests with too many trailer headers
|
||||||
%must be rejected with a 431 status code and the closing of the
|
%must be rejected with a 431 status code and the closing of the
|
||||||
%connection. (RFC6585 5)
|
%connection. (RFC6585 5)
|
||||||
%
|
|
||||||
%remove_transfer_encoding_chunked_after_body_read(Config) ->
|
%% We remove the header immediately so there's no need
|
||||||
%Upon completion of chunk decoding the server must remove "chunked"
|
%% to try to read the body before checking.
|
||||||
%from the transfer-encoding header. This header must be removed if
|
remove_transfer_encoding_chunked_after_body_read(Config) ->
|
||||||
%it becomes empty following this removal. (RFC7230 4.1.3)
|
doc("Upon completion of chunk decoding the server must remove \"chunked\" "
|
||||||
%
|
"from the transfer-encoding header. This header must be removed if "
|
||||||
|
"it becomes empty following this removal. (RFC7230 4.1.3)"),
|
||||||
|
#{code := 200, body := <<"undefined">>} = do_raw(Config, [
|
||||||
|
"POST /echo/header/transfer-encoding HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Transfer-encoding: chunked\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"6\r\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]),
|
||||||
|
ok.
|
||||||
|
|
||||||
%remove_trailer_after_body_read(Config) ->
|
%remove_trailer_after_body_read(Config) ->
|
||||||
%Upon completion of chunk decoding the server must remove the trailer
|
%Upon completion of chunk decoding the server must remove the trailer
|
||||||
%header from the list of headers. (RFC7230 4.1.3)
|
%header from the list of headers. (RFC7230 4.1.3)
|
||||||
|
@ -1184,9 +1348,9 @@ ignore_content_length_when_transfer_encoding(Config) ->
|
||||||
%%% @todo Though we need a compatibility mode as some clients don't send it...
|
%%% @todo Though we need a compatibility mode as some clients don't send it...
|
||||||
%reject_chunked_missing_end_crlf(Config) ->
|
%reject_chunked_missing_end_crlf(Config) ->
|
||||||
%@todo ending CRLF
|
%@todo ending CRLF
|
||||||
%
|
|
||||||
%%% Connection management.
|
%% Connection management.
|
||||||
%
|
|
||||||
%@todo can probably test using auth
|
%@todo can probably test using auth
|
||||||
%Never assume any two requests on a single connection come
|
%Never assume any two requests on a single connection come
|
||||||
%from the same user agent. (RFC7230 2.3)
|
%from the same user agent. (RFC7230 2.3)
|
||||||
|
@ -1206,23 +1370,49 @@ ignore_content_length_when_transfer_encoding(Config) ->
|
||||||
%The server must determine if the connection is persistent for
|
%The server must determine if the connection is persistent for
|
||||||
%every message received by looking at the connection header and
|
%every message received by looking at the connection header and
|
||||||
%HTTP version. (RFC7230 6.3)
|
%HTTP version. (RFC7230 6.3)
|
||||||
%
|
|
||||||
%no_connection_header_keepalive(Config) ->
|
no_connection_header_keepalive(Config) ->
|
||||||
%%% @todo http/1.0 suite? connection_keepalive(Config) ->
|
doc("HTTP/1.1 requests with no \"close\" option and HTTP/1.0 with the "
|
||||||
%HTTP/1.1 requests with no "close" option and HTTP/1.0 with the
|
"\"keep-alive\" option indicate the connection will persist. (RFC7230 6.1, RFC7230 6.3)"),
|
||||||
%"keep-alive" option indicate the connection will persist. (RFC7230 6.1, RFC7230 6.3)
|
#{code := 200, client := Client} = do_raw(Config, [
|
||||||
%
|
"GET / HTTP/1.1\r\n"
|
||||||
%connection_close(Config) ->
|
"Host: localhost\r\n"
|
||||||
%%% @todo http/1.0 suite? no_connection_close(Config) ->
|
"\r\n"]),
|
||||||
%HTTP/1.1 requests with the "close" option and HTTP/1.0 with no
|
{error, timeout} = raw_recv(Client, 0, 1000).
|
||||||
%"keep-alive" option indicate the connection will be closed
|
|
||||||
%upon reception of the response by the client. (RFC7230 6.1, RFC7230 6.3)
|
%% @todo http/1.0 suite? connection_keepalive(Config) ->
|
||||||
%
|
|
||||||
%limit_requests_keepalive(Config) ->
|
connection_close(Config) ->
|
||||||
%The maximum number of requests sent using a persistent connection
|
doc("HTTP/1.1 requests with the \"close\" option and HTTP/1.0 with no "
|
||||||
%must be subject to configuration. The connection must be closed
|
"\"keep-alive\" option indicate the connection will be closed "
|
||||||
%when the limit is reached. (RFC7230 6.3)
|
"upon reception of the response by the client. (RFC7230 6.1, RFC7230 6.3)"),
|
||||||
%
|
#{code := 200, client := Client} = do_raw(Config, [
|
||||||
|
"GET / HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Connection: close\r\n"
|
||||||
|
"\r\n"]),
|
||||||
|
{error, closed} = raw_recv(Client, 0, 1000).
|
||||||
|
|
||||||
|
%% @todo http/1.0 suite? no_connection_close(Config) ->
|
||||||
|
|
||||||
|
limit_requests_keepalive(Config) ->
|
||||||
|
doc("The maximum number of requests sent using a persistent connection "
|
||||||
|
"must be subject to configuration. The connection must be closed "
|
||||||
|
"when the limit is reached. (RFC7230 6.3)"),
|
||||||
|
ConnPid = gun_open(Config),
|
||||||
|
_ = [begin
|
||||||
|
Ref = gun:get(ConnPid, "/"),
|
||||||
|
{response, nofin, 200, RespHeaders} = gun:await(ConnPid, Ref),
|
||||||
|
{ok, <<"Hello world!">>} = gun:await_body(ConnPid, Ref),
|
||||||
|
false = lists:keyfind(<<"connection">>, 1, RespHeaders)
|
||||||
|
end || _ <- lists:seq(1,99)],
|
||||||
|
%% Final request closes the connection.
|
||||||
|
Ref = gun:get(ConnPid, "/"),
|
||||||
|
{response, nofin, 200, RespHeaders} = gun:await(ConnPid, Ref),
|
||||||
|
{ok, <<"Hello world!">>} = gun:await_body(ConnPid, Ref),
|
||||||
|
{_, <<"close">>} = lists:keyfind(<<"connection">>, 1, RespHeaders),
|
||||||
|
ok.
|
||||||
|
|
||||||
%skip_request_body_by_closing_connection(Config) ->
|
%skip_request_body_by_closing_connection(Config) ->
|
||||||
%%A server that doesn't want to read the entire body of a message
|
%%A server that doesn't want to read the entire body of a message
|
||||||
%%must close the connection, if possible after sending the "close"
|
%%must close the connection, if possible after sending the "close"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue