0
Fork 0
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:
Loïc Hoguin 2012-12-07 14:54:45 +01:00
parent db6b1596ae
commit 27da09282d
4 changed files with 191 additions and 468 deletions

View file

@ -76,39 +76,12 @@ rfc1123(DateTime) ->
%% This format is used in the <em>set-cookie</em> header sent with %% This format is used in the <em>set-cookie</em> header sent with
%% HTTP responses. %% HTTP responses.
-spec rfc2109(calendar:datetime()) -> binary(). -spec rfc2109(calendar:datetime()) -> binary().
rfc2109(LocalTime) -> rfc2109({Date = {Y, Mo, D}, {H, Mi, S}}) ->
{{YYYY,MM,DD},{Hour,Min,Sec}} = Wday = calendar:day_of_the_week(Date),
case calendar:local_time_to_universal_time_dst(LocalTime) of << (weekday(Wday))/binary, ", ", (pad_int(D))/binary, "-",
[Gmt] -> Gmt; (month(Mo))/binary, "-", (list_to_binary(integer_to_list(Y)))/binary,
[_,Gmt] -> Gmt; " ", (pad_int(H))/binary, $:, (pad_int(Mi))/binary,
[] -> $:, (pad_int(S))/binary, " 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">>.
%% gen_server. %% gen_server.
@ -219,6 +192,13 @@ month(12) -> <<"Dec">>.
-ifdef(TEST). -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_() -> update_rfc1123_test_() ->
Tests = [ Tests = [
{<<"Sat, 14 May 2011 14:25:33 GMT">>, undefined, {<<"Sat, 14 May 2011 14:25:33 GMT">>, undefined,

View file

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

View file

@ -19,6 +19,7 @@
%% Parsing. %% Parsing.
-export([list/2]). -export([list/2]).
-export([nonempty_list/2]). -export([nonempty_list/2]).
-export([cookie_list/1]).
-export([content_type/1]). -export([content_type/1]).
-export([media_range/2]). -export([media_range/2]).
-export([conneg/2]). -export([conneg/2]).
@ -42,6 +43,7 @@
-export([ce_identity/1]). -export([ce_identity/1]).
%% Interpretation. %% Interpretation.
-export([cookie_to_iodata/3]).
-export([version_to_binary/1]). -export([version_to_binary/1]).
-export([urldecode/1]). -export([urldecode/1]).
-export([urldecode/2]). -export([urldecode/2]).
@ -100,6 +102,33 @@ list(Data, Fun, Acc) ->
end) end)
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. %% @doc Parse a content type.
-spec content_type(binary()) -> any(). -spec content_type(binary()) -> any().
content_type(Data) -> content_type(Data) ->
@ -341,12 +370,17 @@ params(Data, Fun) ->
-spec params(binary(), fun(), [{binary(), binary()}]) -> any(). -spec params(binary(), fun(), [{binary(), binary()}]) -> any().
params(Data, Fun, Acc) -> params(Data, Fun, Acc) ->
whitespace(Data, whitespace(Data,
fun (<< $;, Rest/binary >>) -> param(Rest, Fun, Acc); fun (<< $;, Rest/binary >>) ->
(Rest) -> Fun(Rest, lists:reverse(Acc)) param(Rest,
fun (Rest2, Attr, Value) ->
params(Rest2, Fun, [{Attr, Value}|Acc])
end);
(Rest) ->
Fun(Rest, lists:reverse(Acc))
end). end).
-spec param(binary(), fun(), [{binary(), binary()}]) -> any(). -spec param(binary(), fun()) -> any().
param(Data, Fun, Acc) -> param(Data, Fun) ->
whitespace(Data, whitespace(Data,
fun (Rest) -> fun (Rest) ->
token_ci(Rest, token_ci(Rest,
@ -354,8 +388,7 @@ param(Data, Fun, Acc) ->
(<< $=, Rest2/binary >>, Attr) -> (<< $=, Rest2/binary >>, Attr) ->
word(Rest2, word(Rest2,
fun (Rest3, Value) -> fun (Rest3, Value) ->
params(Rest3, Fun, Fun(Rest3, Attr, Value)
[{Attr, Value}|Acc])
end); end);
(_Rest2, _Attr) -> {error, badarg} (_Rest2, _Attr) -> {error, badarg}
end) end)
@ -772,6 +805,56 @@ ce_identity(Data) ->
%% Interpretation. %% 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. %% @doc Convert an HTTP version tuple to its binary form.
-spec version_to_binary(version()) -> binary(). -spec version_to_binary(version()) -> binary().
version_to_binary({1, 1}) -> <<"HTTP/1.1">>; 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]. [{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_() -> media_range_list_test_() ->
%% {Tokens, Result} %% {Tokens, Result}
Tests = [ Tests = [
@ -1040,6 +1155,44 @@ digits_test_() ->
], ],
[{V, fun() -> R = digits(V) end} || {V, R} <- Tests]. [{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_() -> x_www_form_urlencoded_test_() ->
%% {Qs, Result} %% {Qs, Result}
Tests = [ Tests = [

View file

@ -118,6 +118,12 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-endif. -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()}). -type resp_body_fun() :: fun(() -> {sent, non_neg_integer()}).
-record(http_req, { -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, fun cowboy_http:digits/1);
parse_header(Name, Req, Default) when Name =:= <<"content-type">> -> parse_header(Name, Req, Default) when Name =:= <<"content-type">> ->
parse_header(Name, Req, Default, fun cowboy_http:content_type/1); 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) when Name =:= <<"expect">> ->
parse_header(Name, Req, Default, parse_header(Name, Req, Default,
fun (Value) -> fun (Value) ->
@ -481,11 +489,10 @@ cookie(Name, Req) when is_binary(Name) ->
-spec cookie(binary(), Req, Default) -spec cookie(binary(), Req, Default)
-> {binary() | true | Default, Req} when Req::req(), Default::any(). -> {binary() | true | Default, Req} when Req::req(), Default::any().
cookie(Name, Req=#http_req{cookies=undefined}, Default) when is_binary(Name) -> cookie(Name, Req=#http_req{cookies=undefined}, Default) when is_binary(Name) ->
case header(<<"cookie">>, Req) of case parse_header(<<"cookie">>, Req) of
{undefined, Req2} -> {ok, undefined, Req2} ->
{Default, Req2#http_req{cookies=[]}}; {Default, Req2#http_req{cookies=[]}};
{RawCookie, Req2} -> {ok, Cookies, Req2} ->
Cookies = cowboy_cookies:parse_cookie(RawCookie),
cookie(Name, Req2#http_req{cookies=Cookies}, Default) cookie(Name, Req2#http_req{cookies=Cookies}, Default)
end; end;
cookie(Name, Req, Default) -> cookie(Name, Req, Default) ->
@ -497,11 +504,10 @@ cookie(Name, Req, Default) ->
%% @doc Return the full list of cookie values. %% @doc Return the full list of cookie values.
-spec cookies(Req) -> {list({binary(), binary() | true}), Req} when Req::req(). -spec cookies(Req) -> {list({binary(), binary() | true}), Req} when Req::req().
cookies(Req=#http_req{cookies=undefined}) -> cookies(Req=#http_req{cookies=undefined}) ->
case header(<<"cookie">>, Req) of case parse_header(<<"cookie">>, Req) of
{undefined, Req2} -> {ok, undefined, Req2} ->
{[], Req2#http_req{cookies=[]}}; {[], Req2#http_req{cookies=[]}};
{RawCookie, Req2} -> {ok, Cookies, Req2} ->
Cookies = cowboy_cookies:parse_cookie(RawCookie),
cookies(Req2#http_req{cookies=Cookies}) cookies(Req2#http_req{cookies=Cookies})
end; end;
cookies(Req=#http_req{cookies=Cookies}) -> cookies(Req=#http_req{cookies=Cookies}) ->
@ -794,11 +800,11 @@ multipart_skip(Req) ->
%% Response API. %% Response API.
%% @doc Add a cookie header to the response. %% @doc Add a cookie header to the response.
-spec set_resp_cookie(binary(), binary(), -spec set_resp_cookie(iodata(), iodata(), cookie_opts(), Req)
[cowboy_cookies:cookie_option()], Req) -> Req when Req::req(). -> Req when Req::req().
set_resp_cookie(Name, Value, Options, Req) -> set_resp_cookie(Name, Value, Opts, Req) ->
{HeaderName, HeaderValue} = cowboy_cookies:cookie(Name, Value, Options), Cookie = cowboy_http:cookie_to_iodata(Name, Value, Opts),
set_resp_header(HeaderName, HeaderValue, Req). set_resp_header(<<"set-cookie">>, Cookie, Req).
%% @doc Add a header to the response. %% @doc Add a header to the response.
-spec set_resp_header(binary(), iodata(), Req) -spec set_resp_header(binary(), iodata(), Req)