diff options
-rw-r--r-- | guide/req.md | 21 | ||||
-rw-r--r-- | src/cowboy_req.erl | 49 | ||||
-rw-r--r-- | test/http_SUITE.erl | 42 | ||||
-rw-r--r-- | test/http_handler_body_qs.erl | 39 | ||||
-rw-r--r-- | test/http_handler_echo_body.erl | 34 |
5 files changed, 168 insertions, 17 deletions
diff --git a/guide/req.md b/guide/req.md index 8fd854a..f627737 100644 --- a/guide/req.md +++ b/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. 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 ---------------------- diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl index 93c8656..966e463 100644 --- a/src/cowboy_req.erl +++ b/src/cowboy_req.erl @@ -82,7 +82,9 @@ -export([stream_body/1]). -export([skip_body/1]). -export([body/1]). +-export([body/2]). -export([body_qs/1]). +-export([body_qs/2]). -export([multipart_data/1]). -export([multipart_skip/1]). @@ -729,17 +731,40 @@ content_decode(ContentDecode, Data, Req) -> {error, Reason} -> {error, Reason} 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(). 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(). -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 {ok, Data, Req2} -> - body(Req2, << Acc/binary, Data/binary >>); + read_body(Req2, << Acc/binary, Data/binary >>); {done, Req2} -> {ok, Acc, Req2}; {error, Reason} -> @@ -754,13 +779,21 @@ skip_body(Req) -> {error, Reason} -> {error, Reason} 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) -> {ok, [{binary(), binary() | true}], Req} | {error, atom()} when Req::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, cowboy_http:x_www_form_urlencoded(Body), Req2}; {error, Reason} -> diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl index bd76f00..911efb8 100644 --- a/test/http_SUITE.erl +++ b/test/http_SUITE.erl @@ -30,6 +30,9 @@ -export([check_status/1]). -export([chunked_response/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_before_reply/1]). -export([error_handle_after_reply/1]). @@ -102,6 +105,9 @@ groups() -> check_status, chunked_response, echo_body, + echo_body_max_length, + echo_body_qs, + echo_body_qs_max_length, error_chain_handle_after_reply, error_chain_handle_before_reply, error_handle_after_reply, @@ -348,6 +354,7 @@ init_dispatch(Config) -> {file, <<"test_file.css">>}]}, {"/multipart", http_handler_multipart, []}, {"/echo/body", http_handler_echo_body, []}, + {"/echo/body_qs", http_handler_body_qs, []}, {"/param_all", rest_param_all, []}, {"/bad_accept", rest_simple_resource, []}, {"/simple", rest_simple_resource, []}, @@ -533,6 +540,41 @@ echo_body(Config) -> {ok, Body, _} = cowboy_client:response_body(Client3) 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) -> Client = ?config(client, Config), {ok, Client2} = cowboy_client:request(<<"GET">>, diff --git a/test/http_handler_body_qs.erl b/test/http_handler_body_qs.erl new file mode 100644 index 0000000..306f4dc --- /dev/null +++ b/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. diff --git a/test/http_handler_echo_body.erl b/test/http_handler_echo_body.erl index 31595d5..4b9e765 100644 --- a/test/http_handler_echo_body.erl +++ b/test/http_handler_echo_body.erl @@ -9,11 +9,37 @@ init({_, http}, Req, _) -> handle(Req, State) -> 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), - {ok, Req4} = cowboy_req:reply(200, [], Body, Req3), - {ok, Req4, State}. + {ok, Req3} = cowboy_req:reply(200, [], Body, Req2), + {ok, Req3}. terminate(_, _, _) -> 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. |