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_http.erl | 165 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 159 insertions(+), 6 deletions(-) (limited to 'src/cowboy_http.erl') 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 = [ -- cgit v1.2.3