Skip to content

Commit

Permalink
Add a max_line_length to the HTTP protocol
Browse files Browse the repository at this point in the history
Allows to limit the size of request and header lines, thus preventing
Cowboy from infinitely reading from the socket and never finding an
end of line.

Defaults to 4096 bytes.
  • Loading branch information
Loïc Hoguin committed Oct 19, 2011
1 parent 97df2a2 commit fe5f0ca
Show file tree
Hide file tree
Showing 2 changed files with 28 additions and 11 deletions.
16 changes: 12 additions & 4 deletions src/cowboy_http_protocol.erl
Expand Up @@ -46,6 +46,7 @@
handler :: {module(), any()},
req_empty_lines = 0 :: integer(),
max_empty_lines :: integer(),
max_line_length :: integer(),
timeout :: timeout(),
buffer = <<>> :: binary(),
hibernate = false :: boolean(),
Expand All @@ -68,17 +69,22 @@ start_link(ListenerPid, Socket, Transport, Opts) ->
init(ListenerPid, Socket, Transport, Opts) ->
Dispatch = proplists:get_value(dispatch, Opts, []),
MaxEmptyLines = proplists:get_value(max_empty_lines, Opts, 5),
MaxLineLength = proplists:get_value(max_line_length, Opts, 4096),
Timeout = proplists:get_value(timeout, Opts, 5000),
receive shoot -> ok end,
wait_request(#state{listener=ListenerPid, socket=Socket, transport=Transport,
dispatch=Dispatch, max_empty_lines=MaxEmptyLines, timeout=Timeout}).
dispatch=Dispatch, max_empty_lines=MaxEmptyLines,
max_line_length=MaxLineLength, timeout=Timeout}).

%% @private
-spec parse_request(#state{}) -> ok | none().
%% @todo Use decode_packet options to limit length?
parse_request(State=#state{buffer=Buffer}) ->
%% We limit the length of the Request-line to MaxLength to avoid endlessly
%% reading from the socket and eventually crashing.
parse_request(State=#state{buffer=Buffer, max_line_length=MaxLength}) ->
case erlang:decode_packet(http_bin, Buffer, []) of
{ok, Request, Rest} -> request(Request, State#state{buffer=Rest});
{more, _Length} when byte_size(Buffer) > MaxLength ->
error_terminate(413, State);
{more, _Length} -> wait_request(State);
{error, _Reason} -> error_terminate(400, State)
end.
Expand Down Expand Up @@ -123,9 +129,11 @@ request({http_error, _Any}, State) ->
error_terminate(400, State).

-spec parse_header(#http_req{}, #state{}) -> ok | none().
parse_header(Req, State=#state{buffer=Buffer}) ->
parse_header(Req, State=#state{buffer=Buffer, max_line_length=MaxLength}) ->
case erlang:decode_packet(httph_bin, Buffer, []) of
{ok, Header, Rest} -> header(Header, Req, State#state{buffer=Rest});
{more, _Length} when byte_size(Buffer) > MaxLength ->
error_terminate(413, State);
{more, _Length} -> wait_header(Req, State);
{error, _Reason} -> error_terminate(400, State)
end.
Expand Down
23 changes: 16 additions & 7 deletions test/http_SUITE.erl
Expand Up @@ -19,7 +19,7 @@
-export([all/0, groups/0, init_per_suite/1, end_per_suite/1,
init_per_group/2, end_per_group/2]). %% ct.
-export([chunked_response/1, headers_dupe/1, headers_huge/1,
keepalive_nl/1, nc_rand/1, pipeline/1, raw/1,
keepalive_nl/1, nc_rand/1, nc_zero/1, pipeline/1, raw/1,
ws0/1, ws8/1, ws8_single_bytes/1, ws8_init_shutdown/1,
ws_timeout_hibernate/1]). %% http.
-export([http_200/1, http_404/1]). %% http and https.
Expand All @@ -33,7 +33,7 @@ all() ->
groups() ->
BaseTests = [http_200, http_404],
[{http, [], [chunked_response, headers_dupe, headers_huge,
keepalive_nl, nc_rand, pipeline, raw,
keepalive_nl, nc_rand, nc_zero, pipeline, raw,
ws0, ws8, ws8_single_bytes, ws8_init_shutdown,
ws_timeout_hibernate] ++ BaseTests},
{https, [], BaseTests}, {misc, [], [http_10_hostless]}].
Expand Down Expand Up @@ -126,7 +126,7 @@ headers_dupe(Config) ->

headers_huge(Config) ->
Cookie = lists:flatten(["whatever_man_biiiiiiiiiiiig_cookie_me_want_77="
"Wed Apr 06 2011 10:38:52 GMT-0500 (CDT)" || _N <- lists:seq(1, 1000)]),
"Wed Apr 06 2011 10:38:52 GMT-0500 (CDT)" || _N <- lists:seq(1, 40)]),
{_Packet, 200} = raw_req(["GET / HTTP/1.0\r\nHost: localhost\r\n"
"Set-Cookie: ", Cookie, "\r\n\r\n"], Config).

Expand All @@ -149,6 +149,12 @@ keepalive_nl_loop(Socket, N) ->
keepalive_nl_loop(Socket, N - 1).

nc_rand(Config) ->
nc_reqs(Config, "/dev/urandom").

nc_zero(Config) ->
nc_reqs(Config, "/dev/zero").

nc_reqs(Config, Input) ->
Cat = os:find_executable("cat"),
Nc = os:find_executable("nc"),
case {Cat, Nc} of
Expand All @@ -159,13 +165,13 @@ nc_rand(Config) ->
_Good ->
%% Throw garbage at the server then check if it's still up.
{port, Port} = lists:keyfind(port, 1, Config),
[nc_rand_run(Port) || _N <- lists:seq(1, 100)],
[nc_run_req(Port, Input) || _N <- lists:seq(1, 100)],
Packet = "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n",
{Packet, 200} = raw_req(Packet, Config)
end.

nc_rand_run(Port) ->
os:cmd("cat /dev/urandom | nc localhost " ++ integer_to_list(Port)).
nc_run_req(Port, Input) ->
os:cmd("cat " ++ Input ++ " | nc localhost " ++ integer_to_list(Port)).

pipeline(Config) ->
{port, Port} = lists:keyfind(port, 1, Config),
Expand Down Expand Up @@ -212,6 +218,7 @@ raw_req(Packet, Config) ->
{Packet, Res}.

raw(Config) ->
Huge = [$0 || _N <- lists:seq(1, 5000)],
Tests = [
{"\r\n\r\n\r\n\r\n\r\nGET / HTTP/1.1\r\nHost: localhost\r\n\r\n", 200},
{"\n", 400},
Expand All @@ -229,7 +236,9 @@ raw(Config) ->
{"GET http://localhost/ HTTP/1.1\r\n\r\n", 501},
{"GET / HTTP/1.2\r\nHost: localhost\r\n\r\n", 505},
{"GET /init_shutdown HTTP/1.1\r\nHost: localhost\r\n\r\n", 666},
{"GET /long_polling HTTP/1.1\r\nHost: localhost\r\n\r\n", 102}
{"GET /long_polling HTTP/1.1\r\nHost: localhost\r\n\r\n", 102},
{Huge, 413},
{"GET / HTTP/1.1\r\n" ++ Huge, 413}
],
[{Packet, StatusCode} = raw_req(Packet, Config)
|| {Packet, StatusCode} <- Tests].
Expand Down

0 comments on commit fe5f0ca

Please sign in to comment.