diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/cowboy_clock.erl | 54 | ||||
-rw-r--r-- | src/cowboy_cookies.erl | 416 | ||||
-rw-r--r-- | src/cowboy_dispatcher.erl | 43 | ||||
-rw-r--r-- | src/cowboy_http.erl | 228 | ||||
-rw-r--r-- | src/cowboy_multipart.erl | 92 | ||||
-rw-r--r-- | src/cowboy_protocol.erl | 38 | ||||
-rw-r--r-- | src/cowboy_req.erl | 84 | ||||
-rw-r--r-- | src/cowboy_rest.erl | 57 | ||||
-rw-r--r-- | src/cowboy_static.erl | 53 | ||||
-rw-r--r-- | src/cowboy_websocket.erl | 159 | ||||
-rw-r--r-- | src/cowboy_websocket_handler.erl | 8 |
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()) |