mirror of
https://github.com/ninenines/cowboy.git
synced 2025-07-14 20:30:23 +00:00
Make cookies use universal time instead of local time
Includes: * cowboy_clock:rfc2109/1 now expects UTC datetime * Rewrite of the cookie code to cowboy_http * Removal of cowboy_cookies * Add type cowboy_req:cookie_opts/0 Cookies should now be set using cowboy_req:set_resp_cookie/3. Code calling cowboy_cookies directly will need to be updated.
This commit is contained in:
parent
db6b1596ae
commit
27da09282d
4 changed files with 191 additions and 468 deletions
|
@ -76,39 +76,12 @@ rfc1123(DateTime) ->
|
|||
%% This format is used in the <em>set-cookie</em> header sent with
|
||||
%% HTTP responses.
|
||||
-spec rfc2109(calendar:datetime()) -> binary().
|
||||
rfc2109(LocalTime) ->
|
||||
{{YYYY,MM,DD},{Hour,Min,Sec}} =
|
||||
case calendar:local_time_to_universal_time_dst(LocalTime) of
|
||||
[Gmt] -> Gmt;
|
||||
[_,Gmt] -> Gmt;
|
||||
[] ->
|
||||
%% The localtime generated by cowboy_cookies may fall within
|
||||
%% the hour that is skipped by daylight savings time. If this
|
||||
%% is such a localtime, increment the localtime with one hour
|
||||
%% and try again, if this succeeds, subtracting the max_age
|
||||
%% from the resulting universaltime and converting to a local
|
||||
%% time will yield the original localtime.
|
||||
{Date, {Hour1, Min1, Sec1}} = LocalTime,
|
||||
LocalTime2 = {Date, {Hour1 + 1, Min1, Sec1}},
|
||||
case calendar:local_time_to_universal_time_dst(LocalTime2) of
|
||||
[Gmt] -> Gmt;
|
||||
[_,Gmt] -> Gmt
|
||||
end
|
||||
end,
|
||||
Wday = calendar:day_of_the_week({YYYY,MM,DD}),
|
||||
DayBin = pad_int(DD),
|
||||
YearBin = list_to_binary(integer_to_list(YYYY)),
|
||||
HourBin = pad_int(Hour),
|
||||
MinBin = pad_int(Min),
|
||||
SecBin = pad_int(Sec),
|
||||
WeekDay = weekday(Wday),
|
||||
Month = month(MM),
|
||||
<<WeekDay/binary, ", ",
|
||||
DayBin/binary, " ", Month/binary, " ",
|
||||
YearBin/binary, " ",
|
||||
HourBin/binary, ":",
|
||||
MinBin/binary, ":",
|
||||
SecBin/binary, " GMT">>.
|
||||
rfc2109({Date = {Y, Mo, D}, {H, Mi, S}}) ->
|
||||
Wday = calendar:day_of_the_week(Date),
|
||||
<< (weekday(Wday))/binary, ", ", (pad_int(D))/binary, "-",
|
||||
(month(Mo))/binary, "-", (list_to_binary(integer_to_list(Y)))/binary,
|
||||
" ", (pad_int(H))/binary, $:, (pad_int(Mi))/binary,
|
||||
$:, (pad_int(S))/binary, " GMT" >>.
|
||||
|
||||
%% gen_server.
|
||||
|
||||
|
@ -219,6 +192,13 @@ month(12) -> <<"Dec">>.
|
|||
|
||||
-ifdef(TEST).
|
||||
|
||||
rfc2109_test_() ->
|
||||
Tests = [
|
||||
{<<"Sat, 14-May-2011 14:25:33 GMT">>, {{2011, 5, 14}, {14, 25, 33}}},
|
||||
{<<"Sun, 01-Jan-2012 00:00:00 GMT">>, {{2012, 1, 1}, { 0, 0, 0}}}
|
||||
],
|
||||
[{R, fun() -> R = rfc2109(D) end} || {R, D} <- Tests].
|
||||
|
||||
update_rfc1123_test_() ->
|
||||
Tests = [
|
||||
{<<"Sat, 14 May 2011 14:25:33 GMT">>, undefined,
|
||||
|
|
|
@ -1,416 +0,0 @@
|
|||
%% Copyright 2007 Mochi Media, Inc.
|
||||
%% Copyright 2011 Thomas Burdick <thomas.burdick@gmail.com>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
%% @doc HTTP Cookie parsing and generating (RFC 2965).
|
||||
|
||||
-module(cowboy_cookies).
|
||||
|
||||
%% API.
|
||||
-export([parse_cookie/1]).
|
||||
-export([cookie/3]).
|
||||
-export([cookie/2]).
|
||||
|
||||
%% Types.
|
||||
-type kv() :: {Name::binary(), Value::binary()}.
|
||||
-type kvlist() :: [kv()].
|
||||
-type cookie_option() :: {max_age, integer()}
|
||||
| {local_time, calendar:datetime()}
|
||||
| {domain, binary()} | {path, binary()}
|
||||
| {secure, true | false} | {http_only, true | false}.
|
||||
|
||||
-export_type([kv/0]).
|
||||
-export_type([kvlist/0]).
|
||||
-export_type([cookie_option/0]).
|
||||
|
||||
-define(QUOTE, $\").
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-endif.
|
||||
|
||||
%% API.
|
||||
|
||||
%% @doc Parse the contents of a Cookie header field, ignoring cookie
|
||||
%% attributes, and return a simple property list.
|
||||
-spec parse_cookie(binary()) -> kvlist().
|
||||
parse_cookie(<<>>) ->
|
||||
[];
|
||||
parse_cookie(Cookie) when is_binary(Cookie) ->
|
||||
parse_cookie(Cookie, []).
|
||||
|
||||
%% @equiv cookie(Key, Value, [])
|
||||
-spec cookie(binary(), binary()) -> kv().
|
||||
cookie(Key, Value) when is_binary(Key) andalso is_binary(Value) ->
|
||||
cookie(Key, Value, []).
|
||||
|
||||
%% @doc Generate a Set-Cookie header field tuple.
|
||||
-spec cookie(binary(), binary(), [cookie_option()]) -> kv().
|
||||
cookie(Key, Value, Options) when is_binary(Key)
|
||||
andalso is_binary(Value) andalso is_list(Options) ->
|
||||
Cookie = <<(any_to_binary(Key))/binary, "=",
|
||||
(quote(Value))/binary, "; Version=1">>,
|
||||
%% Set-Cookie:
|
||||
%% Comment, Domain, Max-Age, Path, Secure, Version
|
||||
ExpiresPart =
|
||||
case proplists:get_value(max_age, Options) of
|
||||
undefined ->
|
||||
<<"">>;
|
||||
RawAge ->
|
||||
When = case proplists:get_value(local_time, Options) of
|
||||
undefined ->
|
||||
calendar:local_time();
|
||||
LocalTime ->
|
||||
LocalTime
|
||||
end,
|
||||
Age = case RawAge < 0 of
|
||||
true ->
|
||||
0;
|
||||
false ->
|
||||
RawAge
|
||||
end,
|
||||
AgeBinary = quote(Age),
|
||||
CookieDate = age_to_cookie_date(Age, When),
|
||||
<<"; Expires=", CookieDate/binary,
|
||||
"; Max-Age=", AgeBinary/binary>>
|
||||
end,
|
||||
SecurePart =
|
||||
case proplists:get_value(secure, Options) of
|
||||
true ->
|
||||
<<"; Secure">>;
|
||||
_ ->
|
||||
<<"">>
|
||||
end,
|
||||
DomainPart =
|
||||
case proplists:get_value(domain, Options) of
|
||||
undefined ->
|
||||
<<"">>;
|
||||
Domain ->
|
||||
<<"; Domain=", (quote(Domain))/binary>>
|
||||
end,
|
||||
PathPart =
|
||||
case proplists:get_value(path, Options) of
|
||||
undefined ->
|
||||
<<"">>;
|
||||
Path ->
|
||||
<<"; Path=", (quote(Path, true))/binary>>
|
||||
end,
|
||||
HttpOnlyPart =
|
||||
case proplists:get_value(http_only, Options) of
|
||||
true ->
|
||||
<<"; HttpOnly">>;
|
||||
_ ->
|
||||
<<"">>
|
||||
end,
|
||||
CookieParts = <<Cookie/binary, ExpiresPart/binary, SecurePart/binary,
|
||||
DomainPart/binary, PathPart/binary, HttpOnlyPart/binary>>,
|
||||
{<<"set-cookie">>, CookieParts}.
|
||||
|
||||
%% Internal.
|
||||
|
||||
%% @doc Check if a character is a white space character.
|
||||
-spec is_whitespace(char()) -> boolean().
|
||||
is_whitespace($\s) -> true;
|
||||
is_whitespace($\t) -> true;
|
||||
is_whitespace($\r) -> true;
|
||||
is_whitespace($\n) -> true;
|
||||
is_whitespace(_) -> false.
|
||||
|
||||
%% @doc Check if a character is a separator.
|
||||
-spec is_separator(char()) -> boolean().
|
||||
is_separator(C) when C < 32 -> true;
|
||||
is_separator($\s) -> true;
|
||||
is_separator($\t) -> true;
|
||||
is_separator($() -> true;
|
||||
is_separator($)) -> true;
|
||||
is_separator($<) -> true;
|
||||
is_separator($>) -> true;
|
||||
is_separator($@) -> true;
|
||||
is_separator($,) -> true;
|
||||
is_separator($;) -> true;
|
||||
is_separator($:) -> true;
|
||||
is_separator($\\) -> true;
|
||||
is_separator(?QUOTE) -> true;
|
||||
is_separator($/) -> true;
|
||||
is_separator($[) -> true;
|
||||
is_separator($]) -> true;
|
||||
is_separator($?) -> true;
|
||||
is_separator($=) -> true;
|
||||
is_separator(${) -> true;
|
||||
is_separator($}) -> true;
|
||||
is_separator(_) -> false.
|
||||
|
||||
%% @doc Check if a binary has an ASCII separator character.
|
||||
-spec has_separator(binary(), boolean()) -> boolean().
|
||||
has_separator(<<>>, _) ->
|
||||
false;
|
||||
has_separator(<<$/, Rest/binary>>, true) ->
|
||||
has_separator(Rest, true);
|
||||
has_separator(<<C, Rest/binary>>, IgnoreSlash) ->
|
||||
case is_separator(C) of
|
||||
true ->
|
||||
true;
|
||||
false ->
|
||||
has_separator(Rest, IgnoreSlash)
|
||||
end.
|
||||
|
||||
%% @doc Convert to a binary and raise an error if quoting is required. Quoting
|
||||
%% is broken in different ways for different browsers. Its better to simply
|
||||
%% avoiding doing it at all.
|
||||
%% @end
|
||||
-spec quote(term(), boolean()) -> binary().
|
||||
quote(V0, IgnoreSlash) ->
|
||||
V = any_to_binary(V0),
|
||||
case has_separator(V, IgnoreSlash) of
|
||||
true ->
|
||||
erlang:error({cookie_quoting_required, V});
|
||||
false ->
|
||||
V
|
||||
end.
|
||||
|
||||
%% @equiv quote(Bin, false)
|
||||
-spec quote(term()) -> binary().
|
||||
quote(V0) ->
|
||||
quote(V0, false).
|
||||
|
||||
-spec add_seconds(integer(), calendar:datetime()) -> calendar:datetime().
|
||||
add_seconds(Secs, LocalTime) ->
|
||||
Greg = calendar:datetime_to_gregorian_seconds(LocalTime),
|
||||
calendar:gregorian_seconds_to_datetime(Greg + Secs).
|
||||
|
||||
-spec age_to_cookie_date(integer(), calendar:datetime()) -> binary().
|
||||
age_to_cookie_date(Age, LocalTime) ->
|
||||
cowboy_clock:rfc2109(add_seconds(Age, LocalTime)).
|
||||
|
||||
-spec parse_cookie(binary(), kvlist()) -> kvlist().
|
||||
parse_cookie(<<>>, Acc) ->
|
||||
lists:reverse(Acc);
|
||||
parse_cookie(String, Acc) ->
|
||||
{{Token, Value}, Rest} = read_pair(String),
|
||||
Acc1 = case Token of
|
||||
<<"">> ->
|
||||
Acc;
|
||||
<<"$", _R/binary>> ->
|
||||
Acc;
|
||||
_ ->
|
||||
[{Token, Value} | Acc]
|
||||
end,
|
||||
parse_cookie(Rest, Acc1).
|
||||
|
||||
-spec read_pair(binary()) -> {{binary(), binary()}, binary()}.
|
||||
read_pair(String) ->
|
||||
{Token, Rest} = read_token(skip_whitespace(String)),
|
||||
{Value, Rest1} = read_value(skip_whitespace(Rest)),
|
||||
{{Token, Value}, skip_past_separator(Rest1)}.
|
||||
|
||||
-spec read_value(binary()) -> {binary(), binary()}.
|
||||
read_value(<<"=", Value/binary>>) ->
|
||||
Value1 = skip_whitespace(Value),
|
||||
case Value1 of
|
||||
<<?QUOTE, _R/binary>> ->
|
||||
read_quoted(Value1);
|
||||
_ ->
|
||||
read_token(Value1)
|
||||
end;
|
||||
read_value(String) ->
|
||||
{<<"">>, String}.
|
||||
|
||||
-spec read_quoted(binary()) -> {binary(), binary()}.
|
||||
read_quoted(<<?QUOTE, String/binary>>) ->
|
||||
read_quoted(String, <<"">>).
|
||||
|
||||
-spec read_quoted(binary(), binary()) -> {binary(), binary()}.
|
||||
read_quoted(<<"">>, Acc) ->
|
||||
{Acc, <<"">>};
|
||||
read_quoted(<<?QUOTE, Rest/binary>>, Acc) ->
|
||||
{Acc, Rest};
|
||||
read_quoted(<<$\\, Any, Rest/binary>>, Acc) ->
|
||||
read_quoted(Rest, <<Acc/binary, Any>>);
|
||||
read_quoted(<<C, Rest/binary>>, Acc) ->
|
||||
read_quoted(Rest, <<Acc/binary, C>>).
|
||||
|
||||
%% @doc Drop characters while a function returns true.
|
||||
-spec binary_dropwhile(fun((char()) -> boolean()), binary()) -> binary().
|
||||
binary_dropwhile(_F, <<"">>) ->
|
||||
<<"">>;
|
||||
binary_dropwhile(F, String) ->
|
||||
<<C, Rest/binary>> = String,
|
||||
case F(C) of
|
||||
true ->
|
||||
binary_dropwhile(F, Rest);
|
||||
false ->
|
||||
String
|
||||
end.
|
||||
|
||||
%% @doc Remove leading whitespace.
|
||||
-spec skip_whitespace(binary()) -> binary().
|
||||
skip_whitespace(String) ->
|
||||
binary_dropwhile(fun is_whitespace/1, String).
|
||||
|
||||
%% @doc Split a binary when the current character causes F to return true.
|
||||
-spec binary_splitwith(fun((char()) -> boolean()), binary(), binary())
|
||||
-> {binary(), binary()}.
|
||||
binary_splitwith(_F, Head, <<>>) ->
|
||||
{Head, <<>>};
|
||||
binary_splitwith(F, Head, Tail) ->
|
||||
<<C, NTail/binary>> = Tail,
|
||||
case F(C) of
|
||||
true ->
|
||||
{Head, Tail};
|
||||
false ->
|
||||
binary_splitwith(F, <<Head/binary, C>>, NTail)
|
||||
end.
|
||||
|
||||
%% @doc Split a binary with a function returning true or false on each char.
|
||||
-spec binary_splitwith(fun((char()) -> boolean()), binary())
|
||||
-> {binary(), binary()}.
|
||||
binary_splitwith(F, String) ->
|
||||
binary_splitwith(F, <<>>, String).
|
||||
|
||||
%% @doc Split the binary when the next separator is found.
|
||||
-spec read_token(binary()) -> {binary(), binary()}.
|
||||
read_token(String) ->
|
||||
binary_splitwith(fun is_separator/1, String).
|
||||
|
||||
%% @doc Return string after ; or , characters.
|
||||
-spec skip_past_separator(binary()) -> binary().
|
||||
skip_past_separator(<<"">>) ->
|
||||
<<"">>;
|
||||
skip_past_separator(<<";", Rest/binary>>) ->
|
||||
Rest;
|
||||
skip_past_separator(<<",", Rest/binary>>) ->
|
||||
Rest;
|
||||
skip_past_separator(<<_C, Rest/binary>>) ->
|
||||
skip_past_separator(Rest).
|
||||
|
||||
-spec any_to_binary(binary() | string() | atom() | integer()) -> binary().
|
||||
any_to_binary(V) when is_binary(V) ->
|
||||
V;
|
||||
any_to_binary(V) when is_list(V) ->
|
||||
erlang:list_to_binary(V);
|
||||
any_to_binary(V) when is_atom(V) ->
|
||||
erlang:atom_to_binary(V, latin1);
|
||||
any_to_binary(V) when is_integer(V) ->
|
||||
list_to_binary(integer_to_list(V)).
|
||||
|
||||
%% Tests.
|
||||
|
||||
-ifdef(TEST).
|
||||
|
||||
quote_test() ->
|
||||
%% ?assertError eunit macro is not compatible with coverage module
|
||||
_ = try quote(<<":wq">>)
|
||||
catch error:{cookie_quoting_required, <<":wq">>} -> ok
|
||||
end,
|
||||
?assertEqual(<<"foo">>,quote(foo)),
|
||||
_ = try quote(<<"/test/slashes/">>)
|
||||
catch error:{cookie_quoting_required, <<"/test/slashes/">>} -> ok
|
||||
end,
|
||||
ok.
|
||||
|
||||
parse_cookie_test() ->
|
||||
%% RFC example
|
||||
C1 = <<"$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\";
|
||||
Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\";
|
||||
Shipping=\"FedEx\"; $Path=\"/acme\"">>,
|
||||
?assertEqual(
|
||||
[{<<"Customer">>,<<"WILE_E_COYOTE">>},
|
||||
{<<"Part_Number">>,<<"Rocket_Launcher_0001">>},
|
||||
{<<"Shipping">>,<<"FedEx">>}],
|
||||
parse_cookie(C1)),
|
||||
%% Potential edge cases
|
||||
?assertEqual(
|
||||
[{<<"foo">>, <<"x">>}],
|
||||
parse_cookie(<<"foo=\"\\x\"">>)),
|
||||
?assertEqual(
|
||||
[],
|
||||
parse_cookie(<<"=">>)),
|
||||
?assertEqual(
|
||||
[{<<"foo">>, <<"">>}, {<<"bar">>, <<"">>}],
|
||||
parse_cookie(<<" foo ; bar ">>)),
|
||||
?assertEqual(
|
||||
[{<<"foo">>, <<"">>}, {<<"bar">>, <<"">>}],
|
||||
parse_cookie(<<"foo=;bar=">>)),
|
||||
?assertEqual(
|
||||
[{<<"foo">>, <<"\";">>}, {<<"bar">>, <<"">>}],
|
||||
parse_cookie(<<"foo = \"\\\";\";bar ">>)),
|
||||
?assertEqual(
|
||||
[{<<"foo">>, <<"\";bar">>}],
|
||||
parse_cookie(<<"foo=\"\\\";bar">>)),
|
||||
?assertEqual(
|
||||
[],
|
||||
parse_cookie(<<"">>)),
|
||||
?assertEqual(
|
||||
[{<<"foo">>, <<"bar">>}, {<<"baz">>, <<"wibble">>}],
|
||||
parse_cookie(<<"foo=bar , baz=wibble ">>)),
|
||||
ok.
|
||||
|
||||
domain_test() ->
|
||||
?assertEqual(
|
||||
{<<"set-cookie">>,
|
||||
<<"Customer=WILE_E_COYOTE; "
|
||||
"Version=1; "
|
||||
"Domain=acme.com; "
|
||||
"HttpOnly">>},
|
||||
cookie(<<"Customer">>, <<"WILE_E_COYOTE">>,
|
||||
[{http_only, true}, {domain, <<"acme.com">>}])),
|
||||
ok.
|
||||
|
||||
local_time_test() ->
|
||||
{<<"set-cookie">>, B} = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>,
|
||||
[{max_age, 111}, {secure, true}]),
|
||||
|
||||
?assertMatch(
|
||||
[<<"Customer=WILE_E_COYOTE">>,
|
||||
<<" Version=1">>,
|
||||
<<" Expires=", _R/binary>>,
|
||||
<<" Max-Age=111">>,
|
||||
<<" Secure">>],
|
||||
binary:split(B, <<";">>, [global])),
|
||||
ok.
|
||||
|
||||
-spec cookie_test() -> no_return(). %% Not actually true, just a bad option.
|
||||
cookie_test() ->
|
||||
C1 = {<<"set-cookie">>,
|
||||
<<"Customer=WILE_E_COYOTE; "
|
||||
"Version=1; "
|
||||
"Path=/acme">>},
|
||||
C1 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>, [{path, <<"/acme">>}]),
|
||||
|
||||
C1 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>,
|
||||
[{path, <<"/acme">>}, {badoption, <<"negatory">>}]),
|
||||
|
||||
{<<"set-cookie">>,<<"=NoKey; Version=1">>}
|
||||
= cookie(<<"">>, <<"NoKey">>, []),
|
||||
{<<"set-cookie">>,<<"=NoKey; Version=1">>}
|
||||
= cookie(<<"">>, <<"NoKey">>),
|
||||
LocalTime = calendar:universal_time_to_local_time(
|
||||
{{2007, 5, 15}, {13, 45, 33}}),
|
||||
C2 = {<<"set-cookie">>,
|
||||
<<"Customer=WILE_E_COYOTE; "
|
||||
"Version=1; "
|
||||
"Expires=Tue, 15 May 2007 13:45:33 GMT; "
|
||||
"Max-Age=0">>},
|
||||
C2 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>,
|
||||
[{max_age, -111}, {local_time, LocalTime}]),
|
||||
C3 = {<<"set-cookie">>,
|
||||
<<"Customer=WILE_E_COYOTE; "
|
||||
"Version=1; "
|
||||
"Expires=Wed, 16 May 2007 13:45:50 GMT; "
|
||||
"Max-Age=86417">>},
|
||||
C3 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>,
|
||||
[{max_age, 86417}, {local_time, LocalTime}]),
|
||||
ok.
|
||||
|
||||
-endif.
|
|
@ -19,6 +19,7 @@
|
|||
%% Parsing.
|
||||
-export([list/2]).
|
||||
-export([nonempty_list/2]).
|
||||
-export([cookie_list/1]).
|
||||
-export([content_type/1]).
|
||||
-export([media_range/2]).
|
||||
-export([conneg/2]).
|
||||
|
@ -42,6 +43,7 @@
|
|||
-export([ce_identity/1]).
|
||||
|
||||
%% Interpretation.
|
||||
-export([cookie_to_iodata/3]).
|
||||
-export([version_to_binary/1]).
|
||||
-export([urldecode/1]).
|
||||
-export([urldecode/2]).
|
||||
|
@ -100,6 +102,33 @@ list(Data, Fun, Acc) ->
|
|||
end)
|
||||
end).
|
||||
|
||||
%% @doc Parse a list of cookies.
|
||||
%%
|
||||
%% We need a special function for this because we need to support both
|
||||
%% $; and $, as separators as per RFC2109.
|
||||
-spec cookie_list(binary()) -> [{binary(), binary()}] | {error, badarg}.
|
||||
cookie_list(Data) ->
|
||||
case cookie_list(Data, []) of
|
||||
{error, badarg} -> {error, badarg};
|
||||
[] -> {error, badarg};
|
||||
L -> lists:reverse(L)
|
||||
end.
|
||||
|
||||
-spec cookie_list(binary(), Acc) -> Acc | {error, badarg}
|
||||
when Acc::[{binary(), binary()}].
|
||||
cookie_list(Data, Acc) ->
|
||||
whitespace(Data,
|
||||
fun (<<>>) -> Acc;
|
||||
(<< $,, Rest/binary >>) -> cookie_list(Rest, Acc);
|
||||
(<< $;, Rest/binary >>) -> cookie_list(Rest, Acc);
|
||||
(Rest) -> param(Rest,
|
||||
fun (Rest2, << $$, _/bits >>, _) ->
|
||||
cookie_list(Rest2, Acc);
|
||||
(Rest2, Name, Value) ->
|
||||
cookie_list(Rest2, [{Name, Value}|Acc])
|
||||
end)
|
||||
end).
|
||||
|
||||
%% @doc Parse a content type.
|
||||
-spec content_type(binary()) -> any().
|
||||
content_type(Data) ->
|
||||
|
@ -341,12 +370,17 @@ params(Data, Fun) ->
|
|||
-spec params(binary(), fun(), [{binary(), binary()}]) -> any().
|
||||
params(Data, Fun, Acc) ->
|
||||
whitespace(Data,
|
||||
fun (<< $;, Rest/binary >>) -> param(Rest, Fun, Acc);
|
||||
(Rest) -> Fun(Rest, lists:reverse(Acc))
|
||||
fun (<< $;, Rest/binary >>) ->
|
||||
param(Rest,
|
||||
fun (Rest2, Attr, Value) ->
|
||||
params(Rest2, Fun, [{Attr, Value}|Acc])
|
||||
end);
|
||||
(Rest) ->
|
||||
Fun(Rest, lists:reverse(Acc))
|
||||
end).
|
||||
|
||||
-spec param(binary(), fun(), [{binary(), binary()}]) -> any().
|
||||
param(Data, Fun, Acc) ->
|
||||
-spec param(binary(), fun()) -> any().
|
||||
param(Data, Fun) ->
|
||||
whitespace(Data,
|
||||
fun (Rest) ->
|
||||
token_ci(Rest,
|
||||
|
@ -354,8 +388,7 @@ param(Data, Fun, Acc) ->
|
|||
(<< $=, Rest2/binary >>, Attr) ->
|
||||
word(Rest2,
|
||||
fun (Rest3, Value) ->
|
||||
params(Rest3, Fun,
|
||||
[{Attr, Value}|Acc])
|
||||
Fun(Rest3, Attr, Value)
|
||||
end);
|
||||
(_Rest2, _Attr) -> {error, badarg}
|
||||
end)
|
||||
|
@ -772,6 +805,56 @@ ce_identity(Data) ->
|
|||
|
||||
%% Interpretation.
|
||||
|
||||
%% @doc Convert a cookie name, value and options to its iodata form.
|
||||
%% @end
|
||||
%%
|
||||
%% Initially from Mochiweb:
|
||||
%% * Copyright 2007 Mochi Media, Inc.
|
||||
%% Initial binary implementation:
|
||||
%% * Copyright 2011 Thomas Burdick <thomas.burdick@gmail.com>
|
||||
-spec cookie_to_iodata(iodata(), iodata(), cowboy_req:cookie_opts())
|
||||
-> iodata().
|
||||
cookie_to_iodata(Name, Value, Opts) ->
|
||||
MaxAgeBin = case lists:keyfind(max_age, 1, Opts) of
|
||||
false -> <<>>;
|
||||
{_, MaxAge} when is_integer(MaxAge), MaxAge >= 0 ->
|
||||
UTC = calendar:universal_time(),
|
||||
Secs = calendar:datetime_to_gregorian_seconds(UTC),
|
||||
Expires = calendar:gregorian_seconds_to_datetime(Secs + MaxAge),
|
||||
[<<"; Expires=">>, cowboy_clock:rfc2109(Expires),
|
||||
<<"; Max-Age=">>, integer_to_list(MaxAge)]
|
||||
end,
|
||||
DomainBin = case lists:keyfind(domain, 1, Opts) of
|
||||
false -> <<>>;
|
||||
{_, Domain} -> [<<"; Domain=">>, quote(Domain)]
|
||||
end,
|
||||
PathBin = case lists:keyfind(path, 1, Opts) of
|
||||
false -> <<>>;
|
||||
{_, Path} -> [<<"; Path=">>, quote(Path)]
|
||||
end,
|
||||
SecureBin = case lists:keyfind(secure, 1, Opts) of
|
||||
false -> <<>>;
|
||||
{_, true} -> <<"; Secure">>
|
||||
end,
|
||||
HttpOnlyBin = case lists:keyfind(http_only, 1, Opts) of
|
||||
false -> <<>>;
|
||||
{_, true} -> <<"; HttpOnly">>
|
||||
end,
|
||||
[Name, <<"=">>, quote(Value), <<"; Version=1">>,
|
||||
MaxAgeBin, DomainBin, PathBin, SecureBin, HttpOnlyBin].
|
||||
|
||||
-spec quote(binary()) -> binary().
|
||||
quote(Bin) ->
|
||||
quote(Bin, <<>>).
|
||||
|
||||
-spec quote(binary(), binary()) -> binary().
|
||||
quote(<<>>, Acc) ->
|
||||
Acc;
|
||||
quote(<< $", Rest/bits >>, Acc) ->
|
||||
quote(Rest, << Acc/binary, $\\, $" >>);
|
||||
quote(<< C, Rest/bits >>, Acc) ->
|
||||
quote(Rest, << Acc/binary, C >>).
|
||||
|
||||
%% @doc Convert an HTTP version tuple to its binary form.
|
||||
-spec version_to_binary(version()) -> binary().
|
||||
version_to_binary({1, 1}) -> <<"HTTP/1.1">>;
|
||||
|
@ -927,6 +1010,38 @@ nonempty_token_list_test_() ->
|
|||
],
|
||||
[{V, fun() -> R = nonempty_list(V, fun token/2) end} || {V, R} <- Tests].
|
||||
|
||||
cookie_list_test_() ->
|
||||
%% {Value, Result}.
|
||||
Tests = [
|
||||
{<<"name=value; name2=value2">>, [
|
||||
{<<"name">>, <<"value">>},
|
||||
{<<"name2">>, <<"value2">>}
|
||||
]},
|
||||
{<<"$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"">>, [
|
||||
{<<"customer">>, <<"WILE_E_COYOTE">>}
|
||||
]},
|
||||
{<<"$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"; "
|
||||
"Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"; "
|
||||
"Shipping=\"FedEx\"; $Path=\"/acme\"">>, [
|
||||
{<<"customer">>, <<"WILE_E_COYOTE">>},
|
||||
{<<"part_number">>, <<"Rocket_Launcher_0001">>},
|
||||
{<<"shipping">>, <<"FedEx">>}
|
||||
]},
|
||||
%% Potential edge cases (initially from Mochiweb).
|
||||
{<<"foo=\"\\x\"">>, [{<<"foo">>, <<"x">>}]},
|
||||
{<<"=">>, {error, badarg}},
|
||||
{<<" foo ; bar ">>, {error, badarg}},
|
||||
{<<"foo=;bar=">>, {error, badarg}},
|
||||
{<<"foo=\"\\\";\";bar ">>, {error, badarg}},
|
||||
{<<"foo=\"\\\";\";bar=good ">>,
|
||||
[{<<"foo">>, <<"\";">>}, {<<"bar">>, <<"good">>}]},
|
||||
{<<"foo=\"\\\";bar">>, {error, badarg}},
|
||||
{<<"">>, {error, badarg}},
|
||||
{<<"foo=bar , baz=wibble ">>,
|
||||
[{<<"foo">>, <<"bar">>}, {<<"baz">>, <<"wibble">>}]}
|
||||
],
|
||||
[{V, fun() -> R = cookie_list(V) end} || {V, R} <- Tests].
|
||||
|
||||
media_range_list_test_() ->
|
||||
%% {Tokens, Result}
|
||||
Tests = [
|
||||
|
@ -1040,6 +1155,44 @@ digits_test_() ->
|
|||
],
|
||||
[{V, fun() -> R = digits(V) end} || {V, R} <- Tests].
|
||||
|
||||
cookie_to_iodata_test_() ->
|
||||
%% {Name, Value, Opts, Result}
|
||||
Tests = [
|
||||
{<<"Customer">>, <<"WILE_E_COYOTE">>,
|
||||
[{http_only, true}, {domain, <<"acme.com">>}],
|
||||
<<"Customer=WILE_E_COYOTE; Version=1; "
|
||||
"Domain=acme.com; HttpOnly">>},
|
||||
{<<"Customer">>, <<"WILE_E_COYOTE">>,
|
||||
[{path, <<"/acme">>}],
|
||||
<<"Customer=WILE_E_COYOTE; Version=1; Path=/acme">>},
|
||||
{<<"Customer">>, <<"WILE_E_COYOTE">>,
|
||||
[{path, <<"/acme">>}, {badoption, <<"negatory">>}],
|
||||
<<"Customer=WILE_E_COYOTE; Version=1; Path=/acme">>}
|
||||
],
|
||||
[{R, fun() -> R = iolist_to_binary(cookie_to_iodata(N, V, O)) end}
|
||||
|| {N, V, O, R} <- Tests].
|
||||
|
||||
cookie_to_iodata_max_age_test() ->
|
||||
F = fun(N, V, O) ->
|
||||
binary:split(iolist_to_binary(
|
||||
cookie_to_iodata(N, V, O)), <<";">>, [global])
|
||||
end,
|
||||
[<<"Customer=WILE_E_COYOTE">>,
|
||||
<<" Version=1">>,
|
||||
<<" Expires=", _/binary>>,
|
||||
<<" Max-Age=111">>,
|
||||
<<" Secure">>] = F(<<"Customer">>, <<"WILE_E_COYOTE">>,
|
||||
[{max_age, 111}, {secure, true}]),
|
||||
case catch F(<<"Customer">>, <<"WILE_E_COYOTE">>, [{max_age, -111}]) of
|
||||
{'EXIT', {{case_clause, {max_age, -111}}, _}} -> ok
|
||||
end,
|
||||
[<<"Customer=WILE_E_COYOTE">>,
|
||||
<<" Version=1">>,
|
||||
<<" Expires=", _/binary>>,
|
||||
<<" Max-Age=86417">>] = F(<<"Customer">>, <<"WILE_E_COYOTE">>,
|
||||
[{max_age, 86417}]),
|
||||
ok.
|
||||
|
||||
x_www_form_urlencoded_test_() ->
|
||||
%% {Qs, Result}
|
||||
Tests = [
|
||||
|
|
|
@ -118,6 +118,12 @@
|
|||
-include_lib("eunit/include/eunit.hrl").
|
||||
-endif.
|
||||
|
||||
-type cookie_option() :: {max_age, non_neg_integer()}
|
||||
| {domain, binary()} | {path, binary()}
|
||||
| {secure, boolean()} | {http_only, boolean()}.
|
||||
-type cookie_opts() :: [cookie_option()].
|
||||
-export_type([cookie_opts/0]).
|
||||
|
||||
-type resp_body_fun() :: fun(() -> {sent, non_neg_integer()}).
|
||||
|
||||
-record(http_req, {
|
||||
|
@ -430,6 +436,8 @@ parse_header(Name, Req, Default) when Name =:= <<"content-length">> ->
|
|||
parse_header(Name, Req, Default, fun cowboy_http:digits/1);
|
||||
parse_header(Name, Req, Default) when Name =:= <<"content-type">> ->
|
||||
parse_header(Name, Req, Default, fun cowboy_http:content_type/1);
|
||||
parse_header(Name = <<"cookie">>, Req, Default) ->
|
||||
parse_header(Name, Req, Default, fun cowboy_http:cookie_list/1);
|
||||
parse_header(Name, Req, Default) when Name =:= <<"expect">> ->
|
||||
parse_header(Name, Req, Default,
|
||||
fun (Value) ->
|
||||
|
@ -481,11 +489,10 @@ cookie(Name, Req) when is_binary(Name) ->
|
|||
-spec cookie(binary(), Req, Default)
|
||||
-> {binary() | true | Default, Req} when Req::req(), Default::any().
|
||||
cookie(Name, Req=#http_req{cookies=undefined}, Default) when is_binary(Name) ->
|
||||
case header(<<"cookie">>, Req) of
|
||||
{undefined, Req2} ->
|
||||
case parse_header(<<"cookie">>, Req) of
|
||||
{ok, undefined, Req2} ->
|
||||
{Default, Req2#http_req{cookies=[]}};
|
||||
{RawCookie, Req2} ->
|
||||
Cookies = cowboy_cookies:parse_cookie(RawCookie),
|
||||
{ok, Cookies, Req2} ->
|
||||
cookie(Name, Req2#http_req{cookies=Cookies}, Default)
|
||||
end;
|
||||
cookie(Name, Req, Default) ->
|
||||
|
@ -497,11 +504,10 @@ cookie(Name, Req, Default) ->
|
|||
%% @doc Return the full list of cookie values.
|
||||
-spec cookies(Req) -> {list({binary(), binary() | true}), Req} when Req::req().
|
||||
cookies(Req=#http_req{cookies=undefined}) ->
|
||||
case header(<<"cookie">>, Req) of
|
||||
{undefined, Req2} ->
|
||||
case parse_header(<<"cookie">>, Req) of
|
||||
{ok, undefined, Req2} ->
|
||||
{[], Req2#http_req{cookies=[]}};
|
||||
{RawCookie, Req2} ->
|
||||
Cookies = cowboy_cookies:parse_cookie(RawCookie),
|
||||
{ok, Cookies, Req2} ->
|
||||
cookies(Req2#http_req{cookies=Cookies})
|
||||
end;
|
||||
cookies(Req=#http_req{cookies=Cookies}) ->
|
||||
|
@ -794,11 +800,11 @@ multipart_skip(Req) ->
|
|||
%% Response API.
|
||||
|
||||
%% @doc Add a cookie header to the response.
|
||||
-spec set_resp_cookie(binary(), binary(),
|
||||
[cowboy_cookies:cookie_option()], Req) -> Req when Req::req().
|
||||
set_resp_cookie(Name, Value, Options, Req) ->
|
||||
{HeaderName, HeaderValue} = cowboy_cookies:cookie(Name, Value, Options),
|
||||
set_resp_header(HeaderName, HeaderValue, Req).
|
||||
-spec set_resp_cookie(iodata(), iodata(), cookie_opts(), Req)
|
||||
-> Req when Req::req().
|
||||
set_resp_cookie(Name, Value, Opts, Req) ->
|
||||
Cookie = cowboy_http:cookie_to_iodata(Name, Value, Opts),
|
||||
set_resp_header(<<"set-cookie">>, Cookie, Req).
|
||||
|
||||
%% @doc Add a header to the response.
|
||||
-spec set_resp_header(binary(), iodata(), Req)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue