Browse Source

Make cookies use universal time instead of local time

Includes:
  * cowboy_clock:rfc2109/1 now expects UTC datetime
  * Rewrite of the cookie code to cowboy_http
  * Removal of cowboy_cookies
  * Add type cowboy_req:cookie_opts/0

Cookies should now be set using cowboy_req:set_resp_cookie/3.
Code calling cowboy_cookies directly will need to be updated.
Loïc Hoguin 12 years ago
parent
commit
27da09282d
4 changed files with 191 additions and 468 deletions
  1. 13 33
      src/cowboy_clock.erl
  2. 0 416
      src/cowboy_cookies.erl
  3. 159 6
      src/cowboy_http.erl
  4. 19 13
      src/cowboy_req.erl

+ 13 - 33
src/cowboy_clock.erl

@@ -76,39 +76,12 @@ rfc1123(DateTime) ->
 %% This format is used in the <em>set-cookie</em> header sent with
 %% This format is used in the <em>set-cookie</em> header sent with
 %% HTTP responses.
 %% HTTP responses.
 -spec rfc2109(calendar:datetime()) -> binary().
 -spec rfc2109(calendar:datetime()) -> binary().
-rfc2109(LocalTime) ->
-	{{YYYY,MM,DD},{Hour,Min,Sec}} =
-	case calendar:local_time_to_universal_time_dst(LocalTime) of
-	    [Gmt]   -> Gmt;
-	    [_,Gmt] -> Gmt;
-		[] ->
-			%% The localtime generated by cowboy_cookies may fall within
-			%% the hour that is skipped by daylight savings time. If this
-			%% is such a localtime, increment the localtime with one hour
-			%% and try again, if this succeeds, subtracting the max_age
-			%% from the resulting universaltime and converting to a local
-			%% time will yield the original localtime.
-			{Date, {Hour1, Min1, Sec1}} = LocalTime,
-			LocalTime2 = {Date, {Hour1 + 1, Min1, Sec1}},
-			case calendar:local_time_to_universal_time_dst(LocalTime2) of
-				[Gmt]   -> Gmt;
-				[_,Gmt] -> Gmt
-			end
-	end,
-	Wday = calendar:day_of_the_week({YYYY,MM,DD}),
-	DayBin = pad_int(DD),
-	YearBin = list_to_binary(integer_to_list(YYYY)),
-	HourBin = pad_int(Hour),
-	MinBin = pad_int(Min),
-	SecBin = pad_int(Sec),
-	WeekDay = weekday(Wday),
-	Month = month(MM),
-	<<WeekDay/binary, ", ",
-	DayBin/binary, " ", Month/binary, " ",
-	YearBin/binary, " ",
-	HourBin/binary, ":",
-	MinBin/binary, ":",
-	SecBin/binary, " GMT">>.
+rfc2109({Date = {Y, Mo, D}, {H, Mi, S}}) ->
+	Wday = calendar:day_of_the_week(Date),
+	<< (weekday(Wday))/binary, ", ", (pad_int(D))/binary, "-",
+		(month(Mo))/binary, "-", (list_to_binary(integer_to_list(Y)))/binary,
+		" ", (pad_int(H))/binary, $:, (pad_int(Mi))/binary,
+		$:, (pad_int(S))/binary, " GMT" >>.
 
 
 %% gen_server.
 %% gen_server.
 
 
@@ -219,6 +192,13 @@ month(12) -> <<"Dec">>.
 
 
 -ifdef(TEST).
 -ifdef(TEST).
 
 
