Browse Source

Merge branch 'check-body-length' of git://github.com/rambocoder/cowboy

Loïc Hoguin 12 years ago
parent
commit
fddd4a77fb
5 changed files with 168 additions and 17 deletions
  1. 16 5
      guide/req.md
  2. 41 8
      src/cowboy_req.erl
  3. 42 0
      test/http_SUITE.erl
  4. 39 0
      test/http_handler_body_qs.erl
  5. 30 4
      test/http_handler_echo_body.erl

+ 16 - 5
guide/req.md

@@ -91,11 +91,22 @@ was passed in the request, then Cowboy will return a size
 of `undefined`, as it has no way of knowing it.
 of `undefined`, as it has no way of knowing it.
 
 
 If you know the request contains a body, and that it is
 If you know the request contains a body, and that it is
-of appropriate size, then you can read it directly with
-either `body/1` or `body_qs/1`. Otherwise, you will want
-to stream it with `stream_body/1` and `skip_body/1`, with
-the streaming process optionally initialized using `init_stream/4`
-or `init_stream/5`.
+within 8MB (for `body/1`) or 16KB (for `body_qs/1`) bytes,
+then you can read it directly with either `body/1` or `body_qs/1`.
+If you want to override the default size limits of `body/1`
+or `body_qs/1`, you can pass the maximum body length byte
+size as first parameter to `body/2` and `body_qs/2` or pass
+atom `infinity` to ignore size limits.
+
+If the request contains bigger body than allowed default sizes
+or supplied maximum body length, `body/1`, `body/2`, `body_qs/1`
+and `body_qs/2` will return `{error, badlength}`. If the request
+contains chunked body, `body/1`, `body/2`, `body_qs/1`
+and `body_qs/2` will return `{error, chunked}`.
+If you get either of the above two errors, you will want to
+handle the body of the request using `stream_body/1` and
+`skip_body/1`, with the streaming process optionally
+initialized using `init_stream/4` or `init_stream/5`.
 
 
 Multipart request body
 Multipart request body
 ----------------------
 ----------------------

+ 41 - 8
src/cowboy_req.erl

@@ -82,7 +82,9 @@
 -export([stream_body/1]).
 -export([stream_body/1]).
 -export([skip_body/1]).
 -export([skip_body/1]).
 -export([body/1]).
 -export([body/1]).
+-export([body/2]).
 -export([body_qs/1]).
 -export([body_qs/1]).
+-export([body_qs/2]).
 -export([multipart_data/1]).
 -export([multipart_data/1]).
 -export([multipart_skip/1]).
 -export([multipart_skip/1]).
 
 
@@ -729,17 +731,40 @@ content_decode(ContentDecode, Data, Req) ->
 		{error, Reason} -> {error, Reason}
 		{error, Reason} -> {error, Reason}
 	end.
 	end.
 
 
-%% @doc Return the full body sent with the request.
+%% @equiv body(8000000, Req)
 -spec body(Req) -> {ok, binary(), Req} | {error, atom()} when Req::req().
 -spec body(Req) -> {ok, binary(), Req} | {error, atom()} when Req::req().
 body(Req) ->
 body(Req) ->
-	body(Req, <<>>).
+	body(8000000, Req).
 
 
--spec body(Req, binary())
+%% @doc Return the body sent with the request.
+-spec body(non_neg_integer() | infinity, Req)
 	-> {ok, binary(), Req} | {error, atom()} when Req::req().
 	-> {ok, binary(), Req} | {error, atom()} when Req::req().
-body(Req, Acc) ->
+body(infinity, Req) ->
+	case parse_header(<<"transfer-encoding">>, Req) of
+		{ok, [<<"identity">>], Req2} ->
+			read_body(Req2, <<>>);
+		{ok, _, _} ->
+			{error, chunked}
+	end;
+body(MaxBodyLength, Req) ->
+	case parse_header(<<"transfer-encoding">>, Req) of
+		{ok, [<<"identity">>], Req2} ->
+			{ok, Length, Req3} = parse_header(<<"content-length">>, Req2, 0),
+			if 	Length > MaxBodyLength ->
+					{error, badlength};
+				true ->
+					read_body(Req3, <<>>)
+			end;
+		{ok, _, _} ->
+			{error, chunked}
+	end.
+
+-spec read_body(Req, binary())
+	-> {ok, binary(), Req} | {error, atom()} when Req::req().
+read_body(Req, Acc) ->
 	case stream_body(Req) of
 	case stream_body(Req) of
 		{ok, Data, Req2} ->
 		{ok, Data, Req2} ->
-			body(Req2, << Acc/binary, Data/binary >>);
+			read_body(Req2, << Acc/binary, Data/binary >>);
 		{done, Req2} ->
 		{done, Req2} ->
 			{ok, Acc, Req2};
 			{ok, Acc, Req2};
 		{error, Reason} ->
 		{error, Reason} ->
