aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/src/manual/cowboy_req.asciidoc1
-rw-r--r--doc/src/manual/cowboy_req.filter_cookies.asciidoc70
-rw-r--r--src/cowboy_req.erl30
-rw-r--r--test/handlers/echo_h.erl4
-rw-r--r--test/req_SUITE.erl21
5 files changed, 126 insertions, 0 deletions
diff --git a/doc/src/manual/cowboy_req.asciidoc b/doc/src/manual/cowboy_req.asciidoc
index ca99f9f..f6f4127 100644
--- a/doc/src/manual/cowboy_req.asciidoc
+++ b/doc/src/manual/cowboy_req.asciidoc
@@ -53,6 +53,7 @@ Processed request:
* link:man:cowboy_req:parse_qs(3)[cowboy_req:parse_qs(3)] - Parse the query string
* link:man:cowboy_req:match_qs(3)[cowboy_req:match_qs(3)] - Match the query string against constraints
* link:man:cowboy_req:parse_header(3)[cowboy_req:parse_header(3)] - Parse the given HTTP header
+* link:man:cowboy_req:filter_cookies(3)[cowboy_req:filter_cookies(3)] - Filter cookie headers
* link:man:cowboy_req:parse_cookies(3)[cowboy_req:parse_cookies(3)] - Parse cookie headers
* link:man:cowboy_req:match_cookies(3)[cowboy_req:match_cookies(3)] - Match cookies against constraints
* link:man:cowboy_req:binding(3)[cowboy_req:binding(3)] - Access a value bound from the route
diff --git a/doc/src/manual/cowboy_req.filter_cookies.asciidoc b/doc/src/manual/cowboy_req.filter_cookies.asciidoc
new file mode 100644
index 0000000..20d0a0c
--- /dev/null
+++ b/doc/src/manual/cowboy_req.filter_cookies.asciidoc
@@ -0,0 +1,70 @@
+= cowboy_req:filter_cookies(3)
+
+== Name
+
+cowboy_req:filter_cookies - Filter cookie headers
+
+== Description
+
+[source,erlang]
+----
+filter_cookies(Names, Req) -> Req
+
+Names :: [atom() | binary()]
+----
+
+Filter cookie headers.
+
+This function is meant to be used before attempting to parse
+or match cookies in order to remove cookies that are not
+relevant and are potentially malformed. Because Cowboy by
+default crashes on malformed cookies, this function allows
+processing requests that would otherwise result in a 400
+error.
+
+Malformed cookies are unfortunately fairly common due to
+the string-based interface provided by browsers and this
+function provides a middle ground between Cowboy's strict
+behavior and chaotic real world use cases.
+
+Note that there may still be crashes even after filtering
+cookies because this function does not correct malformed
+values. Cookies that have malformed values should probably
+be unset in an error response or in a redirect.
+
+This function can be called even if there are no cookies
+in the request.
+
+== Arguments
+
+Names::
+
+The cookies that should be kept.
+
+Req::
+
+The Req object.
+
+== Return value
+
+The Req object is returned with its cookie header value
+filtered.
+
+== Changelog
+
+* *2.7*: Function introduced.
+
+== Examples
+
+.Filter then parse cookies
+[source,erlang]
+----
+Req = cowboy_req:filter_cookies([session_id, token], Req0),
+Cookies = cowboy_req:parse_cookies(Req).
+----
+
+== See also
+
+link:man:cowboy_req(3)[cowboy_req(3)],
+link:man:cowboy_req:parse_cookies(3)[cowboy_req:parse_cookies(3)],
+link:man:cowboy_req:match_cookies(3)[cowboy_req:match_cookies(3)]
diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl
index e5ec4a7..8ae090e 100644
--- a/src/cowboy_req.erl
+++ b/src/cowboy_req.erl
@@ -44,6 +44,7 @@
-export([headers/1]).
-export([parse_header/2]).
-export([parse_header/3]).
+-export([filter_cookies/2]).
-export([parse_cookies/1]).
-export([match_cookies/2]).
@@ -450,6 +451,35 @@ parse_header(Name, Req, Default, ParseFun) ->
Value -> ParseFun(Value)
end.
+-spec filter_cookies([atom() | binary()], Req) -> Req when Req::req().
+filter_cookies(Names0, Req=#{headers := Headers}) ->
+ Names = [if
+ is_atom(N) -> atom_to_binary(N, utf8);
+ true -> N
+ end || N <- Names0],
+ case header(<<"cookie">>, Req) of
+ undefined -> Req;
+ Value0 ->
+ Cookies0 = binary:split(Value0, <<$;>>),
+ Cookies = lists:filter(fun(Cookie) ->
+ lists:member(cookie_name(Cookie), Names)
+ end, Cookies0),
+ Value = iolist_to_binary(lists:join($;, Cookies)),
+ Req#{headers => Headers#{<<"cookie">> => Value}}
+ end.
+
+%% This is a specialized function to extract a cookie name
+%% regardless of whether the name is valid or not. We skip
+%% whitespace at the beginning and take whatever's left to
+%% be the cookie name, up to the = sign.
+cookie_name(<<$\s, Rest/binary>>) -> cookie_name(Rest);
+cookie_name(<<$\t, Rest/binary>>) -> cookie_name(Rest);
+cookie_name(Name) -> cookie_name(Name, <<>>).
+
+cookie_name(<<>>, Name) -> Name;
+cookie_name(<<$=, _/bits>>, Name) -> Name;
+cookie_name(<<C, Rest/bits>>, Acc) -> cookie_name(Rest, <<Acc/binary, C>>).
+
-spec parse_cookies(req()) -> [{binary(), binary()}].
parse_cookies(Req) ->
parse_header(<<"cookie">>, Req).
diff --git a/test/handlers/echo_h.erl b/test/handlers/echo_h.erl
index 7d0e75b..1b672d1 100644
--- a/test/handlers/echo_h.erl
+++ b/test/handlers/echo_h.erl
@@ -92,6 +92,10 @@ echo(<<"match">>, Req, Opts) ->
Match
end,
{ok, cowboy_req:reply(200, #{}, value_to_iodata(Value), Req), Opts};
+echo(<<"filter_then_parse_cookies">>, Req0, Opts) ->
+ Req = cowboy_req:filter_cookies([cake, color], Req0),
+ Value = cowboy_req:parse_cookies(Req),
+ {ok, cowboy_req:reply(200, #{}, value_to_iodata(Value), Req), Opts};
echo(What, Req, Opts) ->
Key = binary_to_atom(What, latin1),
Value = case cowboy_req:path(Req) of
diff --git a/test/req_SUITE.erl b/test/req_SUITE.erl
index 2cc8de4..76abba9 100644
--- a/test/req_SUITE.erl
+++ b/test/req_SUITE.erl
@@ -286,6 +286,27 @@ parse_cookies(Config) ->
[{<<"cookie">>, "goodname=strawberry\tmilkshake"}], Config),
ok.
+filter_then_parse_cookies(Config) ->
+ doc("Filter cookies then parse them."),
+ <<"[]">> = do_get_body("/filter_then_parse_cookies", Config),
+ <<"[{<<\"cake\">>,<<\"strawberry\">>}]">>
+ = do_get_body("/filter_then_parse_cookies", [{<<"cookie">>, "cake=strawberry"}], Config),
+ <<"[{<<\"cake\">>,<<\"strawberry\">>},{<<\"color\">>,<<\"blue\">>}]">>
+ = do_get_body("/filter_then_parse_cookies", [{<<"cookie">>, "cake=strawberry; color=blue"}], Config),
+ <<"[{<<\"cake\">>,<<\"strawberry\">>},{<<\"color\">>,<<\"blue\">>}]">>
+ = do_get_body("/filter_then_parse_cookies",
+ [{<<"cookie">>, "cake=strawberry"}, {<<"cookie">>, "color=blue"}], Config),
+ <<"[]">>
+ = do_get_body("/filter_then_parse_cookies",
+ [{<<"cookie">>, "bad name=strawberry"}], Config),
+ <<"[{<<\"cake\">>,<<\"strawberry\">>}]">>
+ = do_get_body("/filter_then_parse_cookies",
+ [{<<"cookie">>, "bad name=strawberry; cake=strawberry"}], Config),
+ <<"[]">>
+ = do_get_body("/filter_then_parse_cookies",
+ [{<<"cookie">>, "Blocked by http://www.example.com/upgrade-to-remove"}], Config),
+ ok.
+
parse_header(Config) ->
doc("Parsed request header with/without default."),
<<"[{{<<\"text\">>,<<\"html\">>,[]},1000,[]}]">>