+rfc2109_test_() ->
+	Tests = [
+		{<<"Sat, 14-May-2011 14:25:33 GMT">>, {{2011, 5, 14}, {14, 25, 33}}},
+		{<<"Sun, 01-Jan-2012 00:00:00 GMT">>, {{2012, 1,  1}, { 0,  0,  0}}}
+	],
+	[{R, fun() -> R = rfc2109(D) end} || {R, D} <- Tests].
+
 update_rfc1123_test_() ->
 update_rfc1123_test_() ->
 	Tests = [
 	Tests = [
 		{<<"Sat, 14 May 2011 14:25:33 GMT">>, undefined,
 		{<<"Sat, 14 May 2011 14:25:33 GMT">>, undefined,

+ 0 - 416
src/cowboy_cookies.erl

@@ -1,416 +0,0 @@
-%% Copyright 2007 Mochi Media, Inc.
-%% Copyright 2011 Thomas Burdick <thomas.burdick@gmail.com>
-%%
-%% Permission to use, copy, modify, and/or distribute this software for any
-%% purpose with or without fee is hereby granted, provided that the above
-%% copyright notice and this permission notice appear in all copies.
-%%
-%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-%% @doc HTTP Cookie parsing and generating (RFC 2965).
-
--module(cowboy_cookies).
-
-%% API.
--export([parse_cookie/1]).
--export([cookie/3]).
--export([cookie/2]).
-
-%% Types.
--type kv() :: {Name::binary(), Value::binary()}.
--type kvlist() :: [kv()].
--type cookie_option() :: {max_age, integer()}
-				| {local_time, calendar:datetime()}
-				| {domain, binary()} | {path, binary()}
-				| {secure, true | false} | {http_only, true | false}.
-
--export_type([kv/0]).
--export_type([kvlist/0]).
--export_type([cookie_option/0]).
-
--define(QUOTE, $\").
-
--ifdef(TEST).
--include_lib("eunit/include/eunit.hrl").
--endif.
-
-%% API.
-
-%% @doc Parse the contents of a Cookie header field, ignoring cookie
-%% attributes, and return a simple property list.
--spec parse_cookie(binary()) -> kvlist().
-parse_cookie(<<>>) ->
-	[];
-parse_cookie(Cookie) when is_binary(Cookie) ->
-	parse_cookie(Cookie, []).
-
-%% @equiv cookie(Key, Value, [])
--spec cookie(binary(), binary()) -> kv().
-cookie(Key, Value) when is_binary(Key) andalso is_binary(Value) ->
-	cookie(Key, Value, []).
-
-%% @doc Generate a Set-Cookie header field tuple.
--spec cookie(binary(), binary(), [cookie_option()]) -> kv().
-cookie(Key, Value, Options) when is_binary(Key)
-		andalso is_binary(Value) andalso is_list(Options) ->
-	Cookie = <<(any_to_binary(Key))/binary, "=",
-		(quote(Value))/binary, "; Version=1">>,
-	%% Set-Cookie:
-	%%    Comment, Domain, Max-Age, Path, Secure, Version
-	ExpiresPart =
-		case proplists:get_value(max_age, Options) of
-			undefined ->
-				<<"">>;
-			RawAge ->
-				When = case proplists:get_value(local_time, Options) of
-						undefined ->
-							calendar:local_time();
-						LocalTime ->
-							LocalTime
-					end,
-				Age = case RawAge < 0 of
-						true ->
-							0;
-						false ->
-							RawAge
-					end,
-				AgeBinary = quote(Age),
-				CookieDate = age_to_cookie_date(Age, When),
-				<<"; Expires=", CookieDate/binary,
-				"; Max-Age=", AgeBinary/binary>>
-		end,
-	SecurePart =
-		case proplists:get_value(secure, Options) of
-			true ->
-				<<"; Secure">>;
-			_ ->
-				<<"">>
-		end,
-	DomainPart =
-		case proplists:get_value(domain, Options) of
-			undefined ->
-				<<"">>;
-			Domain ->
-				<<"; Domain=", (quote(Domain))/binary>>
-		end,
-	PathPart =
-		case proplists:get_value(path, Options) of
-			undefined ->
-				<<"">>;
-			Path ->
-				<<"; Path=", (quote(Path, true))/binary>>
-		end,
-	HttpOnlyPart =
-		case proplists:get_value(http_only, Options) of
-			true ->
-				<<"; HttpOnly">>;
-			_ ->
-				<<"">>
-		end,
-	CookieParts = <<Cookie/binary, ExpiresPart/binary, SecurePart/binary,
-		DomainPart/binary, PathPart/binary, HttpOnlyPart/binary>>,
-	{<<"set-cookie">>, CookieParts}.
-
-%% Internal.
-
-%% @doc Check if a character is a white space character.
--spec is_whitespace(char()) -> boolean().
-is_whitespace($\s) -> true;
-is_whitespace($\t) -> true;
-is_whitespace($\r) -> true;
-is_whitespace($\n) -> true;
-is_whitespace(_) -> false.
-
-%% @doc Check if a character is a separator.
--spec is_separator(char()) -> boolean().
-is_separator(C) when C < 32 -> true;
-is_separator($\s) -> true;
-is_separator($\t) -> true;
-is_separator($() -> true;
-is_separator($)) -> true;
-is_separator($<) -> true;
-is_separator($>) -> true;
-is_separator($@) -> true;
-is_separator($,) -> true;
-is_separator($;) -> true;
-is_separator($:) -> true;
-is_separator($\\) -> true;
-is_separator(?QUOTE) -> true;
-is_separator($/) -> true;
-is_separator($[) -> true;
-is_separator($]) -> true;
-is_separator($?) -> true;
-is_separator($=) -> true;
-is_separator(${) -> true;
-is_separator($}) -> true;
-is_separator(_) -> false.
-
-%% @doc Check if a binary has an ASCII separator character.
--spec has_separator(binary(), boolean()) -> boolean().
-has_separator(<<>>, _) ->
-	false;
-has_separator(<<$/, Rest/binary>>, true) ->
-	has_separator(Rest, true);
-has_separator(<<C, Rest/binary>>, IgnoreSlash) ->
-	case is_separator(C) of
-		true ->
-			true;
-		false ->
-			has_separator(Rest, IgnoreSlash)
-	end.
-
-%% @doc Convert to a binary and raise an error if quoting is required. Quoting
-%% is broken in different ways for different browsers. Its better to simply
-%% avoiding doing it at all.
-%% @end
--spec quote(term(), boolean()) -> binary().
-quote(V0, IgnoreSlash) ->
-	V = any_to_binary(V0),
-	case has_separator(V, IgnoreSlash) of
-		true ->
-			erlang:error({cookie_quoting_required, V});
-		false ->
-			V
-	end.
-
-%% @equiv quote(Bin, false)
--spec quote(term()) -> binary().
-quote(V0) ->
-	quote(V0, false).
-
--spec add_seconds(integer(), calendar:datetime()) -> calendar:datetime().
-add_seconds(Secs, LocalTime) ->
-	Greg = calendar:datetime_to_gregorian_seconds(LocalTime),
-	calendar:gregorian_seconds_to_datetime(Greg + Secs).
-
--spec age_to_cookie_date(integer(), calendar:datetime()) -> binary().
-age_to_cookie_date(Age, LocalTime) ->
-	cowboy_clock:rfc2109(add_seconds(Age, LocalTime)).
-
--spec parse_cookie(binary(), kvlist()) -> kvlist().
-parse_cookie(<<>>, Acc) ->
-	lists:reverse(Acc);
-parse_cookie(String, Acc) ->
-	{{Token, Value}, Rest} = read_pair(String),
-	Acc1 = case Token of
-			<<"">> ->
-				Acc;
-			<<"$", _R/binary>> ->
-				Acc;
-			_ ->
-				[{Token, Value} | Acc]
-		end,
-	parse_cookie(Rest, Acc1).
-
--spec read_pair(binary()) -> {{binary(), binary()}, binary()}.
-read_pair(String) ->
-	{Token, Rest} = read_token(skip_whitespace(String)),
-	{Value, Rest1} = read_value(skip_whitespace(Rest)),
-	{{Token, Value}, skip_past_separator(Rest1)}.
-
--spec read_value(binary()) -> {binary(), binary()}.
-read_value(<<"=",  Value/binary>>) ->
-	Value1 = skip_whitespace(Value),
-	case Value1 of
-		<<?QUOTE, _R/binary>> ->
-			read_quoted(Value1);
-		_ ->
-			read_token(Value1)
-	end;
-read_value(String) ->
-	{<<"">>, String}.
-
--spec read_quoted(binary()) -> {binary(), binary()}.
-read_quoted(<<?QUOTE, String/binary>>) ->
-	read_quoted(String, <<"">>).
-
--spec read_quoted(binary(), binary()) -> {binary(), binary()}.
-read_quoted(<<"">>, Acc) ->
-	{Acc, <<"">>};
-read_quoted(<<?QUOTE, Rest/binary>>, Acc) ->
-	{Acc, Rest};
-read_quoted(<<$\\, Any, Rest/binary>>, Acc) ->
-	read_quoted(Rest, <<Acc/binary, Any>>);
-read_quoted(<<C, Rest/binary>>, Acc) ->
-	read_quoted(Rest, <<Acc/binary, C>>).
-
-%% @doc Drop characters while a function returns true.
--spec binary_dropwhile(fun((char()) -> boolean()), binary()) -> binary().
-binary_dropwhile(_F, <<"">>) ->
-	<<"">>;
-binary_dropwhile(F, String) ->
-	<<C, Rest/binary>> = String,
-	case F(C) of
-		true ->
-			binary_dropwhile(F, Rest);
-		false ->
-			String
-	end.
-
-%% @doc Remove leading whitespace.
--spec skip_whitespace(binary()) -> binary().
-skip_whitespace(String) ->
-	binary_dropwhile(fun is_whitespace/1, String).
-
-%% @doc Split a binary when the current character causes F to return true.
--spec binary_splitwith(fun((char()) -> boolean()), binary(), binary())
-	-> {binary(), binary()}.
-binary_splitwith(_F, Head, <<>>) ->
-	{Head, <<>>};
-binary_splitwith(F, Head, Tail) ->
-	<<C, NTail/binary>> = Tail,
-	case F(C) of
-		true ->
-			{Head, Tail};
-		false ->
-			binary_splitwith(F, <<Head/binary, C>>, NTail)
-	end.
-
-%% @doc Split a binary with a function returning true or false on each char.
--spec binary_splitwith(fun((char()) -> boolean()), binary())
-	-> {binary(), binary()}.
-binary_splitwith(F, String) ->
-	binary_splitwith(F, <<>>, String).
-
-%% @doc Split the binary when the next separator is found.
--spec read_token(binary()) -> {binary(), binary()}.
-read_token(String) ->
-	binary_splitwith(fun is_separator/1, String).
-
-%% @doc Return string after ; or , characters.
--spec skip_past_separator(binary()) -> binary().
-skip_past_separator(<<"">>) ->
-	<<"">>;
-skip_past_separator(<<";", Rest/binary>>) ->
-	Rest;
-skip_past_separator(<<",", Rest/binary>>) ->
-	Rest;
-skip_past_separator(<<_C, Rest/binary>>) ->
-	skip_past_separator(Rest).
-
--spec any_to_binary(binary() | string() | atom() | integer()) -> binary().
-any_to_binary(V) when is_binary(V) ->
-	V;
-any_to_binary(V) when is_list(V) ->
-	erlang:list_to_binary(V);
-any_to_binary(V) when is_atom(V) ->
-	erlang:atom_to_binary(V, latin1);
-any_to_binary(V) when is_integer(V) ->
-	list_to_binary(integer_to_list(V)).
-
-%% Tests.
-
--ifdef(TEST).
-
-quote_test() ->
-	%% ?assertError eunit macro is not compatible with coverage module
-	_ = try quote(<<":wq">>)
-	catch error:{cookie_quoting_required, <<":wq">>} -> ok
-	end,
-	?assertEqual(<<"foo">>,quote(foo)),
-	_ = try quote(<<"/test/slashes/">>)
-	catch error:{cookie_quoting_required, <<"/test/slashes/">>} -> ok
-	end,
-	ok.
-
-parse_cookie_test() ->
-	%% RFC example
-	C1 = <<"$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\";
-	Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\";
-	Shipping=\"FedEx\"; $Path=\"/acme\"">>,
-	?assertEqual(
-	[{<<"Customer">>,<<"WILE_E_COYOTE">>},
-		{<<"Part_Number">>,<<"Rocket_Launcher_0001">>},
-		{<<"Shipping">>,<<"FedEx">>}],
-	parse_cookie(C1)),
-	%% Potential edge cases
-	?assertEqual(
-	[{<<"foo">>, <<"x">>}],
-	parse_cookie(<<"foo=\"\\x\"">>)),
-	?assertEqual(
-	[],
-	parse_cookie(<<"=">>)),
-	?assertEqual(
-	[{<<"foo">>, <<"">>}, {<<"bar">>, <<"">>}],
-	parse_cookie(<<"  foo ; bar  ">>)),
-	?assertEqual(
-	[{<<"foo">>, <<"">>}, {<<"bar">>, <<"">>}],
-	parse_cookie(<<"foo=;bar=">>)),
-	?assertEqual(
-	[{<<"foo">>, <<"\";">>}, {<<"bar">>, <<"">>}],
-	parse_cookie(<<"foo = \"\\\";\";bar ">>)),
-	?assertEqual(
-	[{<<"foo">>, <<"\";bar">>}],
-	parse_cookie(<<"foo=\"\\\";bar">>)),
-	?assertEqual(
-	[],
-	parse_cookie(<<"">>)),
-	?assertEqual(
-	[{<<"foo">>, <<"bar">>}, {<<"baz">>, <<"wibble">>}],
-	parse_cookie(<<"foo=bar , baz=wibble ">>)),
-	ok.
-
-domain_test() ->
-	?assertEqual(
-	{<<"set-cookie">>,
-		<<"Customer=WILE_E_COYOTE; "
-		"Version=1; "
-		"Domain=acme.com; "
-		"HttpOnly">>},
-	cookie(<<"Customer">>, <<"WILE_E_COYOTE">>,
-			[{http_only, true}, {domain, <<"acme.com">>}])),
-	ok.
-
-local_time_test() ->
-	{<<"set-cookie">>, B} = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>,
-							[{max_age, 111}, {secure, true}]),
-
-	?assertMatch(
-	[<<"Customer=WILE_E_COYOTE">>,
-		<<" Version=1">>,
-		<<" Expires=", _R/binary>>,
-		<<" Max-Age=111">>,
-		<<" Secure">>],
-	binary:split(B, <<";">>, [global])),
-	ok.
-
--spec cookie_test() -> no_return(). %% Not actually true, just a bad option.
-cookie_test() ->
-	C1 = {<<"set-cookie">>,
-		<<"Customer=WILE_E_COYOTE; "
-		"Version=1; "
-		"Path=/acme">>},
-	C1 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>, [{path, <<"/acme">>}]),
-
-	C1 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>,
-				[{path, <<"/acme">>}, {badoption, <<"negatory">>}]),
-
-	{<<"set-cookie">>,<<"=NoKey; Version=1">>}
-		= cookie(<<"">>, <<"NoKey">>, []),
-	{<<"set-cookie">>,<<"=NoKey; Version=1">>}
-		= cookie(<<"">>, <<"NoKey">>),
-	LocalTime = calendar:universal_time_to_local_time(
-		{{2007, 5, 15}, {13, 45, 33}}),
-	C2 = {<<"set-cookie">>,
-		<<"Customer=WILE_E_COYOTE; "
-		"Version=1; "
-		"Expires=Tue, 15 May 2007 13:45:33 GMT; "
-		"Max-Age=0">>},
-	C2 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>,
-				[{max_age, -111}, {local_time, LocalTime}]),
-	C3 = {<<"set-cookie">>,
-		<<"Customer=WILE_E_COYOTE; "
-		"Version=1; "
-		"Expires=Wed, 16 May 2007 13:45:50 GMT; "
-		"Max-Age=86417">>},
-	C3 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>,
-				[{max_age, 86417}, {local_time, LocalTime}]),
-	ok.
-
--endif.

