zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

cow_cookie.erl (14693B)


      1 %% Copyright (c) 2013-2020, 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 
     15 -module(cow_cookie).
     16 
     17 -export([parse_cookie/1]).
     18 -export([parse_set_cookie/1]).
     19 -export([cookie/1]).
     20 -export([setcookie/3]).
     21 
     22 -type cookie_attrs() :: #{
     23 	expires => calendar:datetime(),
     24 	max_age => calendar:datetime(),
     25 	domain => binary(),
     26 	path => binary(),
     27 	secure => true,
     28 	http_only => true,
     29 	same_site => strict | lax | none
     30 }.
     31 -export_type([cookie_attrs/0]).
     32 
     33 -type cookie_opts() :: #{
     34 	domain => binary(),
     35 	http_only => boolean(),
     36 	max_age => non_neg_integer(),
     37 	path => binary(),
     38 	same_site => strict | lax | none,
     39 	secure => boolean()
     40 }.
     41 -export_type([cookie_opts/0]).
     42 
     43 -include("cow_inline.hrl").
     44 
     45 %% Cookie header.
     46 
     47 -spec parse_cookie(binary()) -> [{binary(), binary()}].
     48 parse_cookie(Cookie) ->
     49 	parse_cookie(Cookie, []).
     50 
     51 parse_cookie(<<>>, Acc) ->
     52 	lists:reverse(Acc);
     53 parse_cookie(<< $\s, Rest/binary >>, Acc) ->
     54 	parse_cookie(Rest, Acc);
     55 parse_cookie(<< $\t, Rest/binary >>, Acc) ->
     56 	parse_cookie(Rest, Acc);
     57 parse_cookie(<< $,, Rest/binary >>, Acc) ->
     58 	parse_cookie(Rest, Acc);
     59 parse_cookie(<< $;, Rest/binary >>, Acc) ->
     60 	parse_cookie(Rest, Acc);
     61 parse_cookie(Cookie, Acc) ->
     62 	parse_cookie_name(Cookie, Acc, <<>>).
     63 
     64 parse_cookie_name(<<>>, Acc, Name) ->
     65 	lists:reverse([{<<>>, parse_cookie_trim(Name)}|Acc]);
     66 parse_cookie_name(<< $=, _/binary >>, _, <<>>) ->
     67 	error(badarg);
     68 parse_cookie_name(<< $=, Rest/binary >>, Acc, Name) ->
     69 	parse_cookie_value(Rest, Acc, Name, <<>>);
     70 parse_cookie_name(<< $,, _/binary >>, _, _) ->
     71 	error(badarg);
     72 parse_cookie_name(<< $;, Rest/binary >>, Acc, Name) ->
     73 	parse_cookie(Rest, [{<<>>, parse_cookie_trim(Name)}|Acc]);
     74 parse_cookie_name(<< $\t, _/binary >>, _, _) ->
     75 	error(badarg);
     76 parse_cookie_name(<< $\r, _/binary >>, _, _) ->
     77 	error(badarg);
     78 parse_cookie_name(<< $\n, _/binary >>, _, _) ->
     79 	error(badarg);
     80 parse_cookie_name(<< $\013, _/binary >>, _, _) ->
     81 	error(badarg);
     82 parse_cookie_name(<< $\014, _/binary >>, _, _) ->
     83 	error(badarg);
     84 parse_cookie_name(<< C, Rest/binary >>, Acc, Name) ->
     85 	parse_cookie_name(Rest, Acc, << Name/binary, C >>).
     86 
     87 parse_cookie_value(<<>>, Acc, Name, Value) ->
     88 	lists:reverse([{Name, parse_cookie_trim(Value)}|Acc]);
     89 parse_cookie_value(<< $;, Rest/binary >>, Acc, Name, Value) ->
     90 	parse_cookie(Rest, [{Name, parse_cookie_trim(Value)}|Acc]);
     91 parse_cookie_value(<< $\t, _/binary >>, _, _, _) ->
     92 	error(badarg);
     93 parse_cookie_value(<< $\r, _/binary >>, _, _, _) ->
     94 	error(badarg);
     95 parse_cookie_value(<< $\n, _/binary >>, _, _, _) ->
     96 	error(badarg);
     97 parse_cookie_value(<< $\013, _/binary >>, _, _, _) ->
     98 	error(badarg);
     99 parse_cookie_value(<< $\014, _/binary >>, _, _, _) ->
    100 	error(badarg);
    101 parse_cookie_value(<< C, Rest/binary >>, Acc, Name, Value) ->
    102 	parse_cookie_value(Rest, Acc, Name, << Value/binary, C >>).
    103 
    104 parse_cookie_trim(Value = <<>>) ->
    105 	Value;
    106 parse_cookie_trim(Value) ->
    107 	case binary:last(Value) of
    108 		$\s ->
    109 			Size = byte_size(Value) - 1,
    110 			<< Value2:Size/binary, _ >> = Value,
    111 			parse_cookie_trim(Value2);
    112 		_ ->
    113 			Value
    114 	end.
    115 
    116 -ifdef(TEST).
    117 parse_cookie_test_() ->
    118 	%% {Value, Result}.
    119 	Tests = [
    120 		{<<"name=value; name2=value2">>, [
    121 			{<<"name">>, <<"value">>},
    122 			{<<"name2">>, <<"value2">>}
    123 		]},
    124 		%% Space in value.
    125 		{<<"foo=Thu Jul 11 2013 15:38:43 GMT+0400 (MSK)">>,
    126 			[{<<"foo">>, <<"Thu Jul 11 2013 15:38:43 GMT+0400 (MSK)">>}]},
    127 		%% Comma in value. Google Analytics sets that kind of cookies.
    128 		{<<"refk=sOUZDzq2w2; sk=B602064E0139D842D620C7569640DBB4C81C45080651"
    129 			"9CC124EF794863E10E80; __utma=64249653.825741573.1380181332.1400"
    130 			"015657.1400019557.703; __utmb=64249653.1.10.1400019557; __utmc="
    131 			"64249653; __utmz=64249653.1400019557.703.13.utmcsr=bluesky.chic"
    132 			"agotribune.com|utmccn=(referral)|utmcmd=referral|utmcct=/origin"
    133 			"als/chi-12-indispensable-digital-tools-bsi,0,0.storygallery">>, [
    134 				{<<"refk">>, <<"sOUZDzq2w2">>},
    135 				{<<"sk">>, <<"B602064E0139D842D620C7569640DBB4C81C45080651"
    136 					"9CC124EF794863E10E80">>},
    137 				{<<"__utma">>, <<"64249653.825741573.1380181332.1400"
    138 					"015657.1400019557.703">>},
    139 				{<<"__utmb">>, <<"64249653.1.10.1400019557">>},
    140 				{<<"__utmc">>, <<"64249653">>},
    141 				{<<"__utmz">>, <<"64249653.1400019557.703.13.utmcsr=bluesky.chic"
    142 					"agotribune.com|utmccn=(referral)|utmcmd=referral|utmcct=/origin"
    143 					"als/chi-12-indispensable-digital-tools-bsi,0,0.storygallery">>}
    144 		]},
    145 		%% Potential edge cases (initially from Mochiweb).
    146 		{<<"foo=\\x">>, [{<<"foo">>, <<"\\x">>}]},
    147 		{<<"foo=;bar=">>, [{<<"foo">>, <<>>}, {<<"bar">>, <<>>}]},
    148 		{<<"foo=\\\";;bar=good ">>,
    149 			[{<<"foo">>, <<"\\\"">>}, {<<"bar">>, <<"good">>}]},
    150 		{<<"foo=\"\\\";bar=good">>,
    151 			[{<<"foo">>, <<"\"\\\"">>}, {<<"bar">>, <<"good">>}]},
    152 		{<<>>, []}, %% Flash player.
    153 		{<<"foo=bar , baz=wibble ">>, [{<<"foo">>, <<"bar , baz=wibble">>}]},
    154 		%% Technically invalid, but seen in the wild
    155 		{<<"foo">>, [{<<>>, <<"foo">>}]},
    156 		{<<"foo ">>, [{<<>>, <<"foo">>}]},
    157 		{<<"foo;">>, [{<<>>, <<"foo">>}]},
    158 		{<<"bar;foo=1">>, [{<<>>, <<"bar">>}, {<<"foo">>, <<"1">>}]}
    159 	],
    160 	[{V, fun() -> R = parse_cookie(V) end} || {V, R} <- Tests].
    161 
    162 parse_cookie_error_test_() ->
    163 	%% Value.
    164 	Tests = [
    165 		<<"=">>
    166 	],
    167 	[{V, fun() -> {'EXIT', {badarg, _}} = (catch parse_cookie(V)) end} || V <- Tests].
    168 -endif.
    169 
    170 %% Set-Cookie header.
    171 
    172 -spec parse_set_cookie(binary())
    173 	-> {ok, binary(), binary(), cookie_attrs()}
    174 	| ignore.
    175 parse_set_cookie(SetCookie) ->
    176 	{NameValuePair, UnparsedAttrs} = take_until_semicolon(SetCookie, <<>>),
    177 	{Name, Value} = case binary:split(NameValuePair, <<$=>>) of
    178 		[Value0] -> {<<>>, trim(Value0)};
    179 		[Name0, Value0] -> {trim(Name0), trim(Value0)}
    180 	end,
    181 	case {Name, Value} of
    182 		{<<>>, <<>>} ->
    183 			ignore;
    184 		_ ->
    185 			Attrs = parse_set_cookie_attrs(UnparsedAttrs, #{}),
    186 			{ok, Name, Value, Attrs}
    187 	end.
    188 
    189 parse_set_cookie_attrs(<<>>, Attrs) ->
    190 	Attrs;
    191 parse_set_cookie_attrs(<<$;,Rest0/bits>>, Attrs) ->
    192 	{Av, Rest} = take_until_semicolon(Rest0, <<>>),
    193 	{Name, Value} = case binary:split(Av, <<$=>>) of
    194 		[Name0] -> {trim(Name0), <<>>};
    195 		[Name0, Value0] -> {trim(Name0), trim(Value0)}
    196 	end,
    197 	case parse_set_cookie_attr(?LOWER(Name), Value) of
    198 		{ok, AttrName, AttrValue} ->
    199 			parse_set_cookie_attrs(Rest, Attrs#{AttrName => AttrValue});
    200 		{ignore, AttrName} ->
    201 			parse_set_cookie_attrs(Rest, maps:remove(AttrName, Attrs));
    202 		ignore ->
    203 			parse_set_cookie_attrs(Rest, Attrs)
    204 	end.
    205 
    206 take_until_semicolon(Rest = <<$;,_/bits>>, Acc) -> {Acc, Rest};
    207 take_until_semicolon(<<C,R/bits>>, Acc) -> take_until_semicolon(R, <<Acc/binary,C>>);
    208 take_until_semicolon(<<>>, Acc) -> {Acc, <<>>}.
    209 
    210 trim(String) ->
    211 	string:trim(String, both, [$\s, $\t]).
    212 
    213 parse_set_cookie_attr(<<"expires">>, Value) ->
    214 	try cow_date:parse_date(Value) of
    215 		DateTime ->
    216 			{ok, expires, DateTime}
    217 	catch _:_ ->
    218 		ignore
    219 	end;
    220 parse_set_cookie_attr(<<"max-age">>, Value) ->
    221 	try binary_to_integer(Value) of
    222 		MaxAge when MaxAge =< 0 ->
    223 			%% Year 0 corresponds to 1 BC.
    224 			{ok, max_age, {{0, 1, 1}, {0, 0, 0}}};
    225 		MaxAge ->
    226 			CurrentTime = erlang:universaltime(),
    227 			{ok, max_age, calendar:gregorian_seconds_to_datetime(
    228 				calendar:datetime_to_gregorian_seconds(CurrentTime) + MaxAge)}
    229 	catch _:_ ->
    230 		ignore
    231 	end;
    232 parse_set_cookie_attr(<<"domain">>, Value) ->
    233 	case Value of
    234 		<<>> ->
    235 			ignore;
    236 		<<".",Rest/bits>> ->
    237 			{ok, domain, ?LOWER(Rest)};
    238 		_ ->
    239 			{ok, domain, ?LOWER(Value)}
    240 	end;
    241 parse_set_cookie_attr(<<"path">>, Value) ->
    242 	case Value of
    243 		<<"/",_/bits>> ->
    244 			{ok, path, Value};
    245 		%% When the path is not absolute, or the path is empty, the default-path will be used.
    246 		%% Note that the default-path is also used when there are no path attributes,
    247 		%% so we are simply ignoring the attribute here.
    248 		_ ->
    249 			{ignore, path}
    250 	end;
    251 parse_set_cookie_attr(<<"secure">>, _) ->
    252 	{ok, secure, true};
    253 parse_set_cookie_attr(<<"httponly">>, _) ->
    254 	{ok, http_only, true};
    255 parse_set_cookie_attr(<<"samesite">>, Value) ->
    256 	case ?LOWER(Value) of
    257 		<<"strict">> ->
    258 			{ok, same_site, strict};
    259 		<<"lax">> ->
    260 			{ok, same_site, lax};
    261 		%% Clients may have different defaults than "None".
    262 		<<"none">> ->
    263 			{ok, same_site, none};
    264 		%% Unknown values and lack of value are equivalent.
    265 		_ ->
    266 			ignore
    267 	end;
    268 parse_set_cookie_attr(_, _) ->
    269 	ignore.
    270 
    271 -ifdef(TEST).
    272 parse_set_cookie_test_() ->
    273 	Tests = [
    274 		{<<"a=b">>, {ok, <<"a">>, <<"b">>, #{}}},
    275 		{<<"a=b; Secure">>, {ok, <<"a">>, <<"b">>, #{secure => true}}},
    276 		{<<"a=b; HttpOnly">>, {ok, <<"a">>, <<"b">>, #{http_only => true}}},
    277 		{<<"a=b; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Expires=Wed, 21 Oct 2015 07:29:00 GMT">>,
    278 			{ok, <<"a">>, <<"b">>, #{expires => {{2015,10,21},{7,29,0}}}}},
    279 		{<<"a=b; Max-Age=999; Max-Age=0">>,
    280 			{ok, <<"a">>, <<"b">>, #{max_age => {{0,1,1},{0,0,0}}}}},
    281 		{<<"a=b; Domain=example.org; Domain=foo.example.org">>,
    282 			{ok, <<"a">>, <<"b">>, #{domain => <<"foo.example.org">>}}},
    283 		{<<"a=b; Path=/path/to/resource; Path=/">>,
    284 			{ok, <<"a">>, <<"b">>, #{path => <<"/">>}}},
    285 		{<<"a=b; SameSite=Lax; SameSite=Strict">>,
    286 			{ok, <<"a">>, <<"b">>, #{same_site => strict}}}
    287 	],
    288 	[{SetCookie, fun() -> Res = parse_set_cookie(SetCookie) end}
    289 		|| {SetCookie, Res} <- Tests].
    290 -endif.
    291 
    292 %% Build a cookie header.
    293 
    294 -spec cookie([{iodata(), iodata()}]) -> iolist().
    295 cookie([]) ->
    296 	[];
    297 cookie([{<<>>, Value}]) ->
    298 	[Value];
    299 cookie([{Name, Value}]) ->
    300 	[Name, $=, Value];
    301 cookie([{<<>>, Value}|Tail]) ->
    302 	[Value, $;, $\s|cookie(Tail)];
    303 cookie([{Name, Value}|Tail]) ->
    304 	[Name, $=, Value, $;, $\s|cookie(Tail)].
    305 
    306 -ifdef(TEST).
    307 cookie_test_() ->
    308 	Tests = [
    309 		{[], <<>>},
    310 		{[{<<"a">>, <<"b">>}], <<"a=b">>},
    311 		{[{<<"a">>, <<"b">>}, {<<"c">>, <<"d">>}], <<"a=b; c=d">>},
    312 		{[{<<>>, <<"b">>}, {<<"c">>, <<"d">>}], <<"b; c=d">>},
    313 		{[{<<"a">>, <<"b">>}, {<<>>, <<"d">>}], <<"a=b; d">>}
    314 	],
    315 	[{Res, fun() -> Res = iolist_to_binary(cookie(Cookies)) end}
    316 		|| {Cookies, Res} <- Tests].
    317 -endif.
    318 
    319 %% Convert a cookie name, value and options to its iodata form.
    320 %%
    321 %% Initially from Mochiweb:
    322 %%   * Copyright 2007 Mochi Media, Inc.
    323 %% Initial binary implementation:
    324 %%   * Copyright 2011 Thomas Burdick <thomas.burdick@gmail.com>
    325 %%
    326 %% @todo Rename the function to set_cookie eventually.
    327 
    328 -spec setcookie(iodata(), iodata(), cookie_opts()) -> iolist().
    329 setcookie(Name, Value, Opts) ->
    330 	nomatch = binary:match(iolist_to_binary(Name), [<<$=>>, <<$,>>, <<$;>>,
    331 			<<$\s>>, <<$\t>>, <<$\r>>, <<$\n>>, <<$\013>>, <<$\014>>]),
    332 	nomatch = binary:match(iolist_to_binary(Value), [<<$,>>, <<$;>>,
    333 			<<$\s>>, <<$\t>>, <<$\r>>, <<$\n>>, <<$\013>>, <<$\014>>]),
    334 	[Name, <<"=">>, Value, <<"; Version=1">>, attributes(maps:to_list(Opts))].
    335 
    336 attributes([]) -> [];
    337 attributes([{domain, Domain}|Tail]) -> [<<"; Domain=">>, Domain|attributes(Tail)];
    338 attributes([{http_only, false}|Tail]) -> attributes(Tail);
    339 attributes([{http_only, true}|Tail]) -> [<<"; HttpOnly">>|attributes(Tail)];
    340 %% MSIE requires an Expires date in the past to delete a cookie.
    341 attributes([{max_age, 0}|Tail]) ->
    342 	[<<"; Expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0">>|attributes(Tail)];
    343 attributes([{max_age, MaxAge}|Tail]) when is_integer(MaxAge), MaxAge > 0 ->
    344 	Secs = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),
    345 	Expires = cow_date:rfc2109(calendar:gregorian_seconds_to_datetime(Secs + MaxAge)),
    346 	[<<"; Expires=">>, Expires, <<"; Max-Age=">>, integer_to_list(MaxAge)|attributes(Tail)];
    347 attributes([Opt={max_age, _}|_]) ->
    348 	error({badarg, Opt});
    349 attributes([{path, Path}|Tail]) -> [<<"; Path=">>, Path|attributes(Tail)];
    350 attributes([{secure, false}|Tail]) -> attributes(Tail);
    351 attributes([{secure, true}|Tail]) -> [<<"; Secure">>|attributes(Tail)];
    352 attributes([{same_site, lax}|Tail]) -> [<<"; SameSite=Lax">>|attributes(Tail)];
    353 attributes([{same_site, strict}|Tail]) -> [<<"; SameSite=Strict">>|attributes(Tail)];
    354 attributes([{same_site, none}|Tail]) -> [<<"; SameSite=None">>|attributes(Tail)];
    355 %% Skip unknown options.
    356 attributes([_|Tail]) -> attributes(Tail).
    357 
    358 -ifdef(TEST).
    359 setcookie_test_() ->
    360 	%% {Name, Value, Opts, Result}
    361 	Tests = [
    362 		{<<"Customer">>, <<"WILE_E_COYOTE">>,
    363 			#{http_only => true, domain => <<"acme.com">>},
    364 			<<"Customer=WILE_E_COYOTE; Version=1; "
    365 				"Domain=acme.com; HttpOnly">>},
    366 		{<<"Customer">>, <<"WILE_E_COYOTE">>,
    367 			#{path => <<"/acme">>},
    368 			<<"Customer=WILE_E_COYOTE; Version=1; Path=/acme">>},
    369 		{<<"Customer">>, <<"WILE_E_COYOTE">>,
    370 			#{secure => true},
    371 			<<"Customer=WILE_E_COYOTE; Version=1; Secure">>},
    372 		{<<"Customer">>, <<"WILE_E_COYOTE">>,
    373 			#{secure => false, http_only => false},
    374 			<<"Customer=WILE_E_COYOTE; Version=1">>},
    375 		{<<"Customer">>, <<"WILE_E_COYOTE">>,
    376 			#{same_site => lax},
    377 			<<"Customer=WILE_E_COYOTE; Version=1; SameSite=Lax">>},
    378 		{<<"Customer">>, <<"WILE_E_COYOTE">>,
    379 			#{same_site => strict},
    380 			<<"Customer=WILE_E_COYOTE; Version=1; SameSite=Strict">>},
    381 		{<<"Customer">>, <<"WILE_E_COYOTE">>,
    382 			#{path => <<"/acme">>, badoption => <<"negatory">>},
    383 			<<"Customer=WILE_E_COYOTE; Version=1; Path=/acme">>}
    384 	],
    385 	[{R, fun() -> R = iolist_to_binary(setcookie(N, V, O)) end}
    386 		|| {N, V, O, R} <- Tests].
    387 
    388 setcookie_max_age_test() ->
    389 	F = fun(N, V, O) ->
    390 		binary:split(iolist_to_binary(
    391 			setcookie(N, V, O)), <<";">>, [global])
    392 	end,
    393 	[<<"Customer=WILE_E_COYOTE">>,
    394 		<<" Version=1">>,
    395 		<<" Expires=", _/binary>>,
    396 		<<" Max-Age=111">>,
    397 		<<" Secure">>] = F(<<"Customer">>, <<"WILE_E_COYOTE">>,
    398 			#{max_age => 111, secure => true}),
    399 	case catch F(<<"Customer">>, <<"WILE_E_COYOTE">>, #{max_age => -111}) of
    400 		{'EXIT', {{badarg, {max_age, -111}}, _}} -> ok
    401 	end,
    402 	[<<"Customer=WILE_E_COYOTE">>,
    403 		<<" Version=1">>,
    404 		<<" Expires=", _/binary>>,
    405 		<<" Max-Age=86417">>] = F(<<"Customer">>, <<"WILE_E_COYOTE">>,
    406 			 #{max_age => 86417}),
    407 	ok.
    408 
    409 setcookie_failures_test_() ->
    410 	F = fun(N, V) ->
    411 		try setcookie(N, V, #{}) of
    412 			_ ->
    413 				false
    414 		catch _:_ ->
    415 			true
    416 		end
    417 	end,
    418 	Tests = [
    419 		{<<"Na=me">>, <<"Value">>},
    420 		{<<"Name;">>, <<"Value">>},
    421 		{<<"\r\name">>, <<"Value">>},
    422 		{<<"Name">>, <<"Value;">>},
    423 		{<<"Name">>, <<"\value">>}
    424 	],
    425 	[{iolist_to_binary(io_lib:format("{~p, ~p} failure", [N, V])),
    426 		fun() -> true = F(N, V) end}
    427 		|| {N, V} <- Tests].
    428 -endif.