diff options
Diffstat (limited to 'src/cowboy_http.erl')
-rw-r--r-- | src/cowboy_http.erl | 228 |
1 files changed, 222 insertions, 6 deletions
diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl index e0b1632..66383cb 100644 --- a/src/cowboy_http.erl +++ b/src/cowboy_http.erl @@ -19,6 +19,7 @@ %% Parsing. -export([list/2]). -export([nonempty_list/2]). +-export([cookie_list/1]). -export([content_type/1]). -export([media_range/2]). -export([conneg/2]). @@ -42,6 +43,7 @@ -export([ce_identity/1]). %% Interpretation. +-export([cookie_to_iodata/3]). -export([version_to_binary/1]). -export([urldecode/1]). -export([urldecode/2]). @@ -100,6 +102,77 @@ list(Data, Fun, Acc) -> 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) -> cookie(Rest, + fun (Rest2, << $$, _/bits >>, _) -> + cookie_list(Rest2, Acc); + (Rest2, Name, Value) -> + cookie_list(Rest2, [{Name, Value}|Acc]) + end) + end). + +-spec cookie(binary(), fun()) -> any(). +cookie(Data, Fun) -> + whitespace(Data, + fun (Rest) -> + cookie_name(Rest, + fun (_Rest2, <<>>) -> {error, badarg}; + (<< $=, Rest2/binary >>, Name) -> + cookie_value(Rest2, + fun (Rest3, Value) -> + Fun(Rest3, Name, Value) + end); + (_Rest2, _Attr) -> {error, badarg} + end) + end). + +-spec cookie_name(binary(), fun()) -> any(). +cookie_name(Data, Fun) -> + cookie_name(Data, Fun, <<>>). + +-spec cookie_name(binary(), fun(), binary()) -> any(). +cookie_name(<<>>, Fun, Acc) -> + Fun(<<>>, Acc); +cookie_name(Data = << C, _Rest/binary >>, Fun, Acc) + when C =:= $=; C =:= $,; C =:= $;; C =:= $\s; C =:= $\t; + C =:= $\r; C =:= $\n; C =:= $\013; C =:= $\014 -> + Fun(Data, Acc); +cookie_name(<< C, Rest/binary >>, Fun, Acc) -> + C2 = cowboy_bstr:char_to_lower(C), + cookie_name(Rest, Fun, << Acc/binary, C2 >>). + +-spec cookie_value(binary(), fun()) -> any(). +cookie_value(Data, Fun) -> + cookie_value(Data, Fun, <<>>). + +-spec cookie_value(binary(), fun(), binary()) -> any(). +cookie_value(<<>>, Fun, Acc) -> + Fun(<<>>, Acc); +cookie_value(Data = << C, _Rest/binary >>, Fun, Acc) + when C =:= $,; C =:= $;; C =:= $\s; C =:= $\t; + C =:= $\r; C =:= $\n; C =:= $\013; C =:= $\014 -> + Fun(Data, Acc); +cookie_value(<< C, Rest/binary >>, Fun, Acc) -> + cookie_value(Rest, Fun, << Acc/binary, C >>). + %% @doc Parse a content type. -spec content_type(binary()) -> any(). content_type(Data) -> @@ -341,12 +414,17 @@ params(Data, Fun) -> -spec params(binary(), fun(), [{binary(), binary()}]) -> any(). params(Data, Fun, Acc) -> 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). --spec param(binary(), fun(), [{binary(), binary()}]) -> any(). -param(Data, Fun, Acc) -> +-spec param(binary(), fun()) -> any(). +param(Data, Fun) -> whitespace(Data, fun (Rest) -> token_ci(Rest, @@ -354,8 +432,7 @@ param(Data, Fun, Acc) -> (<< $=, Rest2/binary >>, Attr) -> word(Rest2, fun (Rest3, Value) -> - params(Rest3, Fun, - [{Attr, Value}|Acc]) + Fun(Rest3, Attr, Value) end); (_Rest2, _Attr) -> {error, badarg} end) @@ -772,6 +849,55 @@ ce_identity(Data) -> %% 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 <[email protected]> +-spec cookie_to_iodata(iodata(), iodata(), cowboy_req:cookie_opts()) + -> iodata(). +cookie_to_iodata(Name, Value, Opts) -> + case binary:match(iolist_to_binary(Name), [<<$=>>, <<$,>>, <<$;>>, + <<$\s>>, <<$\t>>, <<$\r>>, <<$\n>>, <<$\013>>, <<$\014>>]) of + nomatch -> ok + end, + case binary:match(iolist_to_binary(Value), [<<$,>>, <<$;>>, + <<$\s>>, <<$\t>>, <<$\r>>, <<$\n>>, <<$\013>>, <<$\014>>]) of + nomatch -> ok + end, + MaxAgeBin = case lists:keyfind(max_age, 1, Opts) of + false -> <<>>; + {_, 0} -> + %% MSIE requires an Expires date in the past to delete a cookie. + <<"; Expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0">>; + {_, 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=">>, Domain] + end, + PathBin = case lists:keyfind(path, 1, Opts) of + false -> <<>>; + {_, Path} -> [<<"; Path=">>, 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, <<"=">>, Value, <<"; Version=1">>, + MaxAgeBin, DomainBin, PathBin, SecureBin, HttpOnlyBin]. + %% @doc Convert an HTTP version tuple to its binary form. -spec version_to_binary(version()) -> binary(). version_to_binary({1, 1}) -> <<"HTTP/1.1">>; @@ -927,6 +1053,38 @@ nonempty_token_list_test_() -> ], [{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=">>, [{<<"foo">>, <<>>}, {<<"bar">>, <<>>}]}, + {<<"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_() -> %% {Tokens, Result} Tests = [ @@ -1040,6 +1198,64 @@ digits_test_() -> ], [{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. + +cookie_to_iodata_failures_test_() -> + F = fun(N, V) -> + try cookie_to_iodata(N, V, []) of + _ -> + false + catch _:_ -> + true + end + end, + Tests = [ + {<<"Na=me">>, <<"Value">>}, + {<<"Name;">>, <<"Value">>}, + {<<"\r\name">>, <<"Value">>}, + {<<"Name">>, <<"Value;">>}, + {<<"Name">>, <<"\value">>} + ], + [{iolist_to_binary(io_lib:format("{~p, ~p} failure", [N, V])), + fun() -> true = F(N, V) end} + || {N, V} <- Tests]. + x_www_form_urlencoded_test_() -> %% {Qs, Result} Tests = [ |