Browse Source

Add cowboy_req:read_and_match_urlencoded_body/2,3

Loïc Hoguin 6 years ago
parent
commit
4b385749f2

+ 1 - 0
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
 

+ 148 - 0
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)]

+ 1 - 0
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)]

+ 1 - 0
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)]

+ 1 - 0
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)]

+ 1 - 0
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)]

+ 19 - 1
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)

+ 19 - 1
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) ->

+ 27 - 0
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).