Skip to content

Commit 19f3a5b

Browse files
committed
HTTP/1: Skip request trailers if any
Previously an error would be sent back and the connection closed. Note that for now trailers are only skipped up to a hardcoded value. They will be better handled in a future release.
1 parent c8cd061 commit 19f3a5b

3 files changed

Lines changed: 131 additions & 38 deletions

File tree

src/cowboy_http.erl

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@
9696
transfer_decode_state :: cow_http_te:state()
9797
}).
9898

99+
-record(ps_trailer, {
100+
}).
101+
99102
-record(stream, {
100103
id = undefined :: cowboy_stream:streamid(),
101104
%% Stream handlers and their state.
@@ -146,7 +149,7 @@
146149
in_streamid = 1 :: pos_integer(),
147150

148151
%% Parsing state for the current stream or stream-to-be.
149-
in_state = #ps_request_line{} :: #ps_request_line{} | #ps_header{} | #ps_body{},
152+
in_state = #ps_request_line{} :: #ps_request_line{} | #ps_header{} | #ps_body{} | #ps_trailer{},
150153

151154
%% Flow requested for the current stream.
152155
flow = infinity :: non_neg_integer() | infinity,
@@ -397,7 +400,9 @@ parse(Buffer, State=#state{in_state=PS=#ps_header{headers=Headers, name=Name}})
397400
State#state{in_state=PS#ps_header{headers=undefined, name=undefined}},
398401
Headers, Name));
399402
parse(Buffer, State=#state{in_state=#ps_body{}}) ->
400-
after_parse(parse_body(Buffer, State)).
403+
after_parse(parse_body(Buffer, State));
404+
parse(Buffer, State=#state{in_state=#ps_trailer{}}) ->
405+
after_parse(parse_trailer(Buffer, State)).
401406

402407
after_parse({request, Req=#{streamid := StreamID, method := Method,
403408
headers := Headers, version := Version},
@@ -1024,7 +1029,6 @@ opts_for_upgrade(#state{opts=Opts, dynamic_buffer_size=Size,
10241029
parse_body(Buffer, State=#state{in_streamid=StreamID, in_state=
10251030
PS=#ps_body{received=Received, transfer_decode_fun=TDecode,
10261031
transfer_decode_state=TState0}}) ->
1027-
%% @todo Proper trailers.
10281032
try TDecode(Buffer, TState0) of
10291033
more ->
10301034
{more, State#state{buffer=Buffer}};
@@ -1040,18 +1044,58 @@ parse_body(Buffer, State=#state{in_streamid=StreamID, in_state=
10401044
{data, StreamID, nofin, Data, State#state{buffer=Rest,
10411045
in_state=PS#ps_body{received=Received + byte_size(Data),
10421046
transfer_decode_state=TState}}};
1043-
{done, _HasTrailers, Rest} ->
1044-
{data, StreamID, fin, <<>>,
1045-
State#state{buffer=Rest, in_streamid=StreamID + 1, in_state=#ps_request_line{}}};
1046-
{done, Data, _HasTrailers, Rest} ->
1047-
{data, StreamID, fin, Data,
1048-
State#state{buffer=Rest, in_streamid=StreamID + 1, in_state=#ps_request_line{}}}
1047+
{done, no_trailers, Rest} ->
1048+
{data, StreamID, fin, <<>>, State#state{
1049+
buffer=Rest,
1050+
in_streamid=StreamID + 1,
1051+
in_state=#ps_request_line{}
1052+
}};
1053+
{done, trailers, Rest} ->
1054+
{data, StreamID, fin, <<>>, State#state{
1055+
buffer=Rest,
1056+
in_state=#ps_trailer{}
1057+
}};
1058+
{done, Data, no_trailers, Rest} ->
1059+
{data, StreamID, fin, Data, State#state{
1060+
buffer=Rest,
1061+
in_streamid=StreamID + 1,
1062+
in_state=#ps_request_line{}
1063+
}};
1064+
{done, Data, trailers, Rest} ->
1065+
{data, StreamID, fin, Data, State#state{
1066+
buffer=Rest,
1067+
in_state=#ps_trailer{}
1068+
}}
10491069
catch _:_ ->
10501070
Reason = {connection_error, protocol_error,
10511071
'Failure to decode the content. (RFC7230 4)'},
10521072
terminate(stream_terminate(State, StreamID, Reason), Reason)
10531073
end.
10541074

1075+
%% Trailer field values.
1076+
1077+
%% @todo Proper trailers.
1078+
parse_trailer(Buffer, State=#state{in_streamid=StreamID, streams=Streams}) ->
1079+
case binary:split(Buffer, <<"\r\n\r\n">>) of
1080+
[_] when byte_size(Buffer) >= 4096 ->
1081+
Reason = {connection_error, limit_reached,
1082+
'Trailer field values larger than hard limit allows.'},
1083+
case Streams of
1084+
[#stream{id=StreamID}|_] ->
1085+
terminate(stream_terminate(State, StreamID, Reason), Reason);
1086+
%% Stream already terminated.
1087+
_ ->
1088+
terminate(State, Reason)
1089+
end;
1090+
[_] ->
1091+
{more, State#state{buffer=Buffer}};
1092+
[_, Rest] ->
1093+
parse(Rest, State#state{
1094+
in_streamid=StreamID + 1,
1095+
in_state=#ps_request_line{}
1096+
})
1097+
end.
1098+
10551099
%% Message handling.
10561100

10571101
down(State=#state{opts=Opts, children=Children0}, Pid, Msg) ->
@@ -1522,6 +1566,8 @@ stream_terminate(State0=#state{opts=Opts, in_streamid=InStreamID, in_state=InSta
15221566
terminate(State, skip_body_too_large);
15231567
#ps_body{} when InStreamID =:= OutStreamID ->
15241568
stream_next(State#state{flow=infinity});
1569+
#ps_trailer{} when InStreamID =:= OutStreamID ->
1570+
stream_next(State#state{flow=infinity});
15251571
_ ->
15261572
stream_next(State)
15271573
end.

test/http_SUITE.erl

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,52 @@ do_chunked_body(ChunkSize0, Data, Acc) ->
188188
do_chunked_body(ChunkSize, Rest,
189189
[iolist_to_binary(cow_http_te:chunk(Chunk))|Acc]).
190190

191+
chunked_trailers_skip(Config) ->
192+
doc("Confirm that request trailers are skipped and another "
193+
"request can be sent on the same connection."),
194+
Client = raw_open(Config),
195+
ok = raw_send(Client,
196+
"POST /echo/read_body HTTP/1.1\r\n"
197+
"Host: localhost\r\n"
198+
"Transfer-Encoding: chunked\r\n"
199+
"Trailer: x-foo\r\n"
200+
"\r\n"
201+
"5\r\nHello\r\n"
202+
"0\r\n"
203+
"X-Foo: bar\r\n"
204+
"\r\n"),
205+
Data = raw_recv_head(Client),
206+
{'HTTP/1.1', 200, _, Rest0} = cow_http:parse_status_line(Data),
207+
{_, Rest} = cow_http:parse_headers(Rest0),
208+
RestSize = byte_size(Rest),
209+
<<Rest:RestSize/binary, Expect/bits>> = <<"Hello">>,
210+
raw_expect_recv(Client, Expect),
211+
%% Second request/response on the same connection.
212+
ok = raw_send(Client,
213+
"GET / HTTP/1.1\r\n"
214+
"Host: localhost\r\n"
215+
"\r\n"),
216+
Data2 = raw_recv_head(Client),
217+
{'HTTP/1.1', 200, _, _} = cow_http:parse_status_line(Data2),
218+
ok.
219+
220+
chunked_trailers_skip_limit(Config) ->
221+
doc("Confirm that the trailer size limit is enforced and "
222+
"results in a 400 error and the closing of the connection."),
223+
Client = raw_open(Config),
224+
Large = binary:copy(<<"a">>, 5000),
225+
ok = raw_send(Client, [
226+
"POST /delay_hello HTTP/1.1\r\n"
227+
"Host: localhost\r\n"
228+
"Transfer-Encoding: chunked\r\n"
229+
"\r\n"
230+
"0\r\n"
231+
"X-Foo: ", Large
232+
]),
233+
Data = raw_recv_head(Client),
234+
{'HTTP/1.1', 400, _, _} = cow_http:parse_status_line(Data),
235+
{error, closed} = raw_recv(Client, 0, 1000).
236+
191237
disable_http1_tls(Config) ->
192238
doc("Ensure that we can disable HTTP/1.1 over TLS (force HTTP/2)."),
193239
TlsOpts = ct_helper:get_certs_from_ets(),

test/rfc7230_SUITE.erl

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1548,35 +1548,36 @@ remove_transfer_encoding_chunked_after_body_read(Config) ->
15481548
%The trailer header must be listed in the connection header field.
15491549
%Trailers must be ignored otherwise.
15501550

1551-
reject_invalid_trailer_name(Config) ->
1552-
doc("An invalid trailer field name must be rejected with a 400 status code "
1553-
"and the closing of the connection. (RFC9110 5.5)"),
1554-
#{code := 400, client := Client} = do_raw(Config,
1555-
"POST /echo/read_body HTTP/1.1\r\n"
1556-
"Host: localhost\r\n"
1557-
"Transfer-Encoding: chunked\r\n"
1558-
"Trailer: x-bad\r\n"
1559-
"\r\n"
1560-
"5\r\nHello\r\n"
1561-
"0\r\n"
1562-
"X\0-Bad: v\r\n"
1563-
"\r\n"),
1564-
{error, closed} = raw_recv(Client, 0, 1000).
1565-
1566-
reject_invalid_trailer_value(Config) ->
1567-
doc("An invalid trailer field value must be rejected with a 400 status code "
1568-
"and the closing of the connection. (RFC9110 5.5)"),
1569-
#{code := 400, client := Client} = do_raw(Config,
1570-
"POST /echo/read_body HTTP/1.1\r\n"
1571-
"Host: localhost\r\n"
1572-
"Transfer-Encoding: chunked\r\n"
1573-
"Trailer: x-bad\r\n"
1574-
"\r\n"
1575-
"5\r\nHello\r\n"
1576-
"0\r\n"
1577-
"X-Bad: val\0ue\r\n"
1578-
"\r\n"),
1579-
{error, closed} = raw_recv(Client, 0, 1000).
1551+
%% @todo Enable when request trailers are supported.
1552+
%reject_invalid_trailer_name(Config) ->
1553+
% doc("An invalid trailer field name must be rejected with a 400 status code "
1554+
% "and the closing of the connection. (RFC9110 5.5)"),
1555+
% #{code := 400, client := Client} = do_raw(Config,
1556+
% "POST /echo/read_body HTTP/1.1\r\n"
1557+
% "Host: localhost\r\n"
1558+
% "Transfer-Encoding: chunked\r\n"
1559+
% "Trailer: x-bad\r\n"
1560+
% "\r\n"
1561+
% "5\r\nHello\r\n"
1562+
% "0\r\n"
1563+
% "X\0-Bad: v\r\n"
1564+
% "\r\n"),
1565+
% {error, closed} = raw_recv(Client, 0, 1000).
1566+
%
1567+
%reject_invalid_trailer_value(Config) ->
1568+
% doc("An invalid trailer field value must be rejected with a 400 status code "
1569+
% "and the closing of the connection. (RFC9110 5.5)"),
1570+
% #{code := 400, client := Client} = do_raw(Config,
1571+
% "POST /echo/read_body HTTP/1.1\r\n"
1572+
% "Host: localhost\r\n"
1573+
% "Transfer-Encoding: chunked\r\n"
1574+
% "Trailer: x-bad\r\n"
1575+
% "\r\n"
1576+
% "5\r\nHello\r\n"
1577+
% "0\r\n"
1578+
% "X-Bad: val\0ue\r\n"
1579+
% "\r\n"),
1580+
% {error, closed} = raw_recv(Client, 0, 1000).
15801581

15811582
%%% @todo Though we need a compatibility mode as some clients don't send it...
15821583
%reject_chunked_missing_end_crlf(Config) ->

0 commit comments

Comments
 (0)