+ 159 - 6
src/cowboy_http.erl

@@ -19,6 +19,7 @@
 %% Parsing.
 %% Parsing.
 -export([list/2]).
 -export([list/2]).
 -export([nonempty_list/2]).
 -export([nonempty_list/2]).
+-export([cookie_list/1]).
 -export([content_type/1]).
 -export([content_type/1]).
 -export([media_range/2]).
 -export([media_range/2]).
 -export([conneg/2]).
 -export([conneg/2]).
@@ -42,6 +43,7 @@
 -export([ce_identity/1]).
 -export([ce_identity/1]).
 
 
 %% Interpretation.
 %% Interpretation.
+-export([cookie_to_iodata/3]).
 -export([version_to_binary/1]).
 -export([version_to_binary/1]).
 -export([urldecode/1]).
 -export([urldecode/1]).
 -export([urldecode/2]).
 -export([urldecode/2]).
@@ -100,6 +102,33 @@ list(Data, Fun, Acc) ->
 				end)
 				end)
 		end).
 		end).
 
 
+%% @doc Parse a list of cookies.
+%%
+%% We need a special function for this because we need to support both
+%% $; and $, as separators as per RFC2109.
+-spec cookie_list(binary()) -> [{binary(), binary()}] | {error, badarg}.
+cookie_list(Data) ->
+	case cookie_list(Data, []) of
+		{error, badarg} -> {error, badarg};
+		[] -> {error, badarg};
+		L -> lists:reverse(L)
+	end.
+
+-spec cookie_list(binary(), Acc) -> Acc | {error, badarg}
+	when Acc::[{binary(), binary()}].
+cookie_list(Data, Acc) ->
+	whitespace(Data,
+		fun (<<>>) -> Acc;
+			(<< $,, Rest/binary >>) -> cookie_list(Rest, Acc);
+			(<< $;, Rest/binary >>) -> cookie_list(Rest, Acc);
+			(Rest) -> param(Rest,
+				fun (Rest2, << $$, _/bits >>, _) ->
+						cookie_list(Rest2, Acc);
+					(Rest2, Name, Value) ->
+						cookie_list(Rest2, [{Name, Value}|Acc])
+				end)
+		end).
+
 %% @doc Parse a content type.
 %% @doc Parse a content type.
 -spec content_type(binary()) -> any().
 -spec content_type(binary()) -> any().
 content_type(Data) ->
 content_type(Data) ->