@@ -754,13 +779,21 @@ skip_body(Req) ->
 		{error, Reason} -> {error, Reason}
 		{error, Reason} -> {error, Reason}
 	end.
 	end.
 
 
-%% @doc Return the full body sent with the request, parsed as an
-%% application/x-www-form-urlencoded string. Essentially a POST query string.
+%% @equiv body_qs(16000, Req)
 -spec body_qs(Req)
 -spec body_qs(Req)
 	-> {ok, [{binary(), binary() | true}], Req} | {error, atom()}
 	-> {ok, [{binary(), binary() | true}], Req} | {error, atom()}
 	when Req::req().
 	when Req::req().
 body_qs(Req) ->
 body_qs(Req) ->
-	case body(Req) of
+	body_qs(16000, Req).
+
+%% @doc Return the body sent with the request, parsed as an
+%% application/x-www-form-urlencoded string.
+%% Essentially a POST query string.
+-spec body_qs(non_neg_integer() | infinity, Req)
+	-> {ok, [{binary(), binary() | true}], Req} | {error, atom()}
+	when Req::req().
+body_qs(MaxBodyLength, Req) ->
+	case body(MaxBodyLength, Req) of
 		{ok, Body, Req2} ->
 		{ok, Body, Req2} ->
 			{ok, cowboy_http:x_www_form_urlencoded(Body), Req2};
 			{ok, cowboy_http:x_www_form_urlencoded(Body), Req2};
 		{error, Reason} ->
 		{error, Reason} ->

+ 42 - 0
test/http_SUITE.erl

@@ -30,6 +30,9 @@
 -export([check_status/1]).
 -export([check_status/1]).
 -export([chunked_response/1]).
 -export([chunked_response/1]).
 -export([echo_body/1]).
 -export([echo_body/1]).
+-export([echo_body_max_length/1]).
+-export([echo_body_qs/1]).
+-export([echo_body_qs_max_length/1]).
 -export([error_chain_handle_after_reply/1]).
 -export([error_chain_handle_after_reply/1]).
 -export([error_chain_handle_before_reply/1]).
 -export([error_chain_handle_before_reply/1]).
 -export([error_handle_after_reply/1]).
 -export([error_handle_after_reply/1]).
@@ -102,6 +105,9 @@ groups() ->
 		check_status,
 		check_status,
 		chunked_response,
 		chunked_response,
 		echo_body,
 		echo_body,
+		echo_body_max_length,
+		echo_body_qs,
+		echo_body_qs_max_length,
 		error_chain_handle_after_reply,
 		error_chain_handle_after_reply,
 		error_chain_handle_before_reply,
 		error_chain_handle_before_reply,
 		error_handle_after_reply,
 		error_handle_after_reply,
@@ -348,6 +354,7 @@ init_dispatch(Config) ->
 				 {file, <<"test_file.css">>}]},
 				 {file, <<"test_file.css">>}]},
 			{"/multipart", http_handler_multipart, []},
 			{"/multipart", http_handler_multipart, []},
 			{"/echo/body", http_handler_echo_body, []},
 			{"/echo/body", http_handler_echo_body, []},
+			{"/echo/body_qs", http_handler_body_qs, []},
 			{"/param_all", rest_param_all, []},
 			{"/param_all", rest_param_all, []},
 			{"/bad_accept", rest_simple_resource, []},
 			{"/bad_accept", rest_simple_resource, []},
 			{"/simple", rest_simple_resource, []},
 			{"/simple", rest_simple_resource, []},
@@ -533,6 +540,41 @@ echo_body(Config) ->
 		{ok, Body, _} = cowboy_client:response_body(Client3)
 		{ok, Body, _} = cowboy_client:response_body(Client3)
 	end || Size <- lists:seq(MTU - 500, MTU)].
 	end || Size <- lists:seq(MTU - 500, MTU)].
 
 
