/src/utils/ewgi_util_cookie.erl

http://github.com/skarab/ewgi · Erlang · 211 lines · 158 code · 30 blank · 23 comment · 2 complexity · f2c61b7a867b5a6654e0b8c7f9e91f8d MD5 · raw file

  1. %% @author Emad El-Haraty <emad@mochimedia.com>
  2. %% @copyright 2007 Mochi Media, Inc.
  3. %%
  4. %% @author Hunter Morris <hunter.morris@smarkets.com>
  5. %% @copyright 2009 Smarkets Limited.
  6. %%
  7. %% Licensed under the MIT license:
  8. %% http://www.opensource.org/licenses/mit-license.php
  9. %% @doc HTTP Cookie parsing (RFC 2109, RFC 2965)
  10. %% - parse_cookie/1 shamelessly ripped off mochiweb.
  11. %% - simple_cookie/[3,4] moved off smak_auth_cookie.
  12. -module(ewgi_util_cookie).
  13. -export([parse_cookie/1, test/0]).
  14. -export([cookie_headers/5, cookie_safe_encode/1, cookie_safe_decode/1, get_domains/1]).
  15. -include("ewgi.hrl").
  16. -define(QUOTE, $\").
  17. -define(IS_WHITESPACE(C),
  18. (C =:= $\s orelse C =:= $\t orelse C =:= $\r orelse C =:= $\n)).
  19. %% RFC 2616 separators (called tspecials in RFC 2068)
  20. -define(IS_SEPARATOR(C),
  21. (C < 32 orelse
  22. C =:= $\s orelse C =:= $\t orelse
  23. C =:= $( orelse C =:= $) orelse C =:= $< orelse C =:= $> orelse
  24. C =:= $@ orelse C =:= $, orelse C =:= $; orelse C =:= $: orelse
  25. C =:= $\\ orelse C =:= $\" orelse C =:= $/ orelse
  26. C =:= $[ orelse C =:= $] orelse C =:= $? orelse C =:= $= orelse
  27. C =:= ${ orelse C =:= $})).
  28. -define(COOKIE_DELETE_TRAILER, "; Expires=Thu, 01 Jan 1970 23:00:00 GMT; Max-Age=0").
  29. %% @spec parse_cookie(string()) -> [{K::string(), V::string()}]
  30. %% @doc Parse the contents of a Cookie header field, ignoring cookie
  31. %% attributes, and return a simple property list.
  32. parse_cookie("") ->
  33. [];
  34. parse_cookie(Cookie) ->
  35. parse_cookie(Cookie, []).
  36. %% @spec test() -> ok
  37. %% @doc Run tests for mochiweb_cookies.
  38. test() ->
  39. parse_cookie_test(),
  40. ok.
  41. %% Internal API
  42. parse_cookie([], Acc) ->
  43. lists:reverse(Acc);
  44. parse_cookie(String, Acc) ->
  45. {{Token, Value}, Rest} = read_pair(String),
  46. Acc1 = case Token of
  47. "" ->
  48. Acc;
  49. "$" ++ _ ->
  50. Acc;
  51. _ ->
  52. [{Token, Value} | Acc]
  53. end,
  54. parse_cookie(Rest, Acc1).
  55. read_pair(String) ->
  56. {Token, Rest} = read_token(skip_whitespace(String)),
  57. {Value, Rest1} = read_value(skip_whitespace(Rest)),
  58. {{Token, Value}, skip_past_separator(Rest1)}.
  59. read_value([$= | Value]) ->
  60. Value1 = skip_whitespace(Value),
  61. case Value1 of
  62. [?QUOTE | _] ->
  63. read_quoted(Value1);
  64. _ ->
  65. read_token(Value1)
  66. end;
  67. read_value(String) ->
  68. {"", String}.
  69. read_quoted([?QUOTE | String]) ->
  70. read_quoted(String, []).
  71. read_quoted([], Acc) ->
  72. {lists:reverse(Acc), []};
  73. read_quoted([?QUOTE | Rest], Acc) ->
  74. {lists:reverse(Acc), Rest};
  75. read_quoted([$\\, Any | Rest], Acc) ->
  76. read_quoted(Rest, [Any | Acc]);
  77. read_quoted([C | Rest], Acc) ->
  78. read_quoted(Rest, [C | Acc]).
  79. skip_whitespace(String) ->
  80. F = fun (C) -> ?IS_WHITESPACE(C) end,
  81. lists:dropwhile(F, String).
  82. read_token(String) ->
  83. F = fun (C) -> not ?IS_SEPARATOR(C) end,
  84. lists:splitwith(F, String).
  85. skip_past_separator([]) ->
  86. [];
  87. skip_past_separator([$; | Rest]) ->
  88. Rest;
  89. skip_past_separator([$, | Rest]) ->
  90. Rest;
  91. skip_past_separator([_ | Rest]) ->
  92. skip_past_separator(Rest).
  93. parse_cookie_test() ->
  94. %% RFC example
  95. C1 = "$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\";
  96. Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\";
  97. Shipping=\"FedEx\"; $Path=\"/acme\"",
  98. [
  99. {"Customer","WILE_E_COYOTE"},
  100. {"Part_Number","Rocket_Launcher_0001"},
  101. {"Shipping","FedEx"}
  102. ] = parse_cookie(C1),
  103. %% Potential edge cases
  104. [{"foo", "x"}] = parse_cookie("foo=\"\\x\""),
  105. [] = parse_cookie("="),
  106. [{"foo", ""}, {"bar", ""}] = parse_cookie(" foo ; bar "),
  107. [{"foo", ""}, {"bar", ""}] = parse_cookie("foo=;bar="),
  108. [{"foo", "\";"}, {"bar", ""}] = parse_cookie("foo = \"\\\";\";bar "),
  109. [{"foo", "\";bar"}] = parse_cookie("foo=\"\\\";bar").
  110. %%====================================================================
  111. %% Functions for setting the cookie headers in the ewgi_response()
  112. %%====================================================================
  113. cookie_headers(Ctx, CookieName, CookieVal, Path, Sec) ->
  114. {CurDomain, WildDomain} = get_domains(Ctx),
  115. SessionHeaders =
  116. [simple_cookie(CookieName, CookieVal, Path, Sec),
  117. simple_cookie(CookieName, CookieVal, Path, Sec, CurDomain),
  118. simple_cookie(CookieName, CookieVal, Path, Sec, WildDomain)],
  119. OldHeaders = ewgi_api:response_headers(Ctx),
  120. Headers = SessionHeaders ++ remove_cookie_headers(CookieName, OldHeaders, []),
  121. ewgi_api:response_headers(Headers, Ctx).
  122. -spec get_domains(ewgi_context()) -> {string(), string()}.
  123. get_domains(Ctx) ->
  124. Cur = case ewgi_api:get_header_value("host", Ctx) of
  125. undefined ->
  126. ewgi_api:server_name(Ctx);
  127. H ->
  128. H
  129. end,
  130. Wild = [$.|Cur],
  131. {Cur, Wild}.
  132. remove_cookie_headers(_, [], Acc) ->
  133. Acc;
  134. remove_cookie_headers(CookieName, [{"Set-Cookie", [CookieName|_]}|R], Acc) ->
  135. remove_cookie_headers(CookieName, R, Acc);
  136. remove_cookie_headers(CookieName, [H|R], Acc) ->
  137. remove_cookie_headers(CookieName, R, [H|Acc]).
  138. -spec simple_cookie(string(), string() | binary(), string(), boolean()) -> {string(), iolist()}.
  139. simple_cookie(Name, Val, Path, Sec) when is_binary(Val) ->
  140. simple_cookie(Name, binary_to_list(Val), Path, Sec);
  141. simple_cookie(Name, Val, Path, Sec) when is_list(Name), is_list(Val), is_list(Path) ->
  142. S = if Sec -> "; Secure"; true -> [] end,
  143. Exp = case Val of [] -> ?COOKIE_DELETE_TRAILER; _ -> [] end,
  144. {"Set-Cookie", [Name, $=, Val, "; Path=", Path, S, Exp]}.
  145. -spec simple_cookie(string(), binary() | string(), boolean(), binary() | string(), string()) -> {string(), iolist()}.
  146. simple_cookie(Name, Val, Path, Sec, Domain) when is_binary(Val) ->
  147. simple_cookie(Name, binary_to_list(Val), Path, Sec, Domain);
  148. simple_cookie(Name, Val, Path, Sec, Domain) when is_binary(Domain) ->
  149. simple_cookie(Name, Val, Path, Sec, binary_to_list(Domain));
  150. simple_cookie(Name, Val, Path, Sec, Domain) ->
  151. S = if Sec -> "; Secure"; true -> [] end,
  152. Exp = case Val of [] -> ?COOKIE_DELETE_TRAILER; _ -> [] end,
  153. {"Set-Cookie", io_lib:format("~s=~s; Path=~s; Domain=~s~s~s", [Name, Val, Path, Domain, S, Exp])}.
  154. -spec cookie_safe_encode(binary()) -> binary().
  155. cookie_safe_encode(Bin) when is_binary(Bin) ->
  156. Enc = binary_to_list(base64:encode(Bin)),
  157. list_to_binary(cookie_safe_encode1(Enc, [])).
  158. -spec cookie_safe_encode1(string(), string()) -> string().
  159. cookie_safe_encode1([], Acc) ->
  160. lists:reverse(Acc);
  161. cookie_safe_encode1([$=|Rest], Acc) ->
  162. cookie_safe_encode1(Rest, [$~|Acc]);
  163. cookie_safe_encode1([$/|Rest], Acc) ->
  164. cookie_safe_encode1(Rest, [$_|Acc]);
  165. cookie_safe_encode1([C|Rest], Acc) ->
  166. cookie_safe_encode1(Rest, [C|Acc]).
  167. -spec cookie_safe_decode(binary() | list()) -> binary().
  168. cookie_safe_decode(Bin) when is_binary(Bin) ->
  169. cookie_safe_decode(binary_to_list(Bin));
  170. cookie_safe_decode(L) when is_list(L) ->
  171. Dec = cookie_safe_decode1(L, []),
  172. base64:decode(Dec).
  173. -spec cookie_safe_decode1(string(), list()) -> list().
  174. cookie_safe_decode1([], Acc) ->
  175. lists:reverse(Acc);
  176. cookie_safe_decode1([$~|Rest], Acc) ->
  177. cookie_safe_decode1(Rest, [$=|Acc]);
  178. cookie_safe_decode1([$_|Rest], Acc) ->
  179. cookie_safe_decode1(Rest, [$/|Acc]);
  180. cookie_safe_decode1([C|Rest], Acc) ->
  181. cookie_safe_decode1(Rest, [C|Acc]).