cow_cookie.erl 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. %% Copyright (c) 2013-2014, Loïc Hoguin <essen@ninenines.eu>
  2. %%
  3. %% Permission to use, copy, modify, and/or distribute this software for any
  4. %% purpose with or without fee is hereby granted, provided that the above
  5. %% copyright notice and this permission notice appear in all copies.
  6. %%
  7. %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  8. %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  9. %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  10. %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  11. %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  12. %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  13. %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  14. -module(cow_cookie).
  15. -export([parse_cookie/1]).
  16. -export([setcookie/3]).
  17. -type cookie_option() :: {max_age, non_neg_integer()}
  18. | {domain, binary()} | {path, binary()}
  19. | {secure, boolean()} | {http_only, boolean()}.
  20. -type cookie_opts() :: [cookie_option()].
  21. -export_type([cookie_opts/0]).
  22. %% @doc Parse a cookie header string and return a list of key/values.
  23. -spec parse_cookie(binary()) -> [{binary(), binary()}] | {error, badarg}.
  24. parse_cookie(Cookie) ->
  25. parse_cookie(Cookie, []).
  26. parse_cookie(<<>>, Acc) ->
  27. lists:reverse(Acc);
  28. parse_cookie(<< $\s, Rest/binary >>, Acc) ->
  29. parse_cookie(Rest, Acc);
  30. parse_cookie(<< $\t, Rest/binary >>, Acc) ->
  31. parse_cookie(Rest, Acc);
  32. parse_cookie(<< $,, Rest/binary >>, Acc) ->
  33. parse_cookie(Rest, Acc);
  34. parse_cookie(<< $;, Rest/binary >>, Acc) ->
  35. parse_cookie(Rest, Acc);
  36. parse_cookie(<< $$, Rest/binary >>, Acc) ->
  37. skip_cookie(Rest, Acc);
  38. parse_cookie(Cookie, Acc) ->
  39. parse_cookie_name(Cookie, Acc, <<>>).
  40. skip_cookie(<<>>, Acc) ->
  41. lists:reverse(Acc);
  42. skip_cookie(<< $,, Rest/binary >>, Acc) ->
  43. parse_cookie(Rest, Acc);
  44. skip_cookie(<< $;, Rest/binary >>, Acc) ->
  45. parse_cookie(Rest, Acc);
  46. skip_cookie(<< _, Rest/binary >>, Acc) ->
  47. skip_cookie(Rest, Acc).
  48. parse_cookie_name(<<>>, _, _) ->
  49. {error, badarg};
  50. parse_cookie_name(<< $=, _/binary >>, _, <<>>) ->
  51. {error, badarg};
  52. parse_cookie_name(<< $=, Rest/binary >>, Acc, Name) ->
  53. parse_cookie_value(Rest, Acc, Name, <<>>);
  54. parse_cookie_name(<< $,, _/binary >>, _, _) ->
  55. {error, badarg};
  56. parse_cookie_name(<< $;, _/binary >>, _, _) ->
  57. {error, badarg};
  58. parse_cookie_name(<< $\s, _/binary >>, _, _) ->
  59. {error, badarg};
  60. parse_cookie_name(<< $\t, _/binary >>, _, _) ->
  61. {error, badarg};
  62. parse_cookie_name(<< $\r, _/binary >>, _, _) ->
  63. {error, badarg};
  64. parse_cookie_name(<< $\n, _/binary >>, _, _) ->
  65. {error, badarg};
  66. parse_cookie_name(<< $\013, _/binary >>, _, _) ->
  67. {error, badarg};
  68. parse_cookie_name(<< $\014, _/binary >>, _, _) ->
  69. {error, badarg};
  70. parse_cookie_name(<< C, Rest/binary >>, Acc, Name) ->
  71. parse_cookie_name(Rest, Acc, << Name/binary, C >>).
  72. parse_cookie_value(<<>>, Acc, Name, Value) ->
  73. lists:reverse([{Name, parse_cookie_trim(Value)}|Acc]);
  74. parse_cookie_value(<< $;, Rest/binary >>, Acc, Name, Value) ->
  75. parse_cookie(Rest, [{Name, parse_cookie_trim(Value)}|Acc]);
  76. parse_cookie_value(<< $\t, _/binary >>, _, _, _) ->
  77. {error, badarg};
  78. parse_cookie_value(<< $\r, _/binary >>, _, _, _) ->
  79. {error, badarg};
  80. parse_cookie_value(<< $\n, _/binary >>, _, _, _) ->
  81. {error, badarg};
  82. parse_cookie_value(<< $\013, _/binary >>, _, _, _) ->
  83. {error, badarg};
  84. parse_cookie_value(<< $\014, _/binary >>, _, _, _) ->
  85. {error, badarg};
  86. parse_cookie_value(<< C, Rest/binary >>, Acc, Name, Value) ->
  87. parse_cookie_value(Rest, Acc, Name, << Value/binary, C >>).
  88. parse_cookie_trim(Value = <<>>) ->
  89. Value;
  90. parse_cookie_trim(Value) ->
  91. case binary:last(Value) of
  92. $\s ->
  93. Size = byte_size(Value) - 1,
  94. << Value2:Size/binary, _ >> = Value,
  95. parse_cookie_trim(Value2);
  96. _ ->
  97. Value
  98. end.
  99. -ifdef(TEST).
  100. parse_cookie_test_() ->
  101. %% {Value, Result}.
  102. Tests = [
  103. {<<"name=value; name2=value2">>, [
  104. {<<"name">>, <<"value">>},
  105. {<<"name2">>, <<"value2">>}
  106. ]},
  107. {<<"$Version=1; Customer=WILE_E_COYOTE; $Path=/acme">>, [
  108. {<<"Customer">>, <<"WILE_E_COYOTE">>}
  109. ]},
  110. {<<"$Version=1; Customer=WILE_E_COYOTE; $Path=/acme; "
  111. "Part_Number=Rocket_Launcher_0001; $Path=/acme; "
  112. "Shipping=FedEx; $Path=/acme">>, [
  113. {<<"Customer">>, <<"WILE_E_COYOTE">>},
  114. {<<"Part_Number">>, <<"Rocket_Launcher_0001">>},
  115. {<<"Shipping">>, <<"FedEx">>}
  116. ]},
  117. %% Space in value.
  118. {<<"foo=Thu Jul 11 2013 15:38:43 GMT+0400 (MSK)">>,
  119. [{<<"foo">>, <<"Thu Jul 11 2013 15:38:43 GMT+0400 (MSK)">>}]},
  120. %% Comma in value. Google Analytics sets that kind of cookies.
  121. {<<"refk=sOUZDzq2w2; sk=B602064E0139D842D620C7569640DBB4C81C45080651"
  122. "9CC124EF794863E10E80; __utma=64249653.825741573.1380181332.1400"
  123. "015657.1400019557.703; __utmb=64249653.1.10.1400019557; __utmc="
  124. "64249653; __utmz=64249653.1400019557.703.13.utmcsr=bluesky.chic"
  125. "agotribune.com|utmccn=(referral)|utmcmd=referral|utmcct=/origin"
  126. "als/chi-12-indispensable-digital-tools-bsi,0,0.storygallery">>, [
  127. {<<"refk">>, <<"sOUZDzq2w2">>},
  128. {<<"sk">>, <<"B602064E0139D842D620C7569640DBB4C81C45080651"
  129. "9CC124EF794863E10E80">>},
  130. {<<"__utma">>, <<"64249653.825741573.1380181332.1400"
  131. "015657.1400019557.703">>},
  132. {<<"__utmb">>, <<"64249653.1.10.1400019557">>},
  133. {<<"__utmc">>, <<"64249653">>},
  134. {<<"__utmz">>, <<"64249653.1400019557.703.13.utmcsr=bluesky.chic"
  135. "agotribune.com|utmccn=(referral)|utmcmd=referral|utmcct=/origin"
  136. "als/chi-12-indispensable-digital-tools-bsi,0,0.storygallery">>}
  137. ]},
  138. %% Potential edge cases (initially from Mochiweb).
  139. {<<"foo=\\x">>, [{<<"foo">>, <<"\\x">>}]},
  140. {<<"=">>, {error, badarg}},
  141. {<<" foo ; bar ">>, {error, badarg}},
  142. {<<"foo=;bar=">>, [{<<"foo">>, <<>>}, {<<"bar">>, <<>>}]},
  143. {<<"foo=\\\";;bar ">>, {error, badarg}},
  144. {<<"foo=\\\";;bar=good ">>,
  145. [{<<"foo">>, <<"\\\"">>}, {<<"bar">>, <<"good">>}]},
  146. {<<"foo=\"\\\";bar">>, {error, badarg}},
  147. {<<>>, []},
  148. {<<"foo=bar , baz=wibble ">>, [{<<"foo">>, <<"bar , baz=wibble">>}]}
  149. ],
  150. [{V, fun() -> R = parse_cookie(V) end} || {V, R} <- Tests].
  151. -endif.
  152. %% @doc Convert a cookie name, value and options to its iodata form.
  153. %% @end
  154. %%
  155. %% Initially from Mochiweb:
  156. %% * Copyright 2007 Mochi Media, Inc.
  157. %% Initial binary implementation:
  158. %% * Copyright 2011 Thomas Burdick <thomas.burdick@gmail.com>
  159. -spec setcookie(iodata(), iodata(), cookie_opts()) -> iodata().
  160. setcookie(Name, Value, Opts) ->
  161. nomatch = binary:match(iolist_to_binary(Name), [<<$=>>, <<$,>>, <<$;>>,
  162. <<$\s>>, <<$\t>>, <<$\r>>, <<$\n>>, <<$\013>>, <<$\014>>]),
  163. nomatch = binary:match(iolist_to_binary(Value), [<<$,>>, <<$;>>,
  164. <<$\s>>, <<$\t>>, <<$\r>>, <<$\n>>, <<$\013>>, <<$\014>>]),
  165. MaxAgeBin = case lists:keyfind(max_age, 1, Opts) of
  166. false -> <<>>;
  167. {_, 0} ->
  168. %% MSIE requires an Expires date in the past to delete a cookie.
  169. <<"; Expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0">>;
  170. {_, MaxAge} when is_integer(MaxAge), MaxAge > 0 ->
  171. UTC = calendar:universal_time(),
  172. Secs = calendar:datetime_to_gregorian_seconds(UTC),
  173. Expires = calendar:gregorian_seconds_to_datetime(Secs + MaxAge),
  174. [<<"; Expires=">>, cow_date:rfc2109(Expires),
  175. <<"; Max-Age=">>, integer_to_list(MaxAge)]
  176. end,
  177. DomainBin = case lists:keyfind(domain, 1, Opts) of
  178. false -> <<>>;
  179. {_, Domain} -> [<<"; Domain=">>, Domain]
  180. end,
  181. PathBin = case lists:keyfind(path, 1, Opts) of
  182. false -> <<>>;
  183. {_, Path} -> [<<"; Path=">>, Path]
  184. end,
  185. SecureBin = case lists:keyfind(secure, 1, Opts) of
  186. false -> <<>>;
  187. {_, true} -> <<"; Secure">>
  188. end,
  189. HttpOnlyBin = case lists:keyfind(http_only, 1, Opts) of
  190. false -> <<>>;
  191. {_, true} -> <<"; HttpOnly">>
  192. end,
  193. [Name, <<"=">>, Value, <<"; Version=1">>,
  194. MaxAgeBin, DomainBin, PathBin, SecureBin, HttpOnlyBin].
  195. -ifdef(TEST).
  196. setcookie_test_() ->
  197. %% {Name, Value, Opts, Result}
  198. Tests = [
  199. {<<"Customer">>, <<"WILE_E_COYOTE">>,
  200. [{http_only, true}, {domain, <<"acme.com">>}],
  201. <<"Customer=WILE_E_COYOTE; Version=1; "
  202. "Domain=acme.com; HttpOnly">>},
  203. {<<"Customer">>, <<"WILE_E_COYOTE">>,
  204. [{path, <<"/acme">>}],
  205. <<"Customer=WILE_E_COYOTE; Version=1; Path=/acme">>},
  206. {<<"Customer">>, <<"WILE_E_COYOTE">>,
  207. [{path, <<"/acme">>}, {badoption, <<"negatory">>}],
  208. <<"Customer=WILE_E_COYOTE; Version=1; Path=/acme">>}
  209. ],
  210. [{R, fun() -> R = iolist_to_binary(setcookie(N, V, O)) end}
  211. || {N, V, O, R} <- Tests].
  212. setcookie_max_age_test() ->
  213. F = fun(N, V, O) ->
  214. binary:split(iolist_to_binary(
  215. setcookie(N, V, O)), <<";">>, [global])
  216. end,
  217. [<<"Customer=WILE_E_COYOTE">>,
  218. <<" Version=1">>,
  219. <<" Expires=", _/binary>>,
  220. <<" Max-Age=111">>,
  221. <<" Secure">>] = F(<<"Customer">>, <<"WILE_E_COYOTE">>,
  222. [{max_age, 111}, {secure, true}]),
  223. case catch F(<<"Customer">>, <<"WILE_E_COYOTE">>, [{max_age, -111}]) of
  224. {'EXIT', {{case_clause, {max_age, -111}}, _}} -> ok
  225. end,
  226. [<<"Customer=WILE_E_COYOTE">>,
  227. <<" Version=1">>,
  228. <<" Expires=", _/binary>>,
  229. <<" Max-Age=86417">>] = F(<<"Customer">>, <<"WILE_E_COYOTE">>,
  230. [{max_age, 86417}]),
  231. ok.
  232. setcookie_failures_test_() ->
  233. F = fun(N, V) ->
  234. try setcookie(N, V, []) of
  235. _ ->
  236. false
  237. catch _:_ ->
  238. true
  239. end
  240. end,
  241. Tests = [
  242. {<<"Na=me">>, <<"Value">>},
  243. {<<"Name;">>, <<"Value">>},
  244. {<<"\r\name">>, <<"Value">>},
  245. {<<"Name">>, <<"Value;">>},
  246. {<<"Name">>, <<"\value">>}
  247. ],
  248. [{iolist_to_binary(io_lib:format("{~p, ~p} failure", [N, V])),
  249. fun() -> true = F(N, V) end}
  250. || {N, V} <- Tests].
  251. -endif.