/deps/mochiweb/src/mochiweb_cookies.erl

https://code.google.com/p/zotonic/ · Erlang · 324 lines · 244 code · 34 blank · 46 comment · 0 complexity · 6ff6479dc9fd197d8e5696768e373638 MD5 · raw file

  1. %% @author Emad El-Haraty <emad@mochimedia.com>
  2. %% @copyright 2007 Mochi Media, Inc.
  3. %% @doc HTTP Cookie parsing and generating (RFC 2109, RFC 2965).
  4. -module(mochiweb_cookies).
  5. -export([parse_cookie/1, cookie/3, cookie/2]).
  6. -define(QUOTE, $\").
  7. -define(IS_WHITESPACE(C),
  8. (C =:= $\s orelse C =:= $\t orelse C =:= $\r orelse C =:= $\n)).
  9. %% RFC 2616 separators (called tspecials in RFC 2068)
  10. -define(IS_SEPARATOR(C),
  11. (C < 32 orelse
  12. C =:= $\s orelse C =:= $\t orelse
  13. C =:= $( orelse C =:= $) orelse C =:= $< orelse C =:= $> orelse
  14. C =:= $@ orelse C =:= $, orelse C =:= $; orelse C =:= $: orelse
  15. C =:= $\\ orelse C =:= $\" orelse C =:= $/ orelse
  16. C =:= $[ orelse C =:= $] orelse C =:= $? orelse C =:= $= orelse
  17. C =:= ${ orelse C =:= $})).
  18. %% @type proplist() = [{Key::string(), Value::string()}].
  19. %% @type header() = {Name::string(), Value::string()}.
  20. %% @type int_seconds() = integer().
  21. %% @spec cookie(Key::string(), Value::string()) -> header()
  22. %% @doc Short-hand for <code>cookie(Key, Value, [])</code>.
  23. cookie(Key, Value) ->
  24. cookie(Key, Value, []).
  25. %% @spec cookie(Key::string(), Value::string(), Options::[Option]) -> header()
  26. %% where Option = {max_age, int_seconds()} | {local_time, {date(), time()}}
  27. %% | {domain, string()} | {path, string()}
  28. %% | {secure, true | false} | {http_only, true | false}
  29. %%
  30. %% @doc Generate a Set-Cookie header field tuple.
  31. cookie(Key, Value, Options) ->
  32. Cookie = [any_to_list(Key), "=", quote(Value), "; Version=1"],
  33. %% Set-Cookie:
  34. %% Comment, Domain, Max-Age, Path, Secure, Version
  35. %% Set-Cookie2:
  36. %% Comment, CommentURL, Discard, Domain, Max-Age, Path, Port, Secure,
  37. %% Version
  38. ExpiresPart =
  39. case proplists:get_value(max_age, Options) of
  40. undefined ->
  41. "";
  42. RawAge ->
  43. When = case proplists:get_value(local_time, Options) of
  44. undefined ->
  45. calendar:local_time();
  46. LocalTime ->
  47. LocalTime
  48. end,
  49. Age = case RawAge < 0 of
  50. true ->
  51. 0;
  52. false ->
  53. RawAge
  54. end,
  55. ["; Expires=", age_to_cookie_date(Age, When),
  56. "; Max-Age=", quote(Age)]
  57. end,
  58. SecurePart =
  59. case proplists:get_value(secure, Options) of
  60. true ->
  61. "; Secure";
  62. _ ->
  63. ""
  64. end,
  65. DomainPart =
  66. case proplists:get_value(domain, Options) of
  67. undefined ->
  68. "";
  69. Domain ->
  70. ["; Domain=", quote(Domain)]
  71. end,
  72. PathPart =
  73. case proplists:get_value(path, Options) of
  74. undefined ->
  75. "";
  76. Path ->
  77. ["; Path=", quote(Path)]
  78. end,
  79. HttpOnlyPart =
  80. case proplists:get_value(http_only, Options) of
  81. true ->
  82. "; HttpOnly";
  83. _ ->
  84. ""
  85. end,
  86. CookieParts = [Cookie, ExpiresPart, SecurePart, DomainPart, PathPart, HttpOnlyPart],
  87. {"Set-Cookie", lists:flatten(CookieParts)}.
  88. %% Every major browser incorrectly handles quoted strings in a
  89. %% different and (worse) incompatible manner. Instead of wasting time
  90. %% writing redundant code for each browser, we restrict cookies to
  91. %% only contain characters that browsers handle compatibly.
  92. %%
  93. %% By replacing the definition of quote with this, we generate
  94. %% RFC-compliant cookies:
  95. %%
  96. %% quote(V) ->
  97. %% Fun = fun(?QUOTE, Acc) -> [$\\, ?QUOTE | Acc];
  98. %% (Ch, Acc) -> [Ch | Acc]
  99. %% end,
  100. %% [?QUOTE | lists:foldr(Fun, [?QUOTE], V)].
  101. %% Convert to a string and raise an error if quoting is required.
  102. quote(V0) ->
  103. V = any_to_list(V0),
  104. lists:all(fun(Ch) -> Ch =:= $/ orelse not ?IS_SEPARATOR(Ch) end, V)
  105. orelse erlang:error({cookie_quoting_required, V}),
  106. V.
  107. add_seconds(Secs, LocalTime) ->
  108. Greg = calendar:datetime_to_gregorian_seconds(LocalTime),
  109. calendar:gregorian_seconds_to_datetime(Greg + Secs).
  110. %% Return a date in the form of: Wdy, DD-Mon-YYYY HH:MM:SS GMT
  111. %% See also: rfc2109: 10.1.2
  112. rfc2109_cookie_expires_date(LocalTime) ->
  113. {{YYYY,MM,DD},{Hour,Min,Sec}} =
  114. case calendar:local_time_to_universal_time_dst(LocalTime) of
  115. [Gmt] -> Gmt;
  116. [_,Gmt] -> Gmt
  117. end,
  118. DayNumber = calendar:day_of_the_week({YYYY,MM,DD}),
  119. lists:flatten(
  120. io_lib:format("~s, ~2.2.0w-~3.s-~4.4.0w ~2.2.0w:~2.2.0w:~2.2.0w GMT",
  121. [httpd_util:day(DayNumber),DD,httpd_util:month(MM),YYYY,Hour,Min,Sec])).
  122. age_to_cookie_date(Age, LocalTime) ->
  123. rfc2109_cookie_expires_date(add_seconds(Age, LocalTime)).
  124. %% @spec parse_cookie(string()) -> [{K::string(), V::string()}]
  125. %% @doc Parse the contents of a Cookie header field, ignoring cookie
  126. %% attributes, and return a simple property list.
  127. parse_cookie("") ->
  128. [];
  129. parse_cookie(Cookie) ->
  130. parse_cookie(Cookie, []).
  131. %% Internal API
  132. parse_cookie([], Acc) ->
  133. lists:reverse(Acc);
  134. parse_cookie(String, Acc) ->
  135. {{Token, Value}, Rest} = read_pair(String),
  136. Acc1 = case Token of
  137. "" ->
  138. Acc;
  139. "$" ++ _ ->
  140. Acc;
  141. _ ->
  142. [{Token, Value} | Acc]
  143. end,
  144. parse_cookie(Rest, Acc1).
  145. read_pair(String) ->
  146. {Token, Rest} = read_token(skip_whitespace(String)),
  147. {Value, Rest1} = read_value(skip_whitespace(Rest)),
  148. {{Token, Value}, skip_past_separator(Rest1)}.
  149. read_value([$= | Value]) ->
  150. Value1 = skip_whitespace(Value),
  151. case Value1 of
  152. [?QUOTE | _] ->
  153. read_quoted(Value1);
  154. _ ->
  155. read_token(Value1)
  156. end;
  157. read_value(String) ->
  158. {"", String}.
  159. read_quoted([?QUOTE | String]) ->
  160. read_quoted(String, []).
  161. read_quoted([], Acc) ->
  162. {lists:reverse(Acc), []};
  163. read_quoted([?QUOTE | Rest], Acc) ->
  164. {lists:reverse(Acc), Rest};
  165. read_quoted([$\\, Any | Rest], Acc) ->
  166. read_quoted(Rest, [Any | Acc]);
  167. read_quoted([C | Rest], Acc) ->
  168. read_quoted(Rest, [C | Acc]).
  169. skip_whitespace(String) ->
  170. F = fun (C) -> ?IS_WHITESPACE(C) end,
  171. lists:dropwhile(F, String).
  172. read_token(String) ->
  173. F = fun (C) -> not ?IS_SEPARATOR(C) end,
  174. lists:splitwith(F, String).
  175. skip_past_separator([]) ->
  176. [];
  177. skip_past_separator([$; | Rest]) ->
  178. Rest;
  179. skip_past_separator([$, | Rest]) ->
  180. Rest;
  181. skip_past_separator([_ | Rest]) ->
  182. skip_past_separator(Rest).
  183. any_to_list(V) when is_list(V) ->
  184. V;
  185. any_to_list(V) when is_atom(V) ->
  186. atom_to_list(V);
  187. any_to_list(V) when is_binary(V) ->
  188. binary_to_list(V);
  189. any_to_list(V) when is_integer(V) ->
  190. integer_to_list(V).
  191. %%
  192. %% Tests
  193. %%
  194. -ifdef(TEST).
  195. -include_lib("eunit/include/eunit.hrl").
  196. quote_test() ->
  197. %% ?assertError eunit macro is not compatible with coverage module
  198. try quote(":wq")
  199. catch error:{cookie_quoting_required, ":wq"} -> ok
  200. end,
  201. ?assertEqual(
  202. "foo",
  203. quote(foo)),
  204. ok.
  205. parse_cookie_test() ->
  206. %% RFC example
  207. C1 = "$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\";
  208. Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\";
  209. Shipping=\"FedEx\"; $Path=\"/acme\"",
  210. ?assertEqual(
  211. [{"Customer","WILE_E_COYOTE"},
  212. {"Part_Number","Rocket_Launcher_0001"},
  213. {"Shipping","FedEx"}],
  214. parse_cookie(C1)),
  215. %% Potential edge cases
  216. ?assertEqual(
  217. [{"foo", "x"}],
  218. parse_cookie("foo=\"\\x\"")),
  219. ?assertEqual(
  220. [],
  221. parse_cookie("=")),
  222. ?assertEqual(
  223. [{"foo", ""}, {"bar", ""}],
  224. parse_cookie(" foo ; bar ")),
  225. ?assertEqual(
  226. [{"foo", ""}, {"bar", ""}],
  227. parse_cookie("foo=;bar=")),
  228. ?assertEqual(
  229. [{"foo", "\";"}, {"bar", ""}],
  230. parse_cookie("foo = \"\\\";\";bar ")),
  231. ?assertEqual(
  232. [{"foo", "\";bar"}],
  233. parse_cookie("foo=\"\\\";bar")),
  234. ?assertEqual(
  235. [],
  236. parse_cookie([])),
  237. ?assertEqual(
  238. [{"foo", "bar"}, {"baz", "wibble"}],
  239. parse_cookie("foo=bar , baz=wibble ")),
  240. ok.
  241. domain_test() ->
  242. ?assertEqual(
  243. {"Set-Cookie",
  244. "Customer=WILE_E_COYOTE; "
  245. "Version=1; "
  246. "Domain=acme.com; "
  247. "HttpOnly"},
  248. cookie("Customer", "WILE_E_COYOTE",
  249. [{http_only, true}, {domain, "acme.com"}])),
  250. ok.
  251. local_time_test() ->
  252. {"Set-Cookie", S} = cookie("Customer", "WILE_E_COYOTE",
  253. [{max_age, 111}, {secure, true}]),
  254. ?assertMatch(
  255. ["Customer=WILE_E_COYOTE",
  256. " Version=1",
  257. " Expires=" ++ _,
  258. " Max-Age=111",
  259. " Secure"],
  260. string:tokens(S, ";")),
  261. ok.
  262. cookie_test() ->
  263. C1 = {"Set-Cookie",
  264. "Customer=WILE_E_COYOTE; "
  265. "Version=1; "
  266. "Path=/acme"},
  267. C1 = cookie("Customer", "WILE_E_COYOTE", [{path, "/acme"}]),
  268. C1 = cookie("Customer", "WILE_E_COYOTE",
  269. [{path, "/acme"}, {badoption, "negatory"}]),
  270. C1 = cookie('Customer', 'WILE_E_COYOTE', [{path, '/acme'}]),
  271. C1 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>, [{path, <<"/acme">>}]),
  272. {"Set-Cookie","=NoKey; Version=1"} = cookie("", "NoKey", []),
  273. {"Set-Cookie","=NoKey; Version=1"} = cookie("", "NoKey"),
  274. LocalTime = calendar:universal_time_to_local_time({{2007, 5, 15}, {13, 45, 33}}),
  275. C2 = {"Set-Cookie",
  276. "Customer=WILE_E_COYOTE; "
  277. "Version=1; "
  278. "Expires=Tue, 15-May-2007 13:45:33 GMT; "
  279. "Max-Age=0"},
  280. C2 = cookie("Customer", "WILE_E_COYOTE",
  281. [{max_age, -111}, {local_time, LocalTime}]),
  282. C3 = {"Set-Cookie",
  283. "Customer=WILE_E_COYOTE; "
  284. "Version=1; "
  285. "Expires=Wed, 16-May-2007 13:45:50 GMT; "
  286. "Max-Age=86417"},
  287. C3 = cookie("Customer", "WILE_E_COYOTE",
  288. [{max_age, 86417}, {local_time, LocalTime}]),
  289. ok.
  290. -endif.