From 27da09282da1d52d564a988a6ce0c7ed4cc8ccf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Fri, 7 Dec 2012 14:54:45 +0100 Subject: 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. --- src/cowboy_clock.erl | 46 ++---- src/cowboy_cookies.erl | 416 ------------------------------------------------- src/cowboy_http.erl | 165 +++++++++++++++++++- src/cowboy_req.erl | 32 ++-- 4 files changed, 191 insertions(+), 468 deletions(-) delete mode 100644 src/cowboy_cookies.erl diff --git a/src/cowboy_clock.erl b/src/cowboy_clock.erl index f851211..b439bb1 100644 --- a/src/cowboy_clock.erl +++ b/src/cowboy_clock.erl @@ -76,39 +76,12 @@ rfc1123(DateTime) -> %% This format is used in the set-cookie 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), - <>. +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, diff --git a/src/cowboy_cookies.erl b/src/cowboy_cookies.erl deleted file mode 100644 index 72b5e8a..0000000 --- a/src/cowboy_cookies.erl +++ /dev/null @@ -1,416 +0,0 @@ -%% Copyright 2007 Mochi Media, Inc. -%% Copyright 2011 Thomas Burdick -%% -%% 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 = <>, - {<<"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(<>, 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 - <> -> - read_quoted(Value1); - _ -> - read_token(Value1) - end; -read_value(String) -> - {<<"">>, String}. - --spec read_quoted(binary()) -> {binary(), binary()}. -read_quoted(<>) -> - read_quoted(String, <<"">>). - --spec read_quoted(binary(), binary()) -> {binary(), binary()}. -read_quoted(<<"">>, Acc) -> - {Acc, <<"">>}; -read_quoted(<>, Acc) -> - {Acc, Rest}; -read_quoted(<<$\\, Any, Rest/binary>>, Acc) -> - read_quoted(Rest, <>); -read_quoted(<>, Acc) -> - read_quoted(Rest, <>). - -%% @doc Drop characters while a function returns true. --spec binary_dropwhile(fun((char()) -> boolean()), binary()) -> binary(). -binary_dropwhile(_F, <<"">>) -> - <<"">>; -binary_dropwhile(F, String) -> - <> = 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) -> - <> = Tail, - case F(C) of - true -> - {Head, Tail}; - false -> - binary_splitwith(F, <>, 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. diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl index e0b1632..fb9f21c 100644 --- a/src/cowboy_http.erl +++ b/src/cowboy_http.erl @@ -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 +-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 = [ diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl index 2d45a59..dc98e30 100644 --- a/src/cowboy_req.erl +++ b/src/cowboy_req.erl @@ -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) -- cgit v1.2.3