From 4b385749f2aab90b5c7e44e844159c0221a8790d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Fri, 7 Sep 2018 13:56:12 +0200 Subject: Add cowboy_req:read_and_match_urlencoded_body/2,3 --- doc/src/manual/cowboy_req.asciidoc | 1 + ...boy_req.read_and_match_urlencoded_body.asciidoc | 148 +++++++++++++++++++++ doc/src/manual/cowboy_req.read_body.asciidoc | 1 + doc/src/manual/cowboy_req.read_part.asciidoc | 1 + doc/src/manual/cowboy_req.read_part_body.asciidoc | 1 + .../cowboy_req.read_urlencoded_body.asciidoc | 1 + src/cowboy_req.erl | 20 ++- test/handlers/echo_h.erl | 20 ++- test/req_SUITE.erl | 27 ++++ 9 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 doc/src/manual/cowboy_req.read_and_match_urlencoded_body.asciidoc diff --git a/doc/src/manual/cowboy_req.asciidoc b/doc/src/manual/cowboy_req.asciidoc index 309aedf..2758e8f 100644 --- a/doc/src/manual/cowboy_req.asciidoc +++ b/doc/src/manual/cowboy_req.asciidoc @@ -66,6 +66,7 @@ Request body: * link:man:cowboy_req:body_length(3)[cowboy_req:body_length(3)] - Body length * link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)] - Read the request body * link:man:cowboy_req:read_urlencoded_body(3)[cowboy_req:read_urlencoded_body(3)] - Read and parse a urlencoded request body +* link:man:cowboy_req:read_and_match_urlencoded_body(3)[cowboy_req:read_and_match_urlencoded_body(3)] - Read, parse and match a urlencoded request body against constraints * link:man:cowboy_req:read_part(3)[cowboy_req:read_part(3)] - Read the next multipart headers * link:man:cowboy_req:read_part_body(3)[cowboy_req:read_part_body(3)] - Read the current part's body diff --git a/doc/src/manual/cowboy_req.read_and_match_urlencoded_body.asciidoc b/doc/src/manual/cowboy_req.read_and_match_urlencoded_body.asciidoc new file mode 100644 index 0000000..aa2704c --- /dev/null +++ b/doc/src/manual/cowboy_req.read_and_match_urlencoded_body.asciidoc @@ -0,0 +1,148 @@ += cowboy_req:read_and_match_urlencoded_body(3) + +== Name + +cowboy_req:read_and_match_urlencoded_body - Read, parse +and match a urlencoded request body against constraints + +== Description + +[source,erlang] +---- +read_and_match_urlencoded_body(Fields, Req) + -> read_and_match_urlencoded_body(Fields, Req, #{}) + +read_and_match_urlencoded_body(Fields, Req, Opts) + -> {ok, Body, Req} + +Fields :: cowboy:fields() +Req :: cowboy_req:req() +Opts :: cowboy_req:read_body_opts() +Body :: #{atom() => any()} +---- + +Read, parse and match a urlencoded request body against +constraints. + +This function reads the request body and parses it as +`application/x-www-form-urlencoded`. It then applies +the given field constraints to the urlencoded data +and returns the result as a map. + +The urlencoded media type is used by Web browsers when +submitting HTML forms using the POST method. + +Cowboy will only return the values specified +in the fields list, and ignore all others. Fields can be +either the key requested; the key along with a list of +constraints; or the key, a list of constraints and a +default value in case the key is missing. + +This function will crash if the key is missing and no +default value is provided. This function will also crash +if a constraint fails. + +The key must be provided as an atom. The key of the +returned map will be that atom. The value may be converted +through the use of constraints, making this function able +to extract, validate and convert values all in one step. + +Cowboy needs to read the full body before parsing. By default +it will read bodies of size up to 64KB. It is possible to +provide options to read larger bodies if required. + +Cowboy will automatically handle protocol details including +the expect header, chunked transfer-encoding and others. + +Once the body has been read, Cowboy sets the content-length +header if it was not previously provided. + +This function can only be called once. Calling it again will +result in undefined behavior. + +== Arguments + +Fields:: + +Fields to retrieve from the urlencoded body. ++ +See link:man:cowboy(3)[cowboy(3)] for a complete description. + +Req:: + +The Req object. + +Opts:: + +A map of body reading options. Please refer to +link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)] +for details about each option. ++ +This function defaults the `length` to 64KB and the `period` +to 5 seconds. + +== Return value + +An `ok` tuple is returned. + +Desired values are returned as a map. The key is the atom +that was given in the list of fields, and the value is the +optionally converted value after applying constraints. + +The map contains the same keys that were given in the fields. + +An exception is triggered when the match fails. + +The Req object returned in the tuple must be used from that point +onward. It contains a more up to date representation of the request. +For example it may have an added content-length header once the +body has been read. + +== Changelog + +* *2.5*: Function introduced. + +== Examples + +.Match fields +[source,erlang] +---- +%% ID and Lang are binaries. +#{id := ID, lang := Lang} + = cowboy_req:read_and_match_urlencoded_body( + [id, lang], Req). +---- + +.Match fields and apply constraints +[source,erlang] +---- +%% ID is an integer and Lang a non-empty binary. +#{id := ID, lang := Lang} + = cowboy_req:read_and_match_urlencoded_body( + [{id, int}, {lang, nonempty}], Req). +---- + +.Match fields with default values +[source,erlang] +---- +#{lang := Lang} + = cowboy_req:read_and_match_urlencoded_body( + [{lang, [], <<"en-US">>}], Req). +---- + +.Allow large urlencoded bodies +[source,erlang] +---- +{ok, Body, Req} = cowboy_req:read_and_match_urlencoded_body( + Fields, Req0, #{length => 1000000}). +---- + +== See also + +link:man:cowboy_req(3)[cowboy_req(3)], +link:man:cowboy_req:has_body(3)[cowboy_req:has_body(3)], +link:man:cowboy_req:body_length(3)[cowboy_req:body_length(3)], +link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)], +link:man:cowboy_req:read_urlencoded_body(3)[cowboy_req:read_urlencoded_body(3)], +link:man:cowboy_req:read_part(3)[cowboy_req:read_part(3)], +link:man:cowboy_req:read_part_body(3)[cowboy_req:read_part_body(3)] diff --git a/doc/src/manual/cowboy_req.read_body.asciidoc b/doc/src/manual/cowboy_req.read_body.asciidoc index 729b2f0..2b87405 100644 --- a/doc/src/manual/cowboy_req.read_body.asciidoc +++ b/doc/src/manual/cowboy_req.read_body.asciidoc @@ -112,5 +112,6 @@ link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:has_body(3)[cowboy_req:has_body(3)], link:man:cowboy_req:body_length(3)[cowboy_req:body_length(3)], link:man:cowboy_req:read_urlencoded_body(3)[cowboy_req:read_urlencoded_body(3)], +link:man:cowboy_req:read_and_match_urlencoded_body(3)[cowboy_req:read_and_match_urlencoded_body(3)], link:man:cowboy_req:read_part(3)[cowboy_req:read_part(3)], link:man:cowboy_req:read_part_body(3)[cowboy_req:read_part_body(3)] diff --git a/doc/src/manual/cowboy_req.read_part.asciidoc b/doc/src/manual/cowboy_req.read_part.asciidoc index ceed72a..9123b2e 100644 --- a/doc/src/manual/cowboy_req.read_part.asciidoc +++ b/doc/src/manual/cowboy_req.read_part.asciidoc @@ -131,4 +131,5 @@ link:man:cowboy_req:has_body(3)[cowboy_req:has_body(3)], link:man:cowboy_req:body_length(3)[cowboy_req:body_length(3)], link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)], link:man:cowboy_req:read_urlencoded_body(3)[cowboy_req:read_urlencoded_body(3)], +link:man:cowboy_req:read_and_match_urlencoded_body(3)[cowboy_req:read_and_match_urlencoded_body(3)], link:man:cowboy_req:read_part_body(3)[cowboy_req:read_part_body(3)] diff --git a/doc/src/manual/cowboy_req.read_part_body.asciidoc b/doc/src/manual/cowboy_req.read_part_body.asciidoc index 3a1af2f..dc634f3 100644 --- a/doc/src/manual/cowboy_req.read_part_body.asciidoc +++ b/doc/src/manual/cowboy_req.read_part_body.asciidoc @@ -97,4 +97,5 @@ link:man:cowboy_req:has_body(3)[cowboy_req:has_body(3)], link:man:cowboy_req:body_length(3)[cowboy_req:body_length(3)], link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)], link:man:cowboy_req:read_urlencoded_body(3)[cowboy_req:read_urlencoded_body(3)], +link:man:cowboy_req:read_and_match_urlencoded_body(3)[cowboy_req:read_and_match_urlencoded_body(3)], link:man:cowboy_req:read_part(3)[cowboy_req:read_part(3)] diff --git a/doc/src/manual/cowboy_req.read_urlencoded_body.asciidoc b/doc/src/manual/cowboy_req.read_urlencoded_body.asciidoc index 3b65fcf..e897a69 100644 --- a/doc/src/manual/cowboy_req.read_urlencoded_body.asciidoc +++ b/doc/src/manual/cowboy_req.read_urlencoded_body.asciidoc @@ -90,5 +90,6 @@ link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:has_body(3)[cowboy_req:has_body(3)], link:man:cowboy_req:body_length(3)[cowboy_req:body_length(3)], link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)], +link:man:cowboy_req:read_and_match_urlencoded_body(3)[cowboy_req:read_and_match_urlencoded_body(3)], link:man:cowboy_req:read_part(3)[cowboy_req:read_part(3)], link:man:cowboy_req:read_part_body(3)[cowboy_req:read_part_body(3)] diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl index ee21e98..ee40304 100644 --- a/src/cowboy_req.erl +++ b/src/cowboy_req.erl @@ -54,7 +54,8 @@ -export([read_body/2]). -export([read_urlencoded_body/1]). -export([read_urlencoded_body/2]). -%% @todo read_and_match_urlencoded_body? +-export([read_and_match_urlencoded_body/2]). +-export([read_and_match_urlencoded_body/3]). %% Multipart. -export([read_part/1]). @@ -513,6 +514,23 @@ read_urlencoded_body(Req0, Opts) -> end end. +-spec read_and_match_urlencoded_body(cowboy:fields(), Req) + -> {ok, map(), Req} when Req::req(). +read_and_match_urlencoded_body(Fields, Req) -> + read_and_match_urlencoded_body(Fields, Req, #{length => 64000, period => 5000}). + +-spec read_and_match_urlencoded_body(cowboy:fields(), Req, read_body_opts()) + -> {ok, map(), Req} when Req::req(). +read_and_match_urlencoded_body(Fields, Req0, Opts) -> + {ok, Qs, Req} = read_urlencoded_body(Req0, Opts), + case filter(Fields, kvlist_to_map(Fields, Qs)) of + {ok, Map} -> + {ok, Map, Req}; + {error, Errors} -> + exit({request_error, {read_and_match_urlencoded_body, Errors}, + 'Urlencoded request body validation constraints failed for the reasons provided.'}) + end. + %% Multipart. -spec read_part(Req) diff --git a/test/handlers/echo_h.erl b/test/handlers/echo_h.erl index a116442..ec37a66 100644 --- a/test/handlers/echo_h.erl +++ b/test/handlers/echo_h.erl @@ -46,6 +46,19 @@ echo(<<"read_urlencoded_body">>, Req0, Opts) -> _ -> cowboy_req:read_urlencoded_body(Req0) end, {ok, cowboy_req:reply(200, #{}, value_to_iodata(Body), Req), Opts}; +echo(<<"read_and_match_urlencoded_body">>, Req0, Opts) -> + Path = cowboy_req:path(Req0), + case {Path, Opts} of + {<<"/opts", _/bits>>, #{crash := true}} -> ct_helper:ignore(cowboy_req, read_body, 2); + {_, #{crash := true}} -> ct_helper:ignore(cowboy_req, read_urlencoded_body, 2); + _ -> ok + end, + {ok, Body, Req} = case Path of + <<"/opts", _/bits>> -> cowboy_req:read_and_match_urlencoded_body([], Req0, Opts); + <<"/crash", _/bits>> -> cowboy_req:read_and_match_urlencoded_body([], Req0, Opts); + _ -> cowboy_req:read_and_match_urlencoded_body([], Req0) + end, + {ok, cowboy_req:reply(200, #{}, value_to_iodata(Body), Req), Opts}; echo(<<"uri">>, Req, Opts) -> Value = case cowboy_req:path_info(Req) of [<<"origin">>] -> cowboy_req:uri(Req, #{host => undefined}); @@ -61,7 +74,12 @@ echo(<<"match">>, Req, Opts) -> Fields = [binary_to_atom(F, latin1) || F <- Fields0], Value = case Type of <<"qs">> -> cowboy_req:match_qs(Fields, Req); - <<"cookies">> -> cowboy_req:match_cookies(Fields, Req) + <<"cookies">> -> cowboy_req:match_cookies(Fields, Req); + <<"body_qs">> -> + %% Note that the Req should not be discarded but for the + %% purpose of this test this has no ill impacts. + {ok, Match, _} = cowboy_req:read_and_match_urlencoded_body(Fields, Req), + Match end, {ok, cowboy_req:reply(200, #{}, value_to_iodata(Value), Req), Opts}; echo(What, Req, Opts) -> diff --git a/test/req_SUITE.erl b/test/req_SUITE.erl index 9042f54..6ca4521 100644 --- a/test/req_SUITE.erl +++ b/test/req_SUITE.erl @@ -564,6 +564,33 @@ do_read_urlencoded_body_too_long(Path, Body, Config) -> end, gun:close(ConnPid). +read_and_match_urlencoded_body(Config) -> + doc("Read and match an application/x-www-form-urlencoded request body."), + <<"#{}">> = do_body("POST", "/match/body_qs", [], "a=b&c=d", Config), + <<"#{a => <<\"b\">>}">> = do_body("POST", "/match/body_qs/a", [], "a=b&c=d", Config), + <<"#{c => <<\"d\">>}">> = do_body("POST", "/match/body_qs/c", [], "a=b&c=d", Config), + <<"#{a => <<\"b\">>,c => <<\"d\">>}">> + = do_body("POST", "/match/body_qs/a/c", [], "a=b&c=d", Config), + <<"#{a => <<\"b\">>,c => true}">> = do_body("POST", "/match/body_qs/a/c", [], "a=b&c", Config), + <<"#{a => true,c => <<\"d\">>}">> = do_body("POST", "/match/body_qs/a/c", [], "a&c=d", Config), + %% Ensure match errors result in a 400 response. + {400, _} = do_body_error("POST", "/match/body_qs/a/c", [], "a=b", Config), + %% Ensure parse errors result in a 400 response. + {400, _} = do_body_error("POST", "/match/body_qs", [], "%%%%%", Config), + %% Send a 10MB body, larger than the default length, to ensure a crash occurs. + ok = do_read_urlencoded_body_too_large( + "/no-opts/read_and_match_urlencoded_body", + string:chars($a, 10000000), Config), + %% We read any length for at most 1 second. + %% + %% The body is sent twice, first with nofin, then wait 1.1 second, then again with fin. + %% We expect the handler to crash because read_and_match_urlencoded_body expects the full body. + ok = do_read_urlencoded_body_too_long( + "/crash/read_and_match_urlencoded_body/period", <<"abc">>, Config), + %% The timeout value is set too low on purpose to ensure a crash occurs. + ok = do_read_body_timeout("/opts/read_and_match_urlencoded_body/timeout", <<"abc">>, Config), + ok. + multipart(Config) -> doc("Multipart request body."), do_multipart("/multipart", Config). -- cgit v1.2.3