1
%% Copyright 2007 Mochi Media, Inc.
2
%% Copyright 2011 Thomas Burdick <thomas.burdick@gmail.com>
4
%% Permission to use, copy, modify, and/or distribute this software for any
5
%% purpose with or without fee is hereby granted, provided that the above
6
%% copyright notice and this permission notice appear in all copies.
8
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16
%% @doc HTTP Cookie parsing and generating (RFC 2965).
18
-module(cowboy_cookies).
20
-export([parse_cookie/1, cookie/3, cookie/2]). %% API.
23
-type kv() :: {Name::binary(), Value::binary()}.
24
-type kvlist() :: [kv()].
25
-type cookie_option() :: {max_age, integer()}
26
| {local_time, calendar:datetime()}
27
| {domain, binary()} | {path, binary()}
28
| {secure, true | false} | {http_only, true | false}.
29
-export_type([kv/0, kvlist/0, cookie_option/0]).
33
-include_lib("eunit/include/eunit.hrl").
37
%% @doc Parse the contents of a Cookie header field, ignoring cookie
38
%% attributes, and return a simple property list.
39
-spec parse_cookie(binary()) -> kvlist().
42
parse_cookie(Cookie) when is_binary(Cookie) ->
43
parse_cookie(Cookie, []).
45
%% @equiv cookie(Key, Value, [])
46
-spec cookie(binary(), binary()) -> kv().
47
cookie(Key, Value) when is_binary(Key) andalso is_binary(Value) ->
48
cookie(Key, Value, []).
50
%% @doc Generate a Set-Cookie header field tuple.
51
-spec cookie(binary(), binary(), [cookie_option()]) -> kv().
52
cookie(Key, Value, Options) when is_binary(Key)
53
andalso is_binary(Value) andalso is_list(Options) ->
54
Cookie = <<(any_to_binary(Key))/binary, "=",
55
(quote(Value))/binary, "; Version=1">>,
57
%% Comment, Domain, Max-Age, Path, Secure, Version
59
case proplists:get_value(max_age, Options) of
63
When = case proplists:get_value(local_time, Options) of
65
calendar:local_time();
69
Age = case RawAge < 0 of
75
AgeBinary = quote(Age),
76
CookieDate = age_to_cookie_date(Age, When),
77
<<"; Expires=", CookieDate/binary,
78
"; Max-Age=", AgeBinary/binary>>
81
case proplists:get_value(secure, Options) of
88
case proplists:get_value(domain, Options) of
92
<<"; Domain=", (quote(Domain))/binary>>
95
case proplists:get_value(path, Options) of
99
<<"; Path=", (quote(Path))/binary>>
102
case proplists:get_value(http_only, Options) of
108
CookieParts = <<Cookie/binary, ExpiresPart/binary, SecurePart/binary,
109
DomainPart/binary, PathPart/binary, HttpOnlyPart/binary>>,
110
{<<"Set-Cookie">>, CookieParts}.
114
%% @doc Check if a character is a white space character.
115
is_whitespace($\s) -> true;
116
is_whitespace($\t) -> true;
117
is_whitespace($\r) -> true;
118
is_whitespace($\n) -> true;
119
is_whitespace(_) -> false.
121
%% @doc Check if a character is a seperator.
122
is_separator(C) when C < 32 -> true;
123
is_separator($\s) -> true;
124
is_separator($\t) -> true;
125
is_separator($() -> true;
126
is_separator($)) -> true;
127
is_separator($<) -> true;
128
is_separator($>) -> true;
129
is_separator($@) -> true;
130
is_separator($,) -> true;
131
is_separator($;) -> true;
132
is_separator($:) -> true;
133
is_separator($\\) -> true;
134
is_separator(?QUOTE) -> true;
135
is_separator($/) -> true;
136
is_separator($[) -> true;
137
is_separator($]) -> true;
138
is_separator($?) -> true;
139
is_separator($=) -> true;
140
is_separator(${) -> true;
141
is_separator($}) -> true;
142
is_separator(_) -> false.
144
%% @doc Check if a binary has an ASCII seperator character.
145
has_seperator(<<>>) ->
147
has_seperator(<<$/, Rest/binary>>) ->
149
has_seperator(<<C, Rest/binary>>) ->
150
case is_separator(C) of
157
%% @doc Convert to a binary and raise an error if quoting is required. Quoting
158
%% is broken in different ways for different browsers. Its better to simply
159
%% avoiding doing it at all.
161
-spec quote(term()) -> binary().
163
V = any_to_binary(V0),
164
case has_seperator(V) of
166
erlang:error({cookie_quoting_required, V});
171
-spec add_seconds(integer(), calendar:datetime()) -> calendar:datetime().
172
add_seconds(Secs, LocalTime) ->
173
Greg = calendar:datetime_to_gregorian_seconds(LocalTime),
174
calendar:gregorian_seconds_to_datetime(Greg + Secs).
176
-spec age_to_cookie_date(integer(), calendar:datetime()) -> binary().
177
age_to_cookie_date(Age, LocalTime) ->
178
cowboy_clock:rfc2109(add_seconds(Age, LocalTime)).
180
-spec parse_cookie(binary(), kvlist()) -> kvlist().
181
parse_cookie(<<>>, Acc) ->
183
parse_cookie(String, Acc) ->
184
{{Token, Value}, Rest} = read_pair(String),
188
<<"$", _R/binary>> ->
191
[{Token, Value} | Acc]
193
parse_cookie(Rest, Acc1).
195
-spec read_pair(binary()) -> {{binary(), binary()}, binary()}.
197
{Token, Rest} = read_token(skip_whitespace(String)),
198
{Value, Rest1} = read_value(skip_whitespace(Rest)),
199
{{Token, Value}, skip_past_separator(Rest1)}.
201
-spec read_value(binary()) -> {binary(), binary()}.
202
read_value(<<"=", Value/binary>>) ->
203
Value1 = skip_whitespace(Value),
205
<<?QUOTE, _R/binary>> ->
210
read_value(String) ->
213
-spec read_quoted(binary()) -> {binary(), binary()}.
214
read_quoted(<<?QUOTE, String/binary>>) ->
215
read_quoted(String, <<"">>).
217
-spec read_quoted(binary(), binary()) -> {binary(), binary()}.
218
read_quoted(<<"">>, Acc) ->
220
read_quoted(<<?QUOTE, Rest/binary>>, Acc) ->
222
read_quoted(<<$\\, Any, Rest/binary>>, Acc) ->
223
read_quoted(Rest, <<Acc/binary, Any>>);
224
read_quoted(<<C, Rest/binary>>, Acc) ->
225
read_quoted(Rest, <<Acc/binary, C>>).
227
%% @doc Drop characters while a function returns true.
228
binary_dropwhile(_F, <<"">>) ->
230
binary_dropwhile(F, String) ->
231
<<C, Rest/binary>> = String,
234
binary_dropwhile(F, Rest);
239
%% @doc Remove leading whitespace.
240
-spec skip_whitespace(binary()) -> binary().
241
skip_whitespace(String) ->
242
binary_dropwhile(fun is_whitespace/1, String).
244
%% @doc Split a binary when the current character causes F to return true.
245
binary_splitwith(_F, Head, <<>>) ->
247
binary_splitwith(F, Head, Tail) ->
248
<<C, NTail/binary>> = Tail,
253
binary_splitwith(F, <<Head/binary, C>>, NTail)
256
%% @doc Split a binary with a function returning true or false on each char.
257
binary_splitwith(F, String) ->
258
binary_splitwith(F, <<>>, String).
260
%% @doc Split the binary when the next seperator is found.
261
-spec read_token(binary()) -> {binary(), binary()}.
262
read_token(String) ->
263
binary_splitwith(fun is_separator/1, String).
265
%% @doc Return string after ; or , characters.
266
-spec skip_past_separator(binary()) -> binary().
267
skip_past_separator(<<"">>) ->
269
skip_past_separator(<<";", Rest/binary>>) ->
271
skip_past_separator(<<",", Rest/binary>>) ->
273
skip_past_separator(<<_C, Rest/binary>>) ->
274
skip_past_separator(Rest).
276
-spec any_to_binary(binary() | string() | atom() | integer()) -> binary().
277
any_to_binary(V) when is_binary(V) ->
279
any_to_binary(V) when is_list(V) ->
280
erlang:list_to_binary(V);
281
any_to_binary(V) when is_atom(V) ->
282
erlang:atom_to_binary(V, latin1);
283
any_to_binary(V) when is_integer(V) ->
284
list_to_binary(integer_to_list(V)).
291
%% ?assertError eunit macro is not compatible with coverage module
292
_ = try quote(<<":wq">>)
293
catch error:{cookie_quoting_required, <<":wq">>} -> ok
295
?assertEqual(<<"foo">>,quote(foo)),
298
parse_cookie_test() ->
300
C1 = <<"$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\";
301
Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\";
302
Shipping=\"FedEx\"; $Path=\"/acme\"">>,
304
[{<<"Customer">>,<<"WILE_E_COYOTE">>},
305
{<<"Part_Number">>,<<"Rocket_Launcher_0001">>},
306
{<<"Shipping">>,<<"FedEx">>}],
308
%% Potential edge cases
310
[{<<"foo">>, <<"x">>}],
311
parse_cookie(<<"foo=\"\\x\"">>)),
314
parse_cookie(<<"=">>)),
316
[{<<"foo">>, <<"">>}, {<<"bar">>, <<"">>}],
317
parse_cookie(<<" foo ; bar ">>)),
319
[{<<"foo">>, <<"">>}, {<<"bar">>, <<"">>}],
320
parse_cookie(<<"foo=;bar=">>)),
322
[{<<"foo">>, <<"\";">>}, {<<"bar">>, <<"">>}],
323
parse_cookie(<<"foo = \"\\\";\";bar ">>)),
325
[{<<"foo">>, <<"\";bar">>}],
326
parse_cookie(<<"foo=\"\\\";bar">>)),
329
parse_cookie(<<"">>)),
331
[{<<"foo">>, <<"bar">>}, {<<"baz">>, <<"wibble">>}],
332
parse_cookie(<<"foo=bar , baz=wibble ">>)),
338
<<"Customer=WILE_E_COYOTE; "
342
cookie(<<"Customer">>, <<"WILE_E_COYOTE">>,
343
[{http_only, true}, {domain, <<"acme.com">>}])),
347
{<<"Set-Cookie">>, B} = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>,
348
[{max_age, 111}, {secure, true}]),
351
[<<"Customer=WILE_E_COYOTE">>,
353
<<" Expires=", _R/binary>>,
356
binary:split(B, <<";">>, [global])),
359
-spec cookie_test() -> no_return(). %% Not actually true, just a bad option.
361
C1 = {<<"Set-Cookie">>,
362
<<"Customer=WILE_E_COYOTE; "
365
C1 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>, [{path, <<"/acme">>}]),
367
C1 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>,
368
[{path, <<"/acme">>}, {badoption, <<"negatory">>}]),
370
{<<"Set-Cookie">>,<<"=NoKey; Version=1">>}
371
= cookie(<<"">>, <<"NoKey">>, []),
372
{<<"Set-Cookie">>,<<"=NoKey; Version=1">>}
373
= cookie(<<"">>, <<"NoKey">>),
374
LocalTime = calendar:universal_time_to_local_time(
375
{{2007, 5, 15}, {13, 45, 33}}),
376
C2 = {<<"Set-Cookie">>,
377
<<"Customer=WILE_E_COYOTE; "
379
"Expires=Tue, 15 May 2007 13:45:33 GMT; "
381
C2 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>,
382
[{max_age, -111}, {local_time, LocalTime}]),
383
C3 = {<<"Set-Cookie">>,
384
<<"Customer=WILE_E_COYOTE; "
386
"Expires=Wed, 16 May 2007 13:45:50 GMT; "
388
C3 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>,
389
[{max_age, 86417}, {local_time, LocalTime}]),