0
Fork 0
mirror of https://github.com/ninenines/cowboy.git synced 2025-07-14 12:20:24 +00:00

Add cowboy_req:filter_cookies/2

This commit is contained in:
Loïc Hoguin 2019-10-05 11:23:57 +02:00
parent 5ffb4f98e0
commit 03dac1486d
No known key found for this signature in database
GPG key ID: 8A9DF795F6FED764
5 changed files with 126 additions and 0 deletions

View file

@ -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

View file

@ -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)]

View file

@ -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).

View file

@ -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

View file

@ -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,[]}]">>