@@ -341,12 +370,17 @@ params(Data, Fun) ->
 -spec params(binary(), fun(), [{binary(), binary()}]) -> any().
 -spec params(binary(), fun(), [{binary(), binary()}]) -> any().
 params(Data, Fun, Acc) ->
 params(Data, Fun, Acc) ->
 	whitespace(Data,
 	whitespace(Data,
-		fun (<< $;, Rest/binary >>) -> param(Rest, Fun, Acc);
-			(Rest) -> Fun(Rest, lists:reverse(Acc))
+		fun (<< $;, Rest/binary >>) ->
+				param(Rest,
+					fun (Rest2, Attr, Value) ->
+							params(Rest2, Fun, [{Attr, Value}|Acc])
+					end);
+			(Rest) ->
+				Fun(Rest, lists:reverse(Acc))
 		end).
 		end).
 
 
--spec param(binary(), fun(), [{binary(), binary()}]) -> any().
-param(Data, Fun, Acc) ->
+-spec param(binary(), fun()) -> any().
+param(Data, Fun) ->
 	whitespace(Data,
 	whitespace(Data,
 		fun (Rest) ->
 		fun (Rest) ->
 				token_ci(Rest,
 				token_ci(Rest,
@@ -354,8 +388,7 @@ param(Data, Fun, Acc) ->
 						(<< $=, Rest2/binary >>, Attr) ->
 						(<< $=, Rest2/binary >>, Attr) ->
 							word(Rest2,
 							word(Rest2,
 								fun (Rest3, Value) ->
 								fun (Rest3, Value) ->
-										params(Rest3, Fun,
-											[{Attr, Value}|Acc])
+										Fun(Rest3, Attr, Value)
 								end);
 								end);
 						(_Rest2, _Attr) -> {error, badarg}
 						(_Rest2, _Attr) -> {error, badarg}
 					end)
 					end)
@@ -772,6 +805,56 @@ ce_identity(Data) ->
 
 
 %% Interpretation.
 %% Interpretation.
 
 
+%% @doc Convert a cookie name, value and options to its iodata form.
+%% @end
+%%
+%% Initially from Mochiweb:
+%%   * Copyright 2007 Mochi Media, Inc.
+%% Initial binary implementation:
+%%   * Copyright 2011 Thomas Burdick <thomas.burdick@gmail.com>
+-spec cookie_to_iodata(iodata(), iodata(), cowboy_req:cookie_opts())
+	-> iodata().
+cookie_to_iodata(Name, Value, Opts) ->
+	MaxAgeBin = case lists:keyfind(max_age, 1, Opts) of
+		false -> <<>>;
+		{_, MaxAge} when is_integer(MaxAge), MaxAge >= 0 ->
+			UTC = calendar:universal_time(),
+			Secs = calendar:datetime_to_gregorian_seconds(UTC),
+			Expires = calendar:gregorian_seconds_to_datetime(Secs + MaxAge),
+			[<<"; Expires=">>, cowboy_clock:rfc2109(Expires),
+				<<"; Max-Age=">>, integer_to_list(MaxAge)]
+	end,
+	DomainBin = case lists:keyfind(domain, 1, Opts) of
+		false -> <<>>;
+		{_, Domain} -> [<<"; Domain=">>, quote(Domain)]
+	end,
+	PathBin = case lists:keyfind(path, 1, Opts) of
+		false -> <<>>;
+		{_, Path} -> [<<"; Path=">>, quote(Path)]
+	end,
+	SecureBin = case lists:keyfind(secure, 1, Opts) of
+		false -> <<>>;
+		{_, true} -> <<"; Secure">>
+	end,
+	HttpOnlyBin = case lists:keyfind(http_only, 1, Opts) of
+		false -> <<>>;
+		{_, true} -> <<"; HttpOnly">>
+	end,
+	[Name, <<"=">>, quote(Value), <<"; Version=1">>,
+		MaxAgeBin, DomainBin, PathBin, SecureBin, HttpOnlyBin].
+
+-spec quote(binary()) -> binary().
+quote(Bin) ->
+	quote(Bin, <<>>).
+
+-spec quote(binary(), binary()) -> binary().
+quote(<<>>, Acc) ->
+	Acc;
+quote(<< $", Rest/bits >>, Acc) ->
+	quote(Rest, << Acc/binary, $\\, $" >>);
+quote(<< C, Rest/bits >>, Acc) ->
+	quote(Rest, << Acc/binary, C >>).
+
 %% @doc Convert an HTTP version tuple to its binary form.
 %% @doc Convert an HTTP version tuple to its binary form.
 -spec version_to_binary(version()) -> binary().
 -spec version_to_binary(version()) -> binary().
 version_to_binary({1, 1}) -> <<"HTTP/1.1">>;
 version_to_binary({1, 1}) -> <<"HTTP/1.1">>;
@@ -927,6 +1010,38 @@ nonempty_token_list_test_() ->
 	],
 	],
 	[{V, fun() -> R = nonempty_list(V, fun token/2) end} || {V, R} <- Tests].
 	[{V, fun() -> R = nonempty_list(V, fun token/2) end} || {V, R} <- Tests].
 
 
+cookie_list_test_() ->
+	%% {Value, Result}.
+	Tests = [
+		{<<"name=value; name2=value2">>, [
+			{<<"name">>, <<"value">>},
+			{<<"name2">>, <<"value2">>}
+		]},
+		{<<"$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"">>, [
+			{<<"customer">>, <<"WILE_E_COYOTE">>}
+		]},
+		{<<"$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"; "
+			"Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"; "
+			"Shipping=\"FedEx\"; $Path=\"/acme\"">>, [
+			{<<"customer">>, <<"WILE_E_COYOTE">>},
+			{<<"part_number">>, <<"Rocket_Launcher_0001">>},
+			{<<"shipping">>, <<"FedEx">>}
+		]},
+		%% Potential edge cases (initially from Mochiweb).
+		{<<"foo=\"\\x\"">>, [{<<"foo">>, <<"x">>}]},
+		{<<"=">>, {error, badarg}},
+		{<<"  foo ; bar  ">>, {error, badarg}},
+		{<<"foo=;bar=">>, {error, badarg}},
+		{<<"foo=\"\\\";\";bar ">>, {error, badarg}},
+		{<<"foo=\"\\\";\";bar=good ">>,
+			[{<<"foo">>, <<"\";">>}, {<<"bar">>, <<"good">>}]},
+		{<<"foo=\"\\\";bar">>, {error, badarg}},
+		{<<"">>, {error, badarg}},
+		{<<"foo=bar , baz=wibble ">>,
+			[{<<"foo">>, <<"bar">>}, {<<"baz">>, <<"wibble">>}]}
+	],
+	[{V, fun() -> R = cookie_list(V) end} || {V, R} <- Tests].
+
 media_range_list_test_() ->
 media_range_list_test_() ->
 	%% {Tokens, Result}
 	%% {Tokens, Result}
 	Tests = [
 	Tests = [
@@ -1040,6 +1155,44 @@ digits_test_() ->
 	],
 	],
 	[{V, fun() -> R = digits(V) end} || {V, R} <- Tests].
 	[{V, fun() -> R = digits(V) end} || {V, R} <- Tests].
 
 
+cookie_to_iodata_test_() ->
+	%% {Name, Value, Opts, Result}
+	Tests = [
+		{<<"Customer">>, <<"WILE_E_COYOTE">>,
+			[{http_only, true}, {domain, <<"acme.com">>}],
+			<<"Customer=WILE_E_COYOTE; Version=1; "
+				"Domain=acme.com; HttpOnly">>},
+		{<<"Customer">>, <<"WILE_E_COYOTE">>,
+			[{path, <<"/acme">>}],
+			<<"Customer=WILE_E_COYOTE; Version=1; Path=/acme">>},
+		{<<"Customer">>, <<"WILE_E_COYOTE">>,
+			[{path, <<"/acme">>}, {badoption, <<"negatory">>}],
+			<<"Customer=WILE_E_COYOTE; Version=1; Path=/acme">>}
+	],
+	[{R, fun() -> R = iolist_to_binary(cookie_to_iodata(N, V, O)) end}
+		|| {N, V, O, R} <- Tests].
+
+cookie_to_iodata_max_age_test() ->
+	F = fun(N, V, O) ->
+		binary:split(iolist_to_binary(
+			cookie_to_iodata(N, V, O)), <<";">>, [global])
+	end,
+	[<<"Customer=WILE_E_COYOTE">>,
+		<<" Version=1">>,
+		<<" Expires=", _/binary>>,
+		<<" Max-Age=111">>,
+		<<" Secure">>] = F(<<"Customer">>, <<"WILE_E_COYOTE">>,
+			[{max_age, 111}, {secure, true}]),
+	case catch F(<<"Customer">>, <<"WILE_E_COYOTE">>, [{max_age, -111}]) of
+		{'EXIT', {{case_clause, {max_age, -111}}, _}} -> ok
+	end,
+	[<<"Customer=WILE_E_COYOTE">>,
+		<<" Version=1">>,
+		<<" Expires=", _/binary>>,
+		<<" Max-Age=86417">>] = F(<<"Customer">>, <<"WILE_E_COYOTE">>,
+			 [{max_age, 86417}]),
+	ok.
+
 x_www_form_urlencoded_test_() ->
 x_www_form_urlencoded_test_() ->
 	%% {Qs, Result}
 	%% {Qs, Result}
 	Tests = [
 	Tests = [

+ 19 - 13
src/cowboy_req.erl

@@ -118,6 +118,12 @@
 -include_lib("eunit/include/eunit.hrl").
 -include_lib("eunit/include/eunit.hrl").
 -endif.
 -endif.
 
 
+-type cookie_option() :: {max_age, non_neg_integer()}
+	| {domain, binary()} | {path, binary()}
+	| {secure, boolean()} | {http_only, boolean()}.
+-type cookie_opts() :: [cookie_option()].
+-export_type([cookie_opts/0]).
+
 -type resp_body_fun() :: fun(() -> {sent, non_neg_integer()}).
 -type resp_body_fun() :: fun(() -> {sent, non_neg_integer()}).
 
 
 -record(http_req, {
 -record(http_req, {
@@ -430,6 +436,8 @@ parse_header(Name, Req, Default) when Name =:= <<"content-length">> ->
 	parse_header(Name, Req, Default, fun cowboy_http:digits/1);
 	parse_header(Name, Req, Default, fun cowboy_http:digits/1);
 parse_header(Name, Req, Default) when Name =:= <<"content-type">> ->
 parse_header(Name, Req, Default) when Name =:= <<"content-type">> ->
 	parse_header(Name, Req, Default, fun cowboy_http:content_type/1);
 	parse_header(Name, Req, Default, fun cowboy_http:content_type/1);
+parse_header(Name = <<"cookie">>, Req, Default) ->
+	parse_header(Name, Req, Default, fun cowboy_http:cookie_list/1);
 parse_header(Name, Req, Default) when Name =:= <<"expect">> ->
 parse_header(Name, Req, Default) when Name =:= <<"expect">> ->
 	parse_header(Name, Req, Default,
 	parse_header(Name, Req, Default,
 		fun (Value) ->
 		fun (Value) ->
@@ -481,11 +489,10 @@ cookie(Name, Req) when is_binary(Name) ->
 -spec cookie(binary(), Req, Default)
 -spec cookie(binary(), Req, Default)
 	-> {binary() | true | Default, Req} when Req::req(), Default::any().
 	-> {binary() | true | Default, Req} when Req::req(), Default::any().
 cookie(Name, Req=#http_req{cookies=undefined}, Default) when is_binary(Name) ->
 cookie(Name, Req=#http_req{cookies=undefined}, Default) when is_binary(Name) ->
-	case header(<<"cookie">>, Req) of
-		{undefined, Req2} ->
+	case parse_header(<<"cookie">>, Req) of
+		{ok, undefined, Req2} ->
 			{Default, Req2#http_req{cookies=[]}};
 			{Default, Req2#http_req{cookies=[]}};
-		{RawCookie, Req2} ->
-			Cookies = cowboy_cookies:parse_cookie(RawCookie),
+		{ok, Cookies, Req2} ->
 			cookie(Name, Req2#http_req{cookies=Cookies}, Default)
 			cookie(Name, Req2#http_req{cookies=Cookies}, Default)
 	end;
 	end;
 cookie(Name, Req, Default) ->
 cookie(Name, Req, Default) ->
@@ -497,11 +504,10 @@ cookie(Name, Req, Default) ->
 %% @doc Return the full list of cookie values.
 %% @doc Return the full list of cookie values.
 -spec cookies(Req) -> {list({binary(), binary() | true}), Req} when Req::req().
 -spec cookies(Req) -> {list({binary(), binary() | true}), Req} when Req::req().
 cookies(Req=#http_req{cookies=undefined}) ->
 cookies(Req=#http_req{cookies=undefined}) ->
-	case header(<<"cookie">>, Req) of
-		{undefined, Req2} ->
+	case parse_header(<<"cookie">>, Req) of
+		{ok, undefined, Req2} ->
 			{[], Req2#http_req{cookies=[]}};
 			{[], Req2#http_req{cookies=[]}};
-		{RawCookie, Req2} ->
-			Cookies = cowboy_cookies:parse_cookie(RawCookie),
+		{ok, Cookies, Req2} ->
 			cookies(Req2#http_req{cookies=Cookies})
 			cookies(Req2#http_req{cookies=Cookies})
 	end;
 	end;
 cookies(Req=#http_req{cookies=Cookies}) ->
 cookies(Req=#http_req{cookies=Cookies}) ->
@@ -794,11 +800,11 @@ multipart_skip(Req) ->
 %% Response API.
 %% Response API.
 
 
 %% @doc Add a cookie header to the response.
 %% @doc Add a cookie header to the response.
--spec set_resp_cookie(binary(), binary(),
-	[cowboy_cookies:cookie_option()], Req) -> Req when Req::req().
-set_resp_cookie(Name, Value, Options, Req) ->
-	{HeaderName, HeaderValue} = cowboy_cookies:cookie(Name, Value, Options),
-	set_resp_header(HeaderName, HeaderValue, Req).
+-spec set_resp_cookie(iodata(), iodata(), cookie_opts(), Req)
+	-> Req when Req::req().
+set_resp_cookie(Name, Value, Opts, Req) ->
+	Cookie = cowboy_http:cookie_to_iodata(Name, Value, Opts),
+	set_resp_header(<<"set-cookie">>, Cookie, Req).
 
 
 %% @doc Add a header to the response.
 %% @doc Add a header to the response.
 -spec set_resp_header(binary(), iodata(), Req)
 -spec set_resp_header(binary(), iodata(), Req)