Skip to content
Newer
Older
100644 417 lines (374 sloc) 11.6 KB
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
1 %% Copyright 2007 Mochi Media, Inc.
2 %% Copyright 2011 Thomas Burdick <thomas.burdick@gmail.com>
3 %%
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.
7 %%
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.
15
16 %% @doc HTTP Cookie parsing and generating (RFC 2965).
17
18 -module(cowboy_cookies).
19
a5e7521 @essen Have only one -export and -export_type per line
essen authored
20 %% API.
21 -export([parse_cookie/1]).
22 -export([cookie/3]).
23 -export([cookie/2]).
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
24
25 %% Types.
26 -type kv() :: {Name::binary(), Value::binary()}.
27 -type kvlist() :: [kv()].
28 -type cookie_option() :: {max_age, integer()}
156c84f Use calendar date and time types exported since R14B04
Loïc Hoguin authored
29 | {local_time, calendar:datetime()}
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
30 | {domain, binary()} | {path, binary()}
31 | {secure, true | false} | {http_only, true | false}.
a5e7521 @essen Have only one -export and -export_type per line
essen authored
32
33 -export_type([kv/0]).
34 -export_type([kvlist/0]).
35 -export_type([cookie_option/0]).
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
36
37 -define(QUOTE, $\").
38
13b743b @essen Include the eunit file only if TEST is defined
essen authored
39 -ifdef(TEST).
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
40 -include_lib("eunit/include/eunit.hrl").
13b743b @essen Include the eunit file only if TEST is defined
essen authored
41 -endif.
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
42
528d0eb Small cosmetic changes and doc update to the cookie patch
Loïc Hoguin authored
43 %% API.
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
44
45 %% @doc Parse the contents of a Cookie header field, ignoring cookie
46 %% attributes, and return a simple property list.
47 -spec parse_cookie(binary()) -> kvlist().
48 parse_cookie(<<>>) ->
49 [];
b75859e @bfrog Fail early in cookie-related API functions
bfrog authored
50 parse_cookie(Cookie) when is_binary(Cookie) ->
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
51 parse_cookie(Cookie, []).
52
6138901 Fix cookie tests and specs
Loïc Hoguin authored
53 %% @equiv cookie(Key, Value, [])
54 -spec cookie(binary(), binary()) -> kv().
b75859e @bfrog Fail early in cookie-related API functions
bfrog authored
55 cookie(Key, Value) when is_binary(Key) andalso is_binary(Value) ->
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
56 cookie(Key, Value, []).
57
58 %% @doc Generate a Set-Cookie header field tuple.
6138901 Fix cookie tests and specs
Loïc Hoguin authored
59 -spec cookie(binary(), binary(), [cookie_option()]) -> kv().
60 cookie(Key, Value, Options) when is_binary(Key)
61 andalso is_binary(Value) andalso is_list(Options) ->
62 Cookie = <<(any_to_binary(Key))/binary, "=",
63 (quote(Value))/binary, "; Version=1">>,
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
64 %% Set-Cookie:
65 %% Comment, Domain, Max-Age, Path, Secure, Version
66 ExpiresPart =
67 case proplists:get_value(max_age, Options) of
68 undefined ->
69 <<"">>;
70 RawAge ->
71 When = case proplists:get_value(local_time, Options) of
72 undefined ->
73 calendar:local_time();
74 LocalTime ->
75 LocalTime
76 end,
77 Age = case RawAge < 0 of
78 true ->
79 0;
80 false ->
81 RawAge
82 end,
83 AgeBinary = quote(Age),
84 CookieDate = age_to_cookie_date(Age, When),
85 <<"; Expires=", CookieDate/binary,
86 "; Max-Age=", AgeBinary/binary>>
87 end,
88 SecurePart =
89 case proplists:get_value(secure, Options) of
90 true ->
91 <<"; Secure">>;
92 _ ->
93 <<"">>
94 end,
95 DomainPart =
96 case proplists:get_value(domain, Options) of
97 undefined ->
98 <<"">>;
99 Domain ->
100 <<"; Domain=", (quote(Domain))/binary>>
101 end,
102 PathPart =
103 case proplists:get_value(path, Options) of
104 undefined ->
105 <<"">>;
106 Path ->
7ffd324 @bfrog Only ignore slashes in cookie values for the path
bfrog authored
107 <<"; Path=", (quote(Path, true))/binary>>
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
108 end,
109 HttpOnlyPart =
110 case proplists:get_value(http_only, Options) of
111 true ->
112 <<"; HttpOnly">>;
113 _ ->
114 <<"">>
115 end,
6138901 Fix cookie tests and specs
Loïc Hoguin authored
116 CookieParts = <<Cookie/binary, ExpiresPart/binary, SecurePart/binary,
117 DomainPart/binary, PathPart/binary, HttpOnlyPart/binary>>,
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
118 {<<"Set-Cookie">>, CookieParts}.
119
528d0eb Small cosmetic changes and doc update to the cookie patch
Loïc Hoguin authored
120 %% Internal.
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
121
122 %% @doc Check if a character is a white space character.
123 -spec is_whitespace(char()) -> boolean().
124 is_whitespace($\s) -> true;
125 is_whitespace($\t) -> true;
126 is_whitespace($\r) -> true;
127 is_whitespace($\n) -> true;
128 is_whitespace(_) -> false.
129
7ffd324 @bfrog Only ignore slashes in cookie values for the path
bfrog authored
130 %% @doc Check if a character is a separator.
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
131 -spec is_separator(char()) -> boolean().
132 is_separator(C) when C < 32 -> true;
133 is_separator($\s) -> true;
134 is_separator($\t) -> 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($:) -> true;
143 is_separator($\\) -> true;
144 is_separator(?QUOTE) -> true;
145 is_separator($/) -> true;
146 is_separator($[) -> true;
147 is_separator($]) -> true;
148 is_separator($?) -> true;
149 is_separator($=) -> true;
150 is_separator(${) -> true;
151 is_separator($}) -> true;
152 is_separator(_) -> false.
153
7ffd324 @bfrog Only ignore slashes in cookie values for the path
bfrog authored
154 %% @doc Check if a binary has an ASCII separator character.
155 -spec has_separator(binary(), boolean()) -> boolean().
156 has_separator(<<>>, _) ->
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
157 false;
7ffd324 @bfrog Only ignore slashes in cookie values for the path
bfrog authored
158 has_separator(<<$/, Rest/binary>>, true) ->
159 has_separator(Rest, true);
160 has_separator(<<C, Rest/binary>>, IgnoreSlash) ->
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
161 case is_separator(C) of
162 true ->
163 true;
164 false ->
7ffd324 @bfrog Only ignore slashes in cookie values for the path
bfrog authored
165 has_separator(Rest, IgnoreSlash)
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
166 end.
167
168 %% @doc Convert to a binary and raise an error if quoting is required. Quoting
169 %% is broken in different ways for different browsers. Its better to simply
170 %% avoiding doing it at all.
171 %% @end
7ffd324 @bfrog Only ignore slashes in cookie values for the path
bfrog authored
172 -spec quote(term(), boolean()) -> binary().
173 quote(V0, IgnoreSlash) ->
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
174 V = any_to_binary(V0),
7ffd324 @bfrog Only ignore slashes in cookie values for the path
bfrog authored
175 case has_separator(V, IgnoreSlash) of
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
176 true ->
177 erlang:error({cookie_quoting_required, V});
178 false ->
179 V
180 end.
181
7ffd324 @bfrog Only ignore slashes in cookie values for the path
bfrog authored
182 %% @equiv quote(Bin, false)
183 -spec quote(term()) -> binary().
184 quote(V0) ->
185 quote(V0, false).
186
156c84f Use calendar date and time types exported since R14B04
Loïc Hoguin authored
187 -spec add_seconds(integer(), calendar:datetime()) -> calendar:datetime().
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
188 add_seconds(Secs, LocalTime) ->
189 Greg = calendar:datetime_to_gregorian_seconds(LocalTime),
190 calendar:gregorian_seconds_to_datetime(Greg + Secs).
191
156c84f Use calendar date and time types exported since R14B04
Loïc Hoguin authored
192 -spec age_to_cookie_date(integer(), calendar:datetime()) -> binary().
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
193 age_to_cookie_date(Age, LocalTime) ->
194 cowboy_clock:rfc2109(add_seconds(Age, LocalTime)).
195
196 -spec parse_cookie(binary(), kvlist()) -> kvlist().
197 parse_cookie(<<>>, Acc) ->
198 lists:reverse(Acc);
199 parse_cookie(String, Acc) ->
200 {{Token, Value}, Rest} = read_pair(String),
201 Acc1 = case Token of
202 <<"">> ->
203 Acc;
204 <<"$", _R/binary>> ->
205 Acc;
206 _ ->
207 [{Token, Value} | Acc]
208 end,
209 parse_cookie(Rest, Acc1).
210
6138901 Fix cookie tests and specs
Loïc Hoguin authored
211 -spec read_pair(binary()) -> {{binary(), binary()}, binary()}.
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
212 read_pair(String) ->
213 {Token, Rest} = read_token(skip_whitespace(String)),
214 {Value, Rest1} = read_value(skip_whitespace(Rest)),
215 {{Token, Value}, skip_past_separator(Rest1)}.
216
217 -spec read_value(binary()) -> {binary(), binary()}.
218 read_value(<<"=", Value/binary>>) ->
219 Value1 = skip_whitespace(Value),
220 case Value1 of
221 <<?QUOTE, _R/binary>> ->
222 read_quoted(Value1);
223 _ ->
224 read_token(Value1)
225 end;
226 read_value(String) ->
227 {<<"">>, String}.
228
229 -spec read_quoted(binary()) -> {binary(), binary()}.
230 read_quoted(<<?QUOTE, String/binary>>) ->
231 read_quoted(String, <<"">>).
232
233 -spec read_quoted(binary(), binary()) -> {binary(), binary()}.
234 read_quoted(<<"">>, Acc) ->
235 {Acc, <<"">>};
236 read_quoted(<<?QUOTE, Rest/binary>>, Acc) ->
237 {Acc, Rest};
238 read_quoted(<<$\\, Any, Rest/binary>>, Acc) ->
239 read_quoted(Rest, <<Acc/binary, Any>>);
240 read_quoted(<<C, Rest/binary>>, Acc) ->
241 read_quoted(Rest, <<Acc/binary, C>>).
242
243 %% @doc Drop characters while a function returns true.
244 -spec binary_dropwhile(fun((char()) -> boolean()), binary()) -> binary().
245 binary_dropwhile(_F, <<"">>) ->
246 <<"">>;
247 binary_dropwhile(F, String) ->
248 <<C, Rest/binary>> = String,
249 case F(C) of
250 true ->
251 binary_dropwhile(F, Rest);
252 false ->
253 String
254 end.
255
256 %% @doc Remove leading whitespace.
257 -spec skip_whitespace(binary()) -> binary().
258 skip_whitespace(String) ->
259 binary_dropwhile(fun is_whitespace/1, String).
260
261 %% @doc Split a binary when the current character causes F to return true.
6138901 Fix cookie tests and specs
Loïc Hoguin authored
262 -spec binary_splitwith(fun((char()) -> boolean()), binary(), binary())
263 -> {binary(), binary()}.
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
264 binary_splitwith(_F, Head, <<>>) ->
265 {Head, <<>>};
266 binary_splitwith(F, Head, Tail) ->
267 <<C, NTail/binary>> = Tail,
268 case F(C) of
269 true ->
270 {Head, Tail};
271 false ->
272 binary_splitwith(F, <<Head/binary, C>>, NTail)
273 end.
274
275 %% @doc Split a binary with a function returning true or false on each char.
6138901 Fix cookie tests and specs
Loïc Hoguin authored
276 -spec binary_splitwith(fun((char()) -> boolean()), binary())
277 -> {binary(), binary()}.
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
278 binary_splitwith(F, String) ->
279 binary_splitwith(F, <<>>, String).
280
7ffd324 @bfrog Only ignore slashes in cookie values for the path
bfrog authored
281 %% @doc Split the binary when the next separator is found.
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
282 -spec read_token(binary()) -> {binary(), binary()}.
283 read_token(String) ->
284 binary_splitwith(fun is_separator/1, String).
285
286 %% @doc Return string after ; or , characters.
287 -spec skip_past_separator(binary()) -> binary().
288 skip_past_separator(<<"">>) ->
289 <<"">>;
290 skip_past_separator(<<";", Rest/binary>>) ->
291 Rest;
292 skip_past_separator(<<",", Rest/binary>>) ->
293 Rest;
294 skip_past_separator(<<_C, Rest/binary>>) ->
295 skip_past_separator(Rest).
296
297 -spec any_to_binary(binary() | string() | atom() | integer()) -> binary().
298 any_to_binary(V) when is_binary(V) ->
299 V;
300 any_to_binary(V) when is_list(V) ->
301 erlang:list_to_binary(V);
302 any_to_binary(V) when is_atom(V) ->
303 erlang:atom_to_binary(V, latin1);
304 any_to_binary(V) when is_integer(V) ->
305 list_to_binary(integer_to_list(V)).
306
528d0eb Small cosmetic changes and doc update to the cookie patch
Loïc Hoguin authored
307 %% Tests.
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
308
309 -ifdef(TEST).
310
311 quote_test() ->
312 %% ?assertError eunit macro is not compatible with coverage module
6138901 Fix cookie tests and specs
Loïc Hoguin authored
313 _ = try quote(<<":wq">>)
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
314 catch error:{cookie_quoting_required, <<":wq">>} -> ok
315 end,
316 ?assertEqual(<<"foo">>,quote(foo)),
7ffd324 @bfrog Only ignore slashes in cookie values for the path
bfrog authored
317 _ = try quote(<<"/test/slashes/">>)
318 catch error:{cookie_quoting_required, <<"/test/slashes/">>} -> ok
319 end,
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
320 ok.
321
322 parse_cookie_test() ->
323 %% RFC example
324 C1 = <<"$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\";
325 Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\";
326 Shipping=\"FedEx\"; $Path=\"/acme\"">>,
327 ?assertEqual(
328 [{<<"Customer">>,<<"WILE_E_COYOTE">>},
329 {<<"Part_Number">>,<<"Rocket_Launcher_0001">>},
330 {<<"Shipping">>,<<"FedEx">>}],
331 parse_cookie(C1)),
332 %% Potential edge cases
333 ?assertEqual(
334 [{<<"foo">>, <<"x">>}],
335 parse_cookie(<<"foo=\"\\x\"">>)),
336 ?assertEqual(
337 [],
338 parse_cookie(<<"=">>)),
339 ?assertEqual(
340 [{<<"foo">>, <<"">>}, {<<"bar">>, <<"">>}],
341 parse_cookie(<<" foo ; bar ">>)),
342 ?assertEqual(
343 [{<<"foo">>, <<"">>}, {<<"bar">>, <<"">>}],
344 parse_cookie(<<"foo=;bar=">>)),
345 ?assertEqual(
346 [{<<"foo">>, <<"\";">>}, {<<"bar">>, <<"">>}],
347 parse_cookie(<<"foo = \"\\\";\";bar ">>)),
348 ?assertEqual(
349 [{<<"foo">>, <<"\";bar">>}],
350 parse_cookie(<<"foo=\"\\\";bar">>)),
351 ?assertEqual(
352 [],
353 parse_cookie(<<"">>)),
354 ?assertEqual(
355 [{<<"foo">>, <<"bar">>}, {<<"baz">>, <<"wibble">>}],
356 parse_cookie(<<"foo=bar , baz=wibble ">>)),
357 ok.
358
359 domain_test() ->
360 ?assertEqual(
361 {<<"Set-Cookie">>,
362 <<"Customer=WILE_E_COYOTE; "
363 "Version=1; "
364 "Domain=acme.com; "
365 "HttpOnly">>},
366 cookie(<<"Customer">>, <<"WILE_E_COYOTE">>,
367 [{http_only, true}, {domain, <<"acme.com">>}])),
368 ok.
369
370 local_time_test() ->
371 {<<"Set-Cookie">>, B} = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>,
372 [{max_age, 111}, {secure, true}]),
373
374 ?assertMatch(
375 [<<"Customer=WILE_E_COYOTE">>,
376 <<" Version=1">>,
377 <<" Expires=", _R/binary>>,
378 <<" Max-Age=111">>,
379 <<" Secure">>],
380 binary:split(B, <<";">>, [global])),
381 ok.
382
6138901 Fix cookie tests and specs
Loïc Hoguin authored
383 -spec cookie_test() -> no_return(). %% Not actually true, just a bad option.
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
384 cookie_test() ->
385 C1 = {<<"Set-Cookie">>,
386 <<"Customer=WILE_E_COYOTE; "
387 "Version=1; "
388 "Path=/acme">>},
389 C1 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>, [{path, <<"/acme">>}]),
390
391 C1 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>,
392 [{path, <<"/acme">>}, {badoption, <<"negatory">>}]),
393
6138901 Fix cookie tests and specs
Loïc Hoguin authored
394 {<<"Set-Cookie">>,<<"=NoKey; Version=1">>}
395 = cookie(<<"">>, <<"NoKey">>, []),
396 {<<"Set-Cookie">>,<<"=NoKey; Version=1">>}
397 = cookie(<<"">>, <<"NoKey">>),
398 LocalTime = calendar:universal_time_to_local_time(
399 {{2007, 5, 15}, {13, 45, 33}}),
a29ccb0 @bfrog Add cowboy_cookies for cookie creation and parsing
bfrog authored
400 C2 = {<<"Set-Cookie">>,
401 <<"Customer=WILE_E_COYOTE; "
402 "Version=1; "
403 "Expires=Tue, 15 May 2007 13:45:33 GMT; "
404 "Max-Age=0">>},
405 C2 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>,
406 [{max_age, -111}, {local_time, LocalTime}]),
407 C3 = {<<"Set-Cookie">>,
408 <<"Customer=WILE_E_COYOTE; "
409 "Version=1; "
410 "Expires=Wed, 16 May 2007 13:45:50 GMT; "
411 "Max-Age=86417">>},
412 C3 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>,
413 [{max_age, 86417}, {local_time, LocalTime}]),
414 ok.
415
416 -endif.
Something went wrong with that request. Please try again.