aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/cowboy_clock.erl54
-rw-r--r--src/cowboy_cookies.erl416
-rw-r--r--src/cowboy_dispatcher.erl43
-rw-r--r--src/cowboy_http.erl228
-rw-r--r--src/cowboy_multipart.erl92
-rw-r--r--src/cowboy_protocol.erl38
-rw-r--r--src/cowboy_req.erl84
-rw-r--r--src/cowboy_rest.erl57
-rw-r--r--src/cowboy_static.erl53
-rw-r--r--src/cowboy_websocket.erl159
-rw-r--r--src/cowboy_websocket_handler.erl8
11 files changed, 589 insertions, 643 deletions
diff --git a/src/cowboy_clock.erl b/src/cowboy_clock.erl
index 5e2bf44..b439bb1 100644
--- a/src/cowboy_clock.erl
+++ b/src/cowboy_clock.erl
@@ -25,6 +25,7 @@
-export([start_link/0]).
-export([stop/0]).
-export([rfc1123/0]).
+-export([rfc1123/1]).
-export([rfc2109/1]).
%% gen_server.
@@ -61,50 +62,26 @@ stop() ->
gen_server:call(?SERVER, stop).
%% @doc Return the current date and time formatted according to RFC-1123.
-%%
-%% This format is used in the <em>date</em> header sent with HTTP responses.
-spec rfc1123() -> binary().
rfc1123() ->
ets:lookup_element(?TABLE, rfc1123, 2).
+%% @doc Return the given date and time formatted according to RFC-1123.
+-spec rfc1123(calendar:datetime()) -> binary().
+rfc1123(DateTime) ->
+ update_rfc1123(<<>>, undefined, DateTime).
+
%% @doc Return the current date and time formatted according to RFC-2109.
%%
%% This format is used in the <em>set-cookie</em> header sent with
%% HTTP responses.
-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.
@@ -215,6 +192,13 @@ month(12) -> <<"Dec">>.
-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_() ->
Tests = [
{<<"Sat, 14 May 2011 14:25:33 GMT">>, undefined,
diff --git a/src/cowboy_cookies.erl b/src/cowboy_cookies.erl
deleted file mode 100644
index d10c848..0000000
--- a/src/cowboy_cookies.erl
+++ /dev/null
@@ -1,416 +0,0 @@
-%% Copyright 2007 Mochi Media, Inc.
-%% Copyright 2011 Thomas Burdick <[email protected]>
-%%
-%% 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.
diff --git a/src/cowboy_dispatcher.erl b/src/cowboy_dispatcher.erl
index cfb8fb6..ef6e8ac 100644
--- a/src/cowboy_dispatcher.erl
+++ b/src/cowboy_dispatcher.erl
@@ -21,7 +21,7 @@
-type bindings() :: [{atom(), binary()}].
-type tokens() :: [binary()].
--type match_rule() :: '_' | '*' | [binary() | '_' | '...' | atom()].
+-type match_rule() :: '_' | <<_:8>> | [binary() | '_' | '...' | atom()].
-type dispatch_path() :: [{match_rule(), module(), any()}].
-type dispatch_rule() :: {Host::match_rule(), Path::dispatch_path()}.
-type dispatch_rules() :: [dispatch_rule()].
@@ -45,9 +45,10 @@
%% <em>PathRules</em> being a list of <em>{Path, HandlerMod, HandlerOpts}</em>.
%%
%% <em>Hostname</em> and <em>Path</em> are match rules and can be either the
-%% atom <em>'_'</em>, which matches everything for a single token, the atom
-%% <em>'*'</em>, which matches everything for the rest of the tokens, or a
-%% list of tokens. Each token can be either a binary, the atom <em>'_'</em>,
+%% atom <em>'_'</em>, which matches everything, `<<"*">>', which match the
+%% wildcard path, or a list of tokens.
+%%
+%% Each token can be either a binary, the atom <em>'_'</em>,
%% the atom '...' or a named atom. A binary token must match exactly,
%% <em>'_'</em> matches everything for a single token, <em>'...'</em> matches
%% everything for the rest of the tokens and a named atom will bind the
@@ -67,7 +68,8 @@
-> {ok, module(), any(), bindings(),
HostInfo::undefined | tokens(),
PathInfo::undefined | tokens()}
- | {error, notfound, host} | {error, notfound, path}.
+ | {error, notfound, host} | {error, notfound, path}
+ | {error, badrequest, path}.
match([], _, _) ->
{error, notfound, host};
match([{'_', PathMatchs}|_Tail], _, Path) ->
@@ -91,12 +93,12 @@ match(Dispatch, Host, Path) ->
-> {ok, module(), any(), bindings(),
HostInfo::undefined | tokens(),
PathInfo::undefined | tokens()}
- | {error, notfound, path}.
+ | {error, notfound, path} | {error, badrequest, path}.
match_path([], _, _, _) ->
{error, notfound, path};
match_path([{'_', Handler, Opts}|_Tail], HostInfo, _, Bindings) ->
{ok, Handler, Opts, Bindings, HostInfo, undefined};
-match_path([{'*', Handler, Opts}|_Tail], HostInfo, '*', Bindings) ->
+match_path([{<<"*">>, Handler, Opts}|_Tail], HostInfo, <<"*">>, Bindings) ->
{ok, Handler, Opts, Bindings, HostInfo, undefined};
match_path([{PathMatch, Handler, Opts}|Tail], HostInfo, Tokens,
Bindings) when is_list(Tokens) ->
@@ -106,6 +108,8 @@ match_path([{PathMatch, Handler, Opts}|Tail], HostInfo, Tokens,
{true, PathBinds, PathInfo} ->
{ok, Handler, Opts, Bindings ++ PathBinds, HostInfo, PathInfo}
end;
+match_path(_Dispatch, _HostInfo, badrequest, _Bindings) ->
+ {error, badrequest, path};
match_path(Dispatch, HostInfo, Path, Bindings) ->
match_path(Dispatch, HostInfo, split_path(Path), Bindings).
@@ -135,17 +139,24 @@ split_host(Host, Acc) ->
%% and part of a path segment.
-spec split_path(binary()) -> tokens().
split_path(<< $/, Path/bits >>) ->
- split_path(Path, []).
+ split_path(Path, []);
+split_path(_) ->
+ badrequest.
split_path(Path, Acc) ->
- case binary:match(Path, <<"/">>) of
- nomatch when Path =:= <<>> ->
- lists:reverse([cowboy_http:urldecode(S) || S <- Acc]);
- nomatch ->
- lists:reverse([cowboy_http:urldecode(S) || S <- [Path|Acc]]);
- {Pos, _} ->
- << Segment:Pos/binary, _:8, Rest/bits >> = Path,
- split_path(Rest, [Segment|Acc])
+ try
+ case binary:match(Path, <<"/">>) of
+ nomatch when Path =:= <<>> ->
+ lists:reverse([cowboy_http:urldecode(S) || S <- Acc]);
+ nomatch ->
+ lists:reverse([cowboy_http:urldecode(S) || S <- [Path|Acc]]);
+ {Pos, _} ->
+ << Segment:Pos/binary, _:8, Rest/bits >> = Path,
+ split_path(Rest, [Segment|Acc])
+ end
+ catch
+ error:badarg ->
+ badrequest
end.
-spec list_match(tokens(), match_rule(), bindings())
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 = [
diff --git a/src/cowboy_multipart.erl b/src/cowboy_multipart.erl
index fc889ef..7363054 100644
--- a/src/cowboy_multipart.erl
+++ b/src/cowboy_multipart.erl
@@ -74,17 +74,51 @@ parse(Bin, Boundary) ->
more(Bin, fun (NewBin) -> parse(NewBin, Boundary) end).
-type pattern() :: {binary:cp(), non_neg_integer()}.
+-type patterns() :: {pattern(), pattern()}.
-%% @doc Return a compiled binary pattern with its size in bytes.
-%% The pattern is the boundary prepended with "\r\n--".
--spec pattern(binary()) -> pattern().
+%% @doc Return two compiled binary patterns with their sizes in bytes.
+%% The boundary pattern is the boundary prepended with "\r\n--".
+%% The boundary suffix pattern matches all prefixes of the boundary.
+-spec pattern(binary()) -> patterns().
pattern(Boundary) ->
MatchPattern = <<"\r\n--", Boundary/binary>>,
- {binary:compile_pattern(MatchPattern), byte_size(MatchPattern)}.
+ MatchPrefixes = prefixes(MatchPattern),
+ {{binary:compile_pattern(MatchPattern), byte_size(MatchPattern)},
+ {binary:compile_pattern(MatchPrefixes), byte_size(MatchPattern)}}.
+
+%% @doc Return all prefixes of a binary string.
+%% The list of prefixes includes the full string.
+-spec prefixes(binary()) -> [binary()].
+prefixes(<<C, Rest/binary>>) ->
+ prefixes(Rest, <<C>>).
+
+-spec prefixes(binary(), binary()) -> [binary()].
+prefixes(<<C, Rest/binary>>, Acc) ->
+ [Acc|prefixes(Rest, <<Acc/binary, C>>)];
+prefixes(<<>>, Acc) ->
+ [Acc].
+
+%% @doc Test if a boundary is a possble suffix.
+%% The patterns are expected to have been returned from `pattern/1'.
+-spec suffix_match(binary(), patterns()) -> nomatch | {integer(), integer()}.
+suffix_match(Bin, {_Boundary, {Pat, Len}}) ->
+ Size = byte_size(Bin),
+ suffix_match(Bin, Pat, Size, max(-Size, -Len)).
+
+-spec suffix_match(binary(), tuple(), non_neg_integer(), 0|neg_integer()) ->
+ nomatch | {integer(), integer()}.
+suffix_match(_Bin, _Pat, _Size, _Match=0) ->
+ nomatch;
+suffix_match(Bin, Pat, Size, Match) when Match < 0 ->
+ case binary:match(Bin, Pat, [{scope, {Size, Match}}]) of
+ {Pos, Len}=Part when Pos + Len =:= Size -> Part;
+ {_, Len} -> suffix_match(Bin, Pat, Size, Match + Len);
+ nomatch -> nomatch
+ end.
%% @doc Parse remaining characters of a line beginning with the boundary.
%% If followed by "--", <em>eof</em> is returned and parsing is finished.
--spec parse_boundary_tail(binary(), pattern()) -> more(part_result()).
+-spec parse_boundary_tail(binary(), patterns()) -> more(part_result()).
parse_boundary_tail(Bin, Pattern) when byte_size(Bin) >= 2 ->
case Bin of
<<"--", _Rest/binary>> ->
@@ -100,7 +134,7 @@ parse_boundary_tail(Bin, Pattern) ->
more(Bin, fun (NewBin) -> parse_boundary_tail(NewBin, Pattern) end).
%% @doc Skip whitespace and unknown chars until CRLF.
--spec parse_boundary_eol(binary(), pattern()) -> more(part_result()).
+-spec parse_boundary_eol(binary(), patterns()) -> more(part_result()).
parse_boundary_eol(Bin, Pattern) ->
case binary:match(Bin, <<"\r\n">>) of
{CrlfStart, _Length} ->
@@ -115,7 +149,7 @@ parse_boundary_eol(Bin, Pattern) ->
more(Rest, fun (NewBin) -> parse_boundary_eol(NewBin, Pattern) end)
end.
--spec parse_boundary_crlf(binary(), pattern()) -> more(part_result()).
+-spec parse_boundary_crlf(binary(), patterns()) -> more(part_result()).
parse_boundary_crlf(<<"\r\n", Rest/binary>>, Pattern) ->
% The binary is at least 2 bytes long as this function is only called by
% parse_boundary_eol/3 when CRLF has been found so a more tuple will never
@@ -127,11 +161,11 @@ parse_boundary_crlf(Bin, Pattern) ->
% considered part of the boundary so EOL needs to be searched again.
parse_boundary_eol(Bin, Pattern).
--spec parse_headers(binary(), pattern()) -> more(part_result()).
+-spec parse_headers(binary(), patterns()) -> more(part_result()).
parse_headers(Bin, Pattern) ->
parse_headers(Bin, Pattern, []).
--spec parse_headers(binary(), pattern(), http_headers()) -> more(part_result()).
+-spec parse_headers(binary(), patterns(), http_headers()) -> more(part_result()).
parse_headers(Bin, Pattern, Acc) ->
case erlang:decode_packet(httph_bin, Bin, []) of
{ok, {http_header, _, Name, _, Value}, Rest} ->
@@ -150,8 +184,8 @@ parse_headers(Bin, Pattern, Acc) ->
more(Bin, fun (NewBin) -> parse_headers(NewBin, Pattern, Acc) end)
end.
--spec parse_body(binary(), pattern()) -> more(body_result()).
-parse_body(Bin, Pattern = {P, PSize}) when byte_size(Bin) >= PSize ->
+-spec parse_body(binary(), patterns()) -> more(body_result()).
+parse_body(Bin, Pattern = {{P, PSize}, _}) when byte_size(Bin) >= PSize ->
case binary:match(Bin, P) of
{0, _Length} ->
<<_:PSize/binary, Rest/binary>> = Bin,
@@ -163,19 +197,27 @@ parse_body(Bin, Pattern = {P, PSize}) when byte_size(Bin) >= PSize ->
FResult = end_of_part(Rest, Pattern),
{body, PBody, fun () -> FResult end};
nomatch ->
- PartialLength = byte_size(Bin) - PSize + 1,
- <<PBody:PartialLength/binary, Rest/binary>> = Bin,
- {body, PBody, fun () -> parse_body(Rest, Pattern) end}
+ case suffix_match(Bin, Pattern) of
+ nomatch ->
+ %% Prefix of boundary not found at end of input. it's
+ %% safe to return the whole binary. Saves copying of
+ %% next input onto tail of current input binary.
+ {body, Bin, fun () -> parse_body(<<>>, Pattern) end};
+ {BoundaryStart, Len} ->
+ PBody = binary:part(Bin, 0, BoundaryStart),
+ Rest = binary:part(Bin, BoundaryStart, Len),
+ {body, PBody, fun () -> parse_body(Rest, Pattern) end}
+ end
end;
parse_body(Bin, Pattern) ->
more(Bin, fun (NewBin) -> parse_body(NewBin, Pattern) end).
--spec end_of_part(binary(), pattern()) -> end_of_part().
+-spec end_of_part(binary(), patterns()) -> end_of_part().
end_of_part(Bin, Pattern) ->
{end_of_part, fun () -> parse_boundary_tail(Bin, Pattern) end}.
--spec skip(binary(), pattern()) -> more(part_result()).
-skip(Bin, Pattern = {P, PSize}) ->
+-spec skip(binary(), patterns()) -> more(part_result()).
+skip(Bin, Pattern = {{P, PSize}, _}) ->
case binary:match(Bin, P) of
{BoundaryStart, _Length} ->
% Boundary found, proceed with parsing of the next part.
@@ -255,4 +297,20 @@ title(Bin) ->
),
iolist_to_binary(Title).
+suffix_test_() ->
+ [?_assertEqual(Part, suffix_match(Packet, pattern(Boundary))) ||
+ {Part, Packet, Boundary} <- [
+ {nomatch, <<>>, <<"ABC">>},
+ {{0, 1}, <<"\r">>, <<"ABC">>},
+ {{0, 2}, <<"\r\n">>, <<"ABC">>},
+ {{0, 4}, <<"\r\n--">>, <<"ABC">>},
+ {{0, 5}, <<"\r\n--A">>, <<"ABC">>},
+ {{0, 6}, <<"\r\n--AB">>, <<"ABC">>},
+ {{0, 7}, <<"\r\n--ABC">>, <<"ABC">>},
+ {nomatch, <<"\r\n--AB1">>, <<"ABC">>},
+ {{1, 1}, <<"1\r">>, <<"ABC">>},
+ {{2, 2}, <<"12\r\n">>, <<"ABC">>},
+ {{3, 4}, <<"123\r\n--">>, <<"ABC">>}
+ ]].
+
-endif.
diff --git a/src/cowboy_protocol.erl b/src/cowboy_protocol.erl
index c5ea561..7ab66a9 100644
--- a/src/cowboy_protocol.erl
+++ b/src/cowboy_protocol.erl
@@ -379,7 +379,9 @@ request(B, State=#state{transport=Transport}, M, P, Q, F, Version, Headers) ->
request(B, State, M, P, Q, F, Version, Headers,
<<>>, default_port(Transport:name()));
{_, RawHost} ->
- case parse_host(RawHost, <<>>) of
+ case catch parse_host(RawHost, <<>>) of
+ {'EXIT', _} ->
+ error_terminate(400, State);
{Host, undefined} ->
request(B, State, M, P, Q, F, Version, Headers,
Host, default_port(Transport:name()));
@@ -440,19 +442,19 @@ request(Buffer, State=#state{socket=Socket, transport=Transport,
Req = cowboy_req:new(Socket, Transport, Method, Path, Query, Fragment,
Version, Headers, Host, Port, Buffer, ReqKeepalive < MaxKeepalive,
OnResponse),
- onrequest(Req, State, Host, Path).
+ onrequest(Req, State, Host).
%% Call the global onrequest callback. The callback can send a reply,
%% in which case we consider the request handled and move on to the next
%% one. Note that since we haven't dispatched yet, we don't know the
%% handler, host_info, path_info or bindings yet.
--spec onrequest(cowboy_req:req(), #state{}, binary(), binary()) -> ok.
-onrequest(Req, State=#state{onrequest=undefined}, Host, Path) ->
- dispatch(Req, State, Host, Path);
-onrequest(Req, State=#state{onrequest=OnRequest}, Host, Path) ->
+-spec onrequest(cowboy_req:req(), #state{}, binary()) -> ok.
+onrequest(Req, State=#state{onrequest=undefined}, Host) ->
+ dispatch(Req, State, Host, cowboy_req:get(path, Req));
+onrequest(Req, State=#state{onrequest=OnRequest}, Host) ->
Req2 = OnRequest(Req),
case cowboy_req:get(resp_state, Req2) of
- waiting -> dispatch(Req2, State, Host, Path);
+ waiting -> dispatch(Req2, State, Host, cowboy_req:get(path, Req2));
_ -> next_request(Req2, State, ok)
end.
@@ -464,6 +466,8 @@ dispatch(Req, State=#state{dispatch=Dispatch}, Host, Path) ->
handler_init(Req2, State, Handler, Opts);
{error, notfound, host} ->
error_terminate(400, State);
+ {error, badrequest, path} ->
+ error_terminate(400, State);
{error, notfound, path} ->
error_terminate(404, State)
end.
@@ -489,16 +493,18 @@ handler_init(Req, State=#state{transport=Transport}, Handler, Opts) ->
handler_terminate(Req2, Handler, HandlerState);
%% @todo {upgrade, transport, Module}
{upgrade, protocol, Module} ->
- upgrade_protocol(Req, State, Handler, Opts, Module)
+ upgrade_protocol(Req, State, Handler, Opts, Module);
+ {upgrade, protocol, Module, Req2, Opts2} ->
+ upgrade_protocol(Req2, State, Handler, Opts2, Module)
catch Class:Reason ->
error_terminate(500, State),
error_logger:error_msg(
- "** Handler ~p terminating in init/3~n"
+ "** Cowboy handler ~p terminating in ~p/~p~n"
" for the reason ~p:~p~n"
"** Options were ~p~n"
"** Request was ~p~n"
"** Stacktrace: ~p~n~n",
- [Handler, Class, Reason, Opts,
+ [Handler, init, 3, Class, Reason, Opts,
cowboy_req:to_list(Req), erlang:get_stacktrace()])
end.
@@ -518,12 +524,12 @@ handler_handle(Req, State, Handler, HandlerState) ->
terminate_request(Req2, State, Handler, HandlerState2)
catch Class:Reason ->
error_logger:error_msg(
- "** Handler ~p terminating in handle/2~n"
+ "** Cowboy handler ~p terminating in ~p/~p~n"
" for the reason ~p:~p~n"
"** Handler state was ~p~n"
"** Request was ~p~n"
"** Stacktrace: ~p~n~n",
- [Handler, Class, Reason, HandlerState,
+ [Handler, handle, 2, Class, Reason, HandlerState,
cowboy_req:to_list(Req), erlang:get_stacktrace()]),
handler_terminate(Req, Handler, HandlerState),
error_terminate(500, State)
@@ -576,12 +582,12 @@ handler_call(Req, State, Handler, HandlerState, Message) ->
Handler, HandlerState2)
catch Class:Reason ->
error_logger:error_msg(
- "** Handler ~p terminating in info/3~n"
+ "** Cowboy handler ~p terminating in ~p/~p~n"
" for the reason ~p:~p~n"
"** Handler state was ~p~n"
"** Request was ~p~n"
"** Stacktrace: ~p~n~n",
- [Handler, Class, Reason, HandlerState,
+ [Handler, info, 3, Class, Reason, HandlerState,
cowboy_req:to_list(Req), erlang:get_stacktrace()]),
handler_terminate(Req, Handler, HandlerState),
error_terminate(500, State)
@@ -593,12 +599,12 @@ handler_terminate(Req, Handler, HandlerState) ->
Handler:terminate(cowboy_req:lock(Req), HandlerState)
catch Class:Reason ->
error_logger:error_msg(
- "** Handler ~p terminating in terminate/2~n"
+ "** Cowboy handler ~p terminating in ~p/~p~n"
" for the reason ~p:~p~n"
"** Handler state was ~p~n"
"** Request was ~p~n"
"** Stacktrace: ~p~n~n",
- [Handler, Class, Reason, HandlerState,
+ [Handler, terminate, 2, Class, Reason, HandlerState,
cowboy_req:to_list(Req), erlang:get_stacktrace()])
end.
diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl
index 33aaa33..7f3b566 100644
--- a/src/cowboy_req.erl
+++ b/src/cowboy_req.erl
@@ -118,6 +118,12 @@
-include_lib("eunit/include/eunit.hrl").
-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()}).
-record(http_req, {
@@ -183,15 +189,13 @@ new(Socket, Transport, Method, Path, Query, Fragment,
method=Method, path=Path, qs=Query, fragment=Fragment, version=Version,
headers=Headers, host=Host, port=Port, buffer=Buffer,
onresponse=OnResponse},
- case CanKeepalive of
+ case CanKeepalive and (Version =:= {1, 1}) of
false ->
Req#http_req{connection=close};
true ->
case lists:keyfind(<<"connection">>, 1, Headers) of
- false when Version =:= {1, 1} ->
- Req; %% keepalive
false ->
- Req#http_req{connection=close};
+ Req; %% keepalive
{_, ConnectionHeader} ->
Tokens = parse_connection_before(ConnectionHeader, []),
Connection = connection_to_atom(Tokens),
@@ -335,11 +339,11 @@ host_url(Req=#http_req{transport=Transport, host=Host, port=Port}) ->
-spec url(Req) -> {undefined | binary(), Req} when Req::req().
url(Req=#http_req{}) ->
{HostURL, Req2} = host_url(Req),
- url2(HostURL, Req2).
+ url(HostURL, Req2).
-url2(undefined, Req=#http_req{}) ->
+url(undefined, Req=#http_req{}) ->
{undefined, Req};
-url2(HostURL, Req=#http_req{path=Path, qs=QS, fragment=Fragment}) ->
+url(HostURL, Req=#http_req{path=Path, qs=QS, fragment=Fragment}) ->
QS2 = case QS of
<<>> -> <<>>;
_ -> << "?", QS/binary >>
@@ -439,6 +443,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) when Name =:= <<"content-type">> ->
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,
fun (Value) ->
@@ -490,11 +496,10 @@ cookie(Name, Req) when is_binary(Name) ->
-spec cookie(binary(), Req, Default)
-> {binary() | true | Default, Req} when Req::req(), Default::any().
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=[]}};
- {RawCookie, Req2} ->
- Cookies = cowboy_cookies:parse_cookie(RawCookie),
+ {ok, Cookies, Req2} ->
cookie(Name, Req2#http_req{cookies=Cookies}, Default)
end;
cookie(Name, Req, Default) ->
@@ -506,11 +511,10 @@ cookie(Name, Req, Default) ->
%% @doc Return the full list of cookie values.
-spec cookies(Req) -> {list({binary(), binary() | true}), Req} when Req::req().
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=[]}};
- {RawCookie, Req2} ->
- Cookies = cowboy_cookies:parse_cookie(RawCookie),
+ {ok, Cookies, Req2} ->
cookies(Req2#http_req{cookies=Cookies})
end;
cookies(Req=#http_req{cookies=Cookies}) ->
@@ -540,7 +544,7 @@ meta(Name, Req, Default) ->
%% If the value already exists it will be overwritten.
-spec set_meta(atom(), any(), Req) -> Req when Req::req().
set_meta(Name, Value, Req=#http_req{meta=Meta}) ->
- Req#http_req{meta=[{Name, Value}|lists:keydelete(Name, 1, Meta)]}.
+ Req#http_req{meta=lists:keyreplace(Name, 1, Meta, {Name, Value})}.
%% Request Body API.
@@ -803,11 +807,17 @@ multipart_skip(Req) ->
%% Response API.
%% @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).
+%%
+%% The cookie name cannot contain any of the following characters:
+%% =,;\s\t\r\n\013\014
+%%
+%% The cookie value cannot contain any of the following characters:
+%% ,; \t\r\n\013\014
+-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.
-spec set_resp_header(binary(), iodata(), Req)
@@ -1145,8 +1155,18 @@ response_merge_headers(Headers, RespHeaders, DefaultHeaders) ->
-spec merge_headers(cowboy_http:headers(), cowboy_http:headers())
-> cowboy_http:headers().
+
+%% Merge headers by prepending the tuples in the second list to the
+%% first list. It also handles Set-Cookie properly, which supports
+%% duplicated entries. Notice that, while the RFC2109 does allow more
+%% than one cookie to be set per Set-Cookie header, we are following
+%% the implementation of common web servers and applications which
+%% return many distinct headers per each Set-Cookie entry to avoid
+%% issues with clients/browser which may not support it.
merge_headers(Headers, []) ->
Headers;
+merge_headers(Headers, [{<<"set-cookie">>, Value}|Tail]) ->
+ merge_headers([{<<"set-cookie">>, Value}|Headers], Tail);
merge_headers(Headers, [{Name, Value}|Tail]) ->
Headers2 = case lists:keymember(Name, 1, Headers) of
true -> Headers;
@@ -1343,4 +1363,26 @@ connection_to_atom_test_() ->
[{lists:flatten(io_lib:format("~p", [T])),
fun() -> R = connection_to_atom(T) end} || {T, R} <- Tests].
+merge_headers_test() ->
+ Left0 = [{<<"content-length">>,<<"13">>},{<<"server">>,<<"Cowboy">>}],
+ Right0 = [{<<"set-cookie">>,<<"foo=bar">>},{<<"content-length">>,<<"11">>}],
+
+ ?assertMatch(
+ [{<<"set-cookie">>,<<"foo=bar">>},
+ {<<"content-length">>,<<"13">>},
+ {<<"server">>,<<"Cowboy">>}],
+ merge_headers(Left0, Right0)),
+
+ Left1 = [{<<"content-length">>,<<"13">>},{<<"server">>,<<"Cowboy">>}],
+ Right1 = [{<<"set-cookie">>,<<"foo=bar">>},{<<"set-cookie">>,<<"bar=baz">>}],
+
+ ?assertMatch(
+ [{<<"set-cookie">>,<<"bar=baz">>},
+ {<<"set-cookie">>,<<"foo=bar">>},
+ {<<"content-length">>,<<"13">>},
+ {<<"server">>,<<"Cowboy">>}],
+ merge_headers(Left1, Right1)),
+
+ ok.
+
-endif.
diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl
index 1c0554a..c6b53bd 100644
--- a/src/cowboy_rest.erl
+++ b/src/cowboy_rest.erl
@@ -40,7 +40,7 @@
language_a :: undefined | binary(),
%% Charset.
- charsets_p = [] :: [{binary(), atom()}],
+ charsets_p = [] :: [{binary(), integer()}],
charset_a :: undefined | binary(),
%% Cached resource calls.
@@ -73,10 +73,10 @@ upgrade(_ListenerPid, Handler, Opts, Req) ->
catch Class:Reason ->
PLReq = cowboy_req:to_list(Req),
error_logger:error_msg(
- "** Handler ~p terminating in rest_init/2~n"
+ "** Cowboy handler ~p terminating in ~p/~p~n"
" for the reason ~p:~p~n** Options were ~p~n"
"** Request was ~p~n** Stacktrace: ~p~n~n",
- [Handler, Class, Reason, Opts, PLReq, erlang:get_stacktrace()]),
+ [Handler, rest_init, 2, Class, Reason, Opts, PLReq, erlang:get_stacktrace()]),
{ok, _Req2} = cowboy_req:reply(500, Req),
close
end.
@@ -195,8 +195,9 @@ options(Req, State) ->
%%
%% Note that it is also possible to return a binary content type that will
%% then be parsed by Cowboy. However note that while this may make your
-%% resources a little more readable, this is a lot less efficient. An example
-%% of such a return value would be:
+%% resources a little more readable, this is a lot less efficient.
+%%
+%% An example of such return value would be:
%% {<<"text/html">>, to_html}
content_types_provided(Req, State) ->
case call(Req, State, content_types_provided) of
@@ -210,14 +211,15 @@ content_types_provided(Req, State) ->
CTP2 = [normalize_content_types(P) || P <- CTP],
State2 = State#state{
handler_state=HandlerState, content_types_p=CTP2},
- {ok, Accept, Req3} = cowboy_req:parse_header(<<"accept">>, Req2),
- case Accept of
- undefined ->
+ case cowboy_req:parse_header(<<"accept">>, Req2) of
+ {error, badarg} ->
+ respond(Req2, State2, 400);
+ {ok, undefined, Req3} ->
{PMT, _Fun} = HeadCTP = hd(CTP2),
languages_provided(
cowboy_req:set_meta(media_type, PMT, Req3),
State2#state{content_type_a=HeadCTP});
- Accept ->
+ {ok, Accept, Req3} ->
Accept2 = prioritize_accept(Accept),
choose_media_type(Req3, State2, Accept2)
end
@@ -725,9 +727,19 @@ put_resource(Req, State, OnTrue) ->
%% list of content types, otherwise it'll shadow the ones following.
choose_content_type(Req, State, _OnTrue, _ContentType, []) ->
respond(Req, State, 415);
-choose_content_type(Req, State, OnTrue, ContentType, [{Accepted, Fun}|_Tail])
+choose_content_type(Req,
+ State=#state{handler=Handler, handler_state=HandlerState},
+ OnTrue, ContentType, [{Accepted, Fun}|_Tail])
when Accepted =:= '*' orelse Accepted =:= ContentType ->
case call(Req, State, Fun) of
+ no_call ->
+ error_logger:error_msg(
+ "** Cowboy handler ~p terminating; "
+ "function ~p/~p was not exported~n"
+ "** Request was ~p~n** State was ~p~n~n",
+ [Handler, Fun, 2, cowboy_req:to_list(Req), HandlerState]),
+ {ok, _} = cowboy_req:reply(500, Req),
+ close;
{halt, Req2, HandlerState} ->
terminate(Req2, State#state{handler_state=HandlerState});
{true, Req2, HandlerState} ->
@@ -758,19 +770,28 @@ has_resp_body(Req, State) ->
%% Set the response headers and call the callback found using
%% content_types_provided/2 to obtain the request body and add
%% it to the response.
-set_resp_body(Req, State=#state{content_type_a={_Type, Fun}}) ->
+set_resp_body(Req, State=#state{handler=Handler, handler_state=HandlerState,
+ content_type_a={_Type, Fun}}) ->
{Req2, State2} = set_resp_etag(Req, State),
{LastModified, Req3, State3} = last_modified(Req2, State2),
- case LastModified of
+ Req4 = case LastModified of
LastModified when is_atom(LastModified) ->
- Req4 = Req3;
+ Req3;
LastModified ->
- LastModifiedStr = httpd_util:rfc1123_date(LastModified),
- Req4 = cowboy_req:set_resp_header(
- <<"last-modified">>, LastModifiedStr, Req3)
+ LastModifiedBin = cowboy_clock:rfc1123(LastModified),
+ cowboy_req:set_resp_header(
+ <<"last-modified">>, LastModifiedBin, Req3)
end,
{Req5, State4} = set_resp_expires(Req4, State3),
case call(Req5, State4, Fun) of
+ no_call ->
+ error_logger:error_msg(
+ "** Cowboy handler ~p terminating; "
+ "function ~p/~p was not exported~n"
+ "** Request was ~p~n** State was ~p~n~n",
+ [Handler, Fun, 2, cowboy_req:to_list(Req5), HandlerState]),
+ {ok, _} = cowboy_req:reply(500, Req5),
+ close;
{halt, Req6, HandlerState} ->
terminate(Req6, State4#state{handler_state=HandlerState});
{Body, Req6, HandlerState} ->
@@ -810,9 +831,9 @@ set_resp_expires(Req, State) ->
Expires when is_atom(Expires) ->
{Req2, State2};
Expires ->
- ExpiresStr = httpd_util:rfc1123_date(Expires),
+ ExpiresBin = cowboy_clock:rfc1123(Expires),
Req3 = cowboy_req:set_resp_header(
- <<"expires">>, ExpiresStr, Req2),
+ <<"expires">>, ExpiresBin, Req2),
{Req3, State2}
end.
diff --git a/src/cowboy_static.erl b/src/cowboy_static.erl
index 724bf33..55d01c7 100644
--- a/src/cowboy_static.erl
+++ b/src/cowboy_static.erl
@@ -158,7 +158,7 @@
%% {file, <<"index.html">>}]}
%%
%% %% Serve cowboy/priv/www/page.html under http://example.com/*/page
-%% {['*', <<"page">>], cowboy_static,
+%% {['_', <<"page">>], cowboy_static,
%% [{directory, {priv_dir, cowboy, [<<"www">>]}}
%% {file, <<"page.html">>}]}.
%%
@@ -217,6 +217,7 @@ rest_init(Req, Opts) ->
Directory1 = directory_path(Directory),
Mimetypes = proplists:get_value(mimetypes, Opts, []),
Mimetypes1 = case Mimetypes of
+ {{M, F}, E} -> {fun M:F/2, E};
{_, _} -> Mimetypes;
[] -> {fun path_to_mimetypes/2, []};
[_|_] -> {fun path_to_mimetypes/2, Mimetypes}
@@ -321,58 +322,10 @@ content_types_provided(Req, #state{filepath=Filepath,
file_contents(Req, #state{filepath=Filepath,
fileinfo={ok, #file_info{size=Filesize}}}=State) ->
{ok, Transport, Socket} = cowboy_req:transport(Req),
- Writefile = content_function(Transport, Socket, Filepath),
+ Writefile = fun() -> Transport:sendfile(Socket, Filepath) end,
{{stream, Filesize, Writefile}, Req, State}.
-%% @private Return a function writing the contents of a file to a socket.
-%% The function returns the number of bytes written to the socket to enable
-%% the calling function to determine if the expected number of bytes were
-%% written to the socket.
--spec content_function(module(), inet:socket(), binary()) ->
- fun(() -> {sent, non_neg_integer()}).
-content_function(Transport, Socket, Filepath) ->
- %% `file:sendfile/2' will only work with the `ranch_tcp'
- %% transport module. SSL or future SPDY transports that require the
- %% content to be encrypted or framed as the content is sent
- %% will use the fallback mechanism.
- case erlang:function_exported(file, sendfile, 2) of
- false ->
- fun() -> sfallback(Transport, Socket, Filepath) end;
- _ when Transport =/= ranch_tcp ->
- fun() -> sfallback(Transport, Socket, Filepath) end;
- true ->
- fun() -> sendfile(Socket, Filepath) end
- end.
-
-
-%% @private Sendfile fallback function.
--spec sfallback(module(), inet:socket(), binary()) -> {sent, non_neg_integer()}.
-sfallback(Transport, Socket, Filepath) ->
- {ok, File} = file:open(Filepath, [read,binary,raw]),
- sfallback(Transport, Socket, File, 0).
-
--spec sfallback(module(), inet:socket(), file:io_device(),
- non_neg_integer()) -> {sent, non_neg_integer()}.
-sfallback(Transport, Socket, File, Sent) ->
- case file:read(File, 16#1FFF) of
- eof ->
- ok = file:close(File),
- {sent, Sent};
- {ok, Bin} ->
- case Transport:send(Socket, Bin) of
- ok -> sfallback(Transport, Socket, File, Sent + byte_size(Bin));
- {error, closed} -> {sent, Sent}
- end
- end.
-
-
-%% @private Wrapper for sendfile function.
--spec sendfile(inet:socket(), binary()) -> {sent, non_neg_integer()}.
-sendfile(Socket, Filepath) ->
- {ok, Sent} = file:sendfile(Filepath, Socket),
- {sent, Sent}.
-
-spec directory_path(dirspec()) -> dirpath().
directory_path({priv_dir, App, []}) ->
priv_dir_path(App);
diff --git a/src/cowboy_websocket.erl b/src/cowboy_websocket.erl
index 1c6d20c..8c02ac7 100644
--- a/src/cowboy_websocket.erl
+++ b/src/cowboy_websocket.erl
@@ -21,6 +21,11 @@
%% Internal.
-export([handler_loop/4]).
+-type frame() :: close | ping | pong
+ | {text | binary | close | ping | pong, binary()}
+ | {close, 1000..4999, binary()}.
+-export_type([frame/0]).
+
-type opcode() :: 0 | 1 | 2 | 8 | 9 | 10.
-type mask_key() :: 0..16#ffffffff.
@@ -125,12 +130,12 @@ handler_init(State=#state{transport=Transport, handler=Handler, opts=Opts},
closed
catch Class:Reason ->
upgrade_error(Req),
- PLReq = cowboy_req:to_list(Req),
error_logger:error_msg(
- "** Handler ~p terminating in websocket_init/3~n"
+ "** Cowboy handler ~p terminating in ~p/~p~n"
" for the reason ~p:~p~n** Options were ~p~n"
"** Request was ~p~n** Stacktrace: ~p~n~n",
- [Handler, Class, Reason, Opts, PLReq, erlang:get_stacktrace()])
+ [Handler, websocket_init, 3, Class, Reason, Opts,
+ cowboy_req:to_list(Req),erlang:get_stacktrace()])
end.
-spec upgrade_error(cowboy_req:req()) -> closed.
@@ -149,9 +154,9 @@ websocket_handshake(State=#state{socket=Socket, transport=Transport,
{<< "http", Location/binary >>, Req1} = cowboy_req:url(Req),
{ok, Req2} = cowboy_req:upgrade_reply(
<<"101 WebSocket Protocol Handshake">>,
- [{<<"Upgrade">>, <<"WebSocket">>},
- {<<"Sec-Websocket-Location">>, << "ws", Location/binary >>},
- {<<"Sec-Websocket-Origin">>, Origin}],
+ [{<<"upgrade">>, <<"WebSocket">>},
+ {<<"sec-websocket-location">>, << "ws", Location/binary >>},
+ {<<"sec-websocket-origin">>, Origin}],
Req1),
%% Flush the resp_sent message before moving on.
receive {cowboy_req, resp_sent} -> ok after 0 -> ok end,
@@ -171,18 +176,20 @@ websocket_handshake(State=#state{socket=Socket, transport=Transport,
handler_before_loop(State#state{messages=Transport:messages()},
Req4, HandlerState, <<>>);
_Any ->
- closed %% If an error happened reading the body, stop there.
+ %% If an error happened reading the body, stop there.
+ handler_terminate(State, Req3, HandlerState, {error, closed})
end;
websocket_handshake(State=#state{transport=Transport, challenge=Challenge},
Req, HandlerState) ->
{ok, Req2} = cowboy_req:upgrade_reply(
101,
- [{<<"Upgrade">>, <<"websocket">>},
- {<<"Sec-Websocket-Accept">>, Challenge}],
+ [{<<"upgrade">>, <<"websocket">>},
+ {<<"sec-websocket-accept">>, Challenge}],
Req),
%% Flush the resp_sent message before moving on.
receive {cowboy_req, resp_sent} -> ok after 0 -> ok end,
- handler_before_loop(State#state{messages=Transport:messages()},
+ State2 = handler_loop_timeout(State),
+ handler_before_loop(State2#state{messages=Transport:messages()},
Req2, HandlerState, <<>>).
-spec handler_before_loop(#state{}, cowboy_req:req(), any(), binary()) -> closed.
@@ -190,15 +197,13 @@ handler_before_loop(State=#state{
socket=Socket, transport=Transport, hibernate=true},
Req, HandlerState, SoFar) ->
Transport:setopts(Socket, [{active, once}]),
- State2 = handler_loop_timeout(State),
catch erlang:hibernate(?MODULE, handler_loop,
- [State2#state{hibernate=false}, Req, HandlerState, SoFar]),
+ [State#state{hibernate=false}, Req, HandlerState, SoFar]),
closed;
handler_before_loop(State=#state{socket=Socket, transport=Transport},
Req, HandlerState, SoFar) ->
Transport:setopts(Socket, [{active, once}]),
- State2 = handler_loop_timeout(State),
- handler_loop(State2, Req, HandlerState, SoFar).
+ handler_loop(State, Req, HandlerState, SoFar).
-spec handler_loop_timeout(#state{}) -> #state{}.
handler_loop_timeout(State=#state{timeout=infinity}) ->
@@ -216,7 +221,8 @@ handler_loop(State=#state{
Req, HandlerState, SoFar) ->
receive
{OK, Socket, Data} ->
- websocket_data(State, Req, HandlerState,
+ State2 = handler_loop_timeout(State),
+ websocket_data(State2, Req, HandlerState,
<< SoFar/binary, Data/binary >>);
{Closed, Socket} ->
handler_terminate(State, Req, HandlerState, {error, closed});
@@ -452,38 +458,76 @@ handler_call(State=#state{handler=Handler, opts=Opts}, Req, HandlerState,
Req2, HandlerState2, RemainingData);
{reply, Payload, Req2, HandlerState2}
when is_tuple(Payload) ->
- ok = websocket_send(Payload, State),
- NextState(State, Req2, HandlerState2, RemainingData);
+ case websocket_send(Payload, State) of
+ ok ->
+ State2 = handler_loop_timeout(State),
+ NextState(State2, Req2, HandlerState2, RemainingData);
+ shutdown ->
+ handler_terminate(State, Req2, HandlerState,
+ {normal, shutdown});
+ {error, _} = Error ->
+ handler_terminate(State, Req2, HandlerState2, Error)
+ end;
{reply, Payload, Req2, HandlerState2, hibernate}
when is_tuple(Payload) ->
- ok = websocket_send(Payload, State),
- NextState(State#state{hibernate=true},
- Req2, HandlerState2, RemainingData);
+ case websocket_send(Payload, State) of
+ ok ->
+ State2 = handler_loop_timeout(State),
+ NextState(State2#state{hibernate=true},
+ Req2, HandlerState2, RemainingData);
+ shutdown ->
+ handler_terminate(State, Req2, HandlerState,
+ {normal, shutdown});
+ {error, _} = Error ->
+ handler_terminate(State, Req2, HandlerState2, Error)
+ end;
{reply, Payload, Req2, HandlerState2}
when is_list(Payload) ->
- ok = websocket_send_many(Payload, State),
- NextState(State, Req2, HandlerState2, RemainingData);
+ case websocket_send_many(Payload, State) of
+ ok ->
+ State2 = handler_loop_timeout(State),
+ NextState(State2, Req2, HandlerState2, RemainingData);
+ shutdown ->
+ handler_terminate(State, Req2, HandlerState,
+ {normal, shutdown});
+ {error, _} = Error ->
+ handler_terminate(State, Req2, HandlerState2, Error)
+ end;
{reply, Payload, Req2, HandlerState2, hibernate}
when is_list(Payload) ->
- ok = websocket_send_many(Payload, State),
- NextState(State#state{hibernate=true},
- Req2, HandlerState2, RemainingData);
+ case websocket_send_many(Payload, State) of
+ ok ->
+ State2 = handler_loop_timeout(State),
+ NextState(State2#state{hibernate=true},
+ Req2, HandlerState2, RemainingData);
+ shutdown ->
+ handler_terminate(State, Req2, HandlerState,
+ {normal, shutdown});
+ {error, _} = Error ->
+ handler_terminate(State, Req2, HandlerState2, Error)
+ end;
{shutdown, Req2, HandlerState2} ->
websocket_close(State, Req2, HandlerState2, {normal, shutdown})
catch Class:Reason ->
PLReq = cowboy_req:to_list(Req),
error_logger:error_msg(
- "** Handler ~p terminating in ~p/3~n"
+ "** Cowboy handler ~p terminating in ~p/~p~n"
" for the reason ~p:~p~n** Message was ~p~n"
"** Options were ~p~n** Handler state was ~p~n"
"** Request was ~p~n** Stacktrace: ~p~n~n",
- [Handler, Callback, Class, Reason, Message, Opts,
+ [Handler, Callback, 3, Class, Reason, Message, Opts,
HandlerState, PLReq, erlang:get_stacktrace()]),
websocket_close(State, Req, HandlerState, {error, handler})
end.
--spec websocket_send({text | binary | ping | pong, binary()}, #state{})
- -> ok | {error, atom()}.
+websocket_opcode(text) -> 1;
+websocket_opcode(binary) -> 2;
+websocket_opcode(close) -> 8;
+websocket_opcode(ping) -> 9;
+websocket_opcode(pong) -> 10.
+
+-spec websocket_send(frame(), #state{})
+ -> ok | shutdown | {error, atom()}.
%% hixie-76 text frame.
websocket_send({text, Payload}, #state{
socket=Socket, transport=Transport, version=0}) ->
@@ -491,24 +535,52 @@ websocket_send({text, Payload}, #state{
%% Ignore all unknown frame types for compatibility with hixie 76.
websocket_send(_Any, #state{version=0}) ->
ok;
+websocket_send(Type, #state{socket=Socket, transport=Transport})
+ when Type =:= close ->
+ Opcode = websocket_opcode(Type),
+ case Transport:send(Socket, << 1:1, 0:3, Opcode:4, 0:8 >>) of
+ ok -> shutdown;
+ Error -> Error
+ end;
+websocket_send(Type, #state{socket=Socket, transport=Transport})
+ when Type =:= ping; Type =:= pong ->
+ Opcode = websocket_opcode(Type),
+ Transport:send(Socket, << 1:1, 0:3, Opcode:4, 0:8 >>);
+websocket_send({close, Payload}, State) ->
+ websocket_send({close, 1000, Payload}, State);
+websocket_send({Type = close, StatusCode, Payload}, #state{
+ socket=Socket, transport=Transport}) ->
+ Opcode = websocket_opcode(Type),
+ Len = 2 + iolist_size(Payload),
+ %% Control packets must not be > 125 in length.
+ true = Len =< 125,
+ BinLen = hybi_payload_length(Len),
+ Transport:send(Socket,
+ [<< 1:1, 0:3, Opcode:4, 0:1, BinLen/bits, StatusCode:16 >>, Payload]),
+ shutdown;
websocket_send({Type, Payload}, #state{socket=Socket, transport=Transport}) ->
- Opcode = case Type of
- text -> 1;
- binary -> 2;
- ping -> 9;
- pong -> 10
+ Opcode = websocket_opcode(Type),
+ Len = iolist_size(Payload),
+ %% Control packets must not be > 125 in length.
+ true = if Type =:= ping; Type =:= pong ->
+ Len =< 125;
+ true ->
+ true
end,
- Len = hybi_payload_length(iolist_size(Payload)),
- Transport:send(Socket, [<< 1:1, 0:3, Opcode:4, 0:1, Len/bits >>,
- Payload]).
+ BinLen = hybi_payload_length(Len),
+ Transport:send(Socket,
+ [<< 1:1, 0:3, Opcode:4, 0:1, BinLen/bits >>, Payload]).
--spec websocket_send_many([{text | binary | ping | pong, binary()}], #state{})
- -> ok | {error, atom()}.
+-spec websocket_send_many([frame()], #state{})
+ -> ok | shutdown | {error, atom()}.
websocket_send_many([], _) ->
ok;
websocket_send_many([Frame|Tail], State) ->
- ok = websocket_send(Frame, State),
- websocket_send_many(Tail, State).
+ case websocket_send(Frame, State) of
+ ok -> websocket_send_many(Tail, State);
+ shutdown -> shutdown;
+ Error -> Error
+ end.
-spec websocket_close(#state{}, cowboy_req:req(), any(), {atom(), atom()})
-> closed.
@@ -516,7 +588,6 @@ websocket_close(State=#state{socket=Socket, transport=Transport, version=0},
Req, HandlerState, Reason) ->
Transport:send(Socket, << 255, 0 >>),
handler_terminate(State, Req, HandlerState, Reason);
-%% @todo Send a Payload? Using Reason is usually good but we're quite careless.
websocket_close(State=#state{socket=Socket, transport=Transport},
Req, HandlerState, Reason) ->
Transport:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>),
@@ -531,11 +602,11 @@ handler_terminate(#state{handler=Handler, opts=Opts},
catch Class:Reason ->
PLReq = cowboy_req:to_list(Req),
error_logger:error_msg(
- "** Handler ~p terminating in websocket_terminate/3~n"
+ "** Cowboy handler ~p terminating in ~p/~p~n"
" for the reason ~p:~p~n** Initial reason was ~p~n"
"** Options were ~p~n** Handler state was ~p~n"
"** Request was ~p~n** Stacktrace: ~p~n~n",
- [Handler, Class, Reason, TerminateReason, Opts,
+ [Handler, websocket_terminate, 3, Class, Reason, TerminateReason, Opts,
HandlerState, PLReq, erlang:get_stacktrace()])
end,
closed.
diff --git a/src/cowboy_websocket_handler.erl b/src/cowboy_websocket_handler.erl
index 34749ba..6d7f9de 100644
--- a/src/cowboy_websocket_handler.erl
+++ b/src/cowboy_websocket_handler.erl
@@ -66,15 +66,15 @@
-callback websocket_handle({text | binary | ping | pong, binary()}, Req, State)
-> {ok, Req, State}
| {ok, Req, State, hibernate}
- | {reply, {text | binary | ping | pong, binary()}, Req, State}
- | {reply, {text | binary | ping | pong, binary()}, Req, State, hibernate}
+ | {reply, cowboy_websocket:frame() | [cowboy_websocket:frame()], Req, State}
+ | {reply, cowboy_websocket:frame() | [cowboy_websocket:frame()], Req, State, hibernate}
| {shutdown, Req, State}
when Req::cowboy_req:req(), State::state().
-callback websocket_info(any(), Req, State)
-> {ok, Req, State}
| {ok, Req, State, hibernate}
- | {reply, {text | binary | ping | pong, binary()}, Req, State}
- | {reply, {text | binary | ping | pong, binary()}, Req, State, hibernate}
+ | {reply, cowboy_websocket:frame() | [cowboy_websocket:frame()], Req, State}
+ | {reply, cowboy_websocket:frame() | [cowboy_websocket:frame()], Req, State, hibernate}
| {shutdown, Req, State}
when Req::cowboy_req:req(), State::state().
-callback websocket_terminate(terminate_reason(), cowboy_req:req(), state())