123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401 |
- %% 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).
- -export([parse_cookie/1, cookie/3, cookie/2]). %% API.
- %% Types.
- -type kv() :: {Name::binary(), Value::binary()}.
- -type kvlist() :: [kv()].
- -type cookie_option() :: {max_age, integer()}
- | {local_time, {cowboy_clock:date(), cowboy_clock:time()}}
- | {domain, binary()} | {path, binary()}
- | {secure, true | false} | {http_only, true | false}.
- -export_type([kv/0, kvlist/0, cookie_option/0]).
- -define(QUOTE, $\").
- -include_lib("eunit/include/eunit.hrl").
- %% 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))/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 seperator.
- -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 seperator character.
- -spec has_seperator(binary()) -> boolean().
- has_seperator(<<>>) ->
- false;
- has_seperator(<<$/, Rest/binary>>) ->
- has_seperator(Rest);
- has_seperator(<<C, Rest/binary>>) ->
- case is_separator(C) of
- true ->
- true;
- false ->
- has_seperator(Rest)
- 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()) -> binary().
- quote(V0) ->
- V = any_to_binary(V0),
- case has_seperator(V) of
- true ->
- erlang:error({cookie_quoting_required, V});
- false ->
- V
- end.
- -spec add_seconds(integer(), cowboy_clock:datetime())
- -> cowboy_clock: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(), cowboy_clock: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 seperator 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)),
- 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.
|