Browse Source

Add a max_line_length to the HTTP protocol

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.
Loïc Hoguin 13 years ago
parent
commit
fe5f0ca539
2 changed files with 28 additions and 11 deletions
  1. 12 4
      src/cowboy_http_protocol.erl
  2. 16 7
      test/http_SUITE.erl

+ 12 - 4
src/cowboy_http_protocol.erl

@@ -46,6 +46,7 @@
 	handler :: {module(), any()},
 	handler :: {module(), any()},
 	req_empty_lines = 0 :: integer(),
 	req_empty_lines = 0 :: integer(),
 	max_empty_lines :: integer(),
 	max_empty_lines :: integer(),
+	max_line_length :: integer(),
 	timeout :: timeout(),
 	timeout :: timeout(),
 	buffer = <<>> :: binary(),
 	buffer = <<>> :: binary(),
 	hibernate = false :: boolean(),
 	hibernate = false :: boolean(),
@@ -68,17 +69,22 @@ start_link(ListenerPid, Socket, Transport, Opts) ->
 init(ListenerPid, Socket, Transport, Opts) ->
 init(ListenerPid, Socket, Transport, Opts) ->
 	Dispatch = proplists:get_value(dispatch, Opts, []),
 	Dispatch = proplists:get_value(dispatch, Opts, []),
 	MaxEmptyLines = proplists:get_value(max_empty_lines, Opts, 5),
 	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),
 	Timeout = proplists:get_value(timeout, Opts, 5000),
 	receive shoot -> ok end,
 	receive shoot -> ok end,
 	wait_request(#state{listener=ListenerPid, socket=Socket, transport=Transport,
 	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
 %% @private
 -spec parse_request(#state{}) -> ok | none().
 -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
 	case erlang:decode_packet(http_bin, Buffer, []) of
 		{ok, Request, Rest} -> request(Request, State#state{buffer=Rest});
 		{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);
 		{more, _Length} -> wait_request(State);
 		{error, _Reason} -> error_terminate(400, State)
 		{error, _Reason} -> error_terminate(400, State)
 	end.
 	end.
@@ -123,9 +129,11 @@ request({http_error, _Any}, State) ->
 	error_terminate(400, State).
 	error_terminate(400, State).
 
 
 -spec parse_header(#http_req{}, #state{}) -> ok | none().
 -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
 	case erlang:decode_packet(httph_bin, Buffer, []) of
 		{ok, Header, Rest} -> header(Header, Req, State#state{buffer=Rest});
 		{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);
 		{more, _Length} -> wait_header(Req, State);
 		{error, _Reason} -> error_terminate(400, State)
 		{error, _Reason} -> error_terminate(400, State)
 	end.
 	end.

+ 16 - 7
test/http_SUITE.erl

@@ -19,7 +19,7 @@
 -export([all/0, groups/0, init_per_suite/1, end_per_suite/1,
 -export([all/0, groups/0, init_per_suite/1, end_per_suite/1,
 	init_per_group/2, end_per_group/2]). %% ct.
 	init_per_group/2, end_per_group/2]). %% ct.
 -export([chunked_response/1, headers_dupe/1, headers_huge/1,
 -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,
 	ws0/1, ws8/1, ws8_single_bytes/1, ws8_init_shutdown/1,
 	ws_timeout_hibernate/1]). %% http.
 	ws_timeout_hibernate/1]). %% http.
 -export([http_200/1, http_404/1]). %% http and https.
 -export([http_200/1, http_404/1]). %% http and https.
@@ -33,7 +33,7 @@ all() ->
 groups() ->
 groups() ->
 	BaseTests = [http_200, http_404],
 	BaseTests = [http_200, http_404],
 	[{http, [], [chunked_response, headers_dupe, headers_huge,
 	[{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,
 		ws0, ws8, ws8_single_bytes, ws8_init_shutdown,
 		ws_timeout_hibernate] ++ BaseTests},
 		ws_timeout_hibernate] ++ BaseTests},
 	{https, [], BaseTests}, {misc, [], [http_10_hostless]}].
 	{https, [], BaseTests}, {misc, [], [http_10_hostless]}].
@@ -126,7 +126,7 @@ headers_dupe(Config) ->
 
 
 headers_huge(Config) ->
 headers_huge(Config) ->
 	Cookie = lists:flatten(["whatever_man_biiiiiiiiiiiig_cookie_me_want_77="
 	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"
 	{_Packet, 200} = raw_req(["GET / HTTP/1.0\r\nHost: localhost\r\n"
 		"Set-Cookie: ", Cookie, "\r\n\r\n"], Config).
 		"Set-Cookie: ", Cookie, "\r\n\r\n"], Config).
 
 
@@ -149,6 +149,12 @@ keepalive_nl_loop(Socket, N) ->
 	keepalive_nl_loop(Socket, N - 1).
 	keepalive_nl_loop(Socket, N - 1).
 
 
 nc_rand(Config) ->
 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"),
 	Cat = os:find_executable("cat"),
 	Nc = os:find_executable("nc"),
 	Nc = os:find_executable("nc"),
 	case {Cat, Nc} of
 	case {Cat, Nc} of
@@ -159,13 +165,13 @@ nc_rand(Config) ->
 		_Good ->
 		_Good ->
 			%% Throw garbage at the server then check if it's still up.
 			%% Throw garbage at the server then check if it's still up.
 			{port, Port} = lists:keyfind(port, 1, Config),
 			{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 = "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n",
 			{Packet, 200} = raw_req(Packet, Config)
 			{Packet, 200} = raw_req(Packet, Config)
 	end.
 	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) ->
 pipeline(Config) ->
 	{port, Port} = lists:keyfind(port, 1, Config),
 	{port, Port} = lists:keyfind(port, 1, Config),
@@ -212,6 +218,7 @@ raw_req(Packet, Config) ->
 	{Packet, Res}.
 	{Packet, Res}.
 
 
 raw(Config) ->
 raw(Config) ->
+	Huge = [$0 || _N <- lists:seq(1, 5000)],
 	Tests = [
 	Tests = [
 		{"\r\n\r\n\r\n\r\n\r\nGET / HTTP/1.1\r\nHost: localhost\r\n\r\n", 200},
 		{"\r\n\r\n\r\n\r\n\r\nGET / HTTP/1.1\r\nHost: localhost\r\n\r\n", 200},
 		{"\n", 400},
 		{"\n", 400},
@@ -229,7 +236,9 @@ raw(Config) ->
 		{"GET http://localhost/ HTTP/1.1\r\n\r\n", 501},
 		{"GET http://localhost/ HTTP/1.1\r\n\r\n", 501},
 		{"GET / HTTP/1.2\r\nHost: localhost\r\n\r\n", 505},
 		{"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 /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} = raw_req(Packet, Config)
 		|| {Packet, StatusCode} <- Tests].
 		|| {Packet, StatusCode} <- Tests].