+%% Check if sending request whose size is bigger than 1000000 bytes causes 413
+echo_body_max_length(Config) ->
+	Client = ?config(client, Config),
+	Body = <<$a:8000008>>,
+	{ok, Client2} = cowboy_client:request(<<"POST">>,
+		build_url("/echo/body", Config),
+		[{<<"connection">>, <<"close">>}],
+		Body, Client),
+	{ok, 413, _, _} = cowboy_client:response(Client2).
+
+% check if body_qs echo's back results
+echo_body_qs(Config) ->
+	Client = ?config(client, Config),
+	Body = <<"echo=67890">>,
+	{ok, Client2} = cowboy_client:request(<<"POST">>,
+		build_url("/echo/body_qs", Config),
+		[{<<"connection">>, <<"close">>}],
+		Body, Client),
+	{ok, 200, _, Client3} = cowboy_client:response(Client2),
+	{ok, <<"67890">>, _} = cowboy_client:response_body(Client3).
+
+%% Check if sending request whose size is bigger 16000 bytes causes 413
+echo_body_qs_max_length(Config) ->
+	Client = ?config(client, Config),
+	DefaultMaxBodyQsLength = 16000,
+	% subtract "echo=" minus 1 byte from max to hit the limit
+	Bits = (DefaultMaxBodyQsLength - 4) * 8,
+	AppendedBody = <<$a:Bits>>,
+	Body = <<"echo=", AppendedBody/binary>>,
+	{ok, Client2} = cowboy_client:request(<<"POST">>,
+		build_url("/echo/body_qs", Config),
+		[{<<"connection">>, <<"close">>}],
+		Body, Client),
+	{ok, 413, _, _} = cowboy_client:response(Client2).
+
 error_chain_handle_after_reply(Config) ->
 error_chain_handle_after_reply(Config) ->
 	Client = ?config(client, Config),
 	Client = ?config(client, Config),
 	{ok, Client2} = cowboy_client:request(<<"GET">>,
 	{ok, Client2} = cowboy_client:request(<<"GET">>,

+ 39 - 0
test/http_handler_body_qs.erl

@@ -0,0 +1,39 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+-module(http_handler_body_qs).
+-behaviour(cowboy_http_handler).
+-export([init/3, handle/2, terminate/3]).
+
+init({_, http}, Req, _) ->
+	{ok, Req, undefined}.
+
+handle(Req, State) ->
+	{Method, Req2} = cowboy_req:method(Req),
+	HasBody = cowboy_req:has_body(Req2),
+	{ok, Req3} = maybe_echo(Method, HasBody, Req2),
+	{ok, Req3, State}.
+
+maybe_echo(<<"POST">>, true, Req) ->
+	case cowboy_req:body_qs(Req) of
+		{error,badlength} ->
+			echo(badlength, Req);
+		{ok, PostVals, Req2} ->
+			echo(proplists:get_value(<<"echo">>, PostVals), Req2)
+	end;
+
+maybe_echo(<<"POST">>, false, Req) ->
+	cowboy_req:reply(400, [], <<"Missing body.">>, Req);
+maybe_echo(_, _, Req) ->
+	%% Method not allowed.
+	cowboy_req:reply(405, Req).
+
+echo(badlength, Req) ->
+	cowboy_req:reply(413, [], <<"POST body bigger than 16000 bytes">>, Req);
+echo(undefined, Req) ->
+	cowboy_req:reply(400, [], <<"Missing echo parameter.">>, Req);
+echo(Echo, Req) ->
+	cowboy_req:reply(200,
+		[{<<"content-encoding">>, <<"utf-8">>}], Echo, Req).
+
+terminate(_, _, _) ->
+	ok.

+ 30 - 4
test/http_handler_echo_body.erl

@@ -9,11 +9,37 @@ init({_, http}, Req, _) ->
 
 
 handle(Req, State) ->
 handle(Req, State) ->
 	true = cowboy_req:has_body(Req),
 	true = cowboy_req:has_body(Req),
-	{ok, Body, Req2} = cowboy_req:body(Req),
-	{Size, Req3} = cowboy_req:body_length(Req2),
+	{ok, Req3} = case cowboy_req:body(1000000, Req) of
+		{error, chunked} -> handle_chunked(Req);
+		{error, badlength} -> handle_badlength(Req);
+		{ok, Body, Req2} -> handle_body(Req2, Body)
+	end,
+	{ok, Req3, State}.
+
+handle_chunked(Req) ->
+	{ok, Data, Req2} = read_body(Req, <<>>, 1000000),
+	{ok, Req3} = cowboy_req:reply(200, [], Data, Req2),
+	{ok, Req3}.
+
+handle_badlength(Req) ->
+	{ok, Req2} = cowboy_req:reply(413, [], <<"Request entity too large">>, Req),
+	{ok, Req2}.
+
+handle_body(Req, Body) ->
+	{Size, Req2} = cowboy_req:body_length(Req),
 	Size = byte_size(Body),
 	Size = byte_size(Body),
-	{ok, Req4} = cowboy_req:reply(200, [], Body, Req3),
-	{ok, Req4, State}.
+	{ok, Req3} = cowboy_req:reply(200, [], Body, Req2),
+	{ok, Req3}.
 
 
 terminate(_, _, _) ->
 terminate(_, _, _) ->
 	ok.
 	ok.
+
+% Read chunked request content
+read_body(Req, Acc, BodyLengthRemaining) ->
+	case cowboy_req:stream_body(Req) of
+		{ok, Data, Req2} ->
+			BodyLengthRem = BodyLengthRemaining - byte_size(Data),
+			read_body(Req2, << Acc/binary, Data/binary >>, BodyLengthRem);
+		{done, Req2} ->
+			{ok, Acc, Req2}
+	end.