aboutsummaryrefslogtreecommitdiffstats
path: root/src/cowboy_http.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/cowboy_http.erl')
-rw-r--r--src/cowboy_http.erl165
1 files changed, 159 insertions, 6 deletions
diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl
index e0b1632..fb9f21c 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,33 @@ 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) -> 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.
-spec content_type(binary()) -> any().
content_type(Data) ->
@@ -341,12 +370,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 +388,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 +805,56 @@ 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) ->
+ 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.
-spec version_to_binary(version()) -> binary().
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].
+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_() ->
%% {Tokens, Result}
Tests = [
@@ -1040,6 +1155,44 @@ 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.
+
x_www_form_urlencoded_test_() ->
%% {Qs, Result}
Tests = [