diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/cowboy.app.src | 2 | ||||
-rw-r--r-- | src/cowboy.erl | 2 | ||||
-rw-r--r-- | src/cowboy_app.erl | 2 | ||||
-rw-r--r-- | src/cowboy_bstr.erl | 51 | ||||
-rw-r--r-- | src/cowboy_client.erl | 2 | ||||
-rw-r--r-- | src/cowboy_clock.erl | 4 | ||||
-rw-r--r-- | src/cowboy_dispatcher.erl | 291 | ||||
-rw-r--r-- | src/cowboy_handler.erl | 220 | ||||
-rw-r--r-- | src/cowboy_http.erl | 88 | ||||
-rw-r--r-- | src/cowboy_http_handler.erl | 12 | ||||
-rw-r--r-- | src/cowboy_loop_handler.erl | 12 | ||||
-rw-r--r-- | src/cowboy_middleware.erl | 36 | ||||
-rw-r--r-- | src/cowboy_protocol.erl | 317 | ||||
-rw-r--r-- | src/cowboy_req.erl | 201 | ||||
-rw-r--r-- | src/cowboy_rest.erl | 114 | ||||
-rw-r--r-- | src/cowboy_router.erl | 565 | ||||
-rw-r--r-- | src/cowboy_static.erl | 12 | ||||
-rw-r--r-- | src/cowboy_sup.erl | 2 | ||||
-rw-r--r-- | src/cowboy_websocket.erl | 724 | ||||
-rw-r--r-- | src/cowboy_websocket_handler.erl | 4 |
20 files changed, 1699 insertions, 962 deletions
diff --git a/src/cowboy.app.src b/src/cowboy.app.src index d32262e..59fa8fe 100644 --- a/src/cowboy.app.src +++ b/src/cowboy.app.src @@ -1,4 +1,4 @@ -%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]> +%% Copyright (c) 2011-2013, Loïc Hoguin <[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 diff --git a/src/cowboy.erl b/src/cowboy.erl index 9e4a66a..79dbb71 100644 --- a/src/cowboy.erl +++ b/src/cowboy.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]> +%% Copyright (c) 2011-2013, Loïc Hoguin <[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 diff --git a/src/cowboy_app.erl b/src/cowboy_app.erl index 180d400..b46ba1d 100644 --- a/src/cowboy_app.erl +++ b/src/cowboy_app.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]> +%% Copyright (c) 2011-2013, Loïc Hoguin <[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 diff --git a/src/cowboy_bstr.erl b/src/cowboy_bstr.erl index 6e5b353..bc6818f 100644 --- a/src/cowboy_bstr.erl +++ b/src/cowboy_bstr.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]> +%% Copyright (c) 2011-2013, Loïc Hoguin <[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 @@ -16,16 +16,40 @@ -module(cowboy_bstr). %% Binary strings. +-export([capitalize_token/1]). -export([to_lower/1]). %% Characters. -export([char_to_lower/1]). -export([char_to_upper/1]). +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). +-endif. + +%% @doc Capitalize a token. +%% +%% The first letter and all letters after a dash are capitalized. +%% This is the form seen for header names in the HTTP/1.1 RFC and +%% others. Note that using this form isn't required, as header name +%% are case insensitive, and it is only provided for use with eventual +%% badly implemented clients. +-spec capitalize_token(B) -> B when B::binary(). +capitalize_token(B) -> + capitalize_token(B, true, <<>>). +capitalize_token(<<>>, _, Acc) -> + Acc; +capitalize_token(<< $-, Rest/bits >>, _, Acc) -> + capitalize_token(Rest, true, << Acc/binary, $- >>); +capitalize_token(<< C, Rest/bits >>, true, Acc) -> + capitalize_token(Rest, false, << Acc/binary, (char_to_upper(C)) >>); +capitalize_token(<< C, Rest/bits >>, false, Acc) -> + capitalize_token(Rest, false, << Acc/binary, (char_to_lower(C)) >>). + %% @doc Convert a binary string to lowercase. --spec to_lower(binary()) -> binary(). -to_lower(L) -> - << << (char_to_lower(C)) >> || << C >> <= L >>. +-spec to_lower(B) -> B when B::binary(). +to_lower(B) -> + << << (char_to_lower(C)) >> || << C >> <= B >>. %% @doc Convert [A-Z] characters to lowercase. %% @end @@ -88,3 +112,22 @@ char_to_upper($x) -> $X; char_to_upper($y) -> $Y; char_to_upper($z) -> $Z; char_to_upper(Ch) -> Ch. + +%% Tests. + +-ifdef(TEST). + +capitalize_token_test_() -> + %% {Header, Result} + Tests = [ + {<<"heLLo-woRld">>, <<"Hello-World">>}, + {<<"Sec-Websocket-Version">>, <<"Sec-Websocket-Version">>}, + {<<"Sec-WebSocket-Version">>, <<"Sec-Websocket-Version">>}, + {<<"sec-websocket-version">>, <<"Sec-Websocket-Version">>}, + {<<"SEC-WEBSOCKET-VERSION">>, <<"Sec-Websocket-Version">>}, + {<<"Sec-WebSocket--Version">>, <<"Sec-Websocket--Version">>}, + {<<"Sec-WebSocket---Version">>, <<"Sec-Websocket---Version">>} + ], + [{H, fun() -> R = capitalize_token(H) end} || {H, R} <- Tests]. + +-endif. diff --git a/src/cowboy_client.erl b/src/cowboy_client.erl index fee6793..4d958b1 100644 --- a/src/cowboy_client.erl +++ b/src/cowboy_client.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2012, Loïc Hoguin <[email protected]> +%% Copyright (c) 2012-2013, Loïc Hoguin <[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 diff --git a/src/cowboy_clock.erl b/src/cowboy_clock.erl index b439bb1..71bcb21 100644 --- a/src/cowboy_clock.erl +++ b/src/cowboy_clock.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]> +%% Copyright (c) 2011-2013, Loïc Hoguin <[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 @@ -71,7 +71,7 @@ rfc1123() -> rfc1123(DateTime) -> update_rfc1123(<<>>, undefined, DateTime). -%% @doc Return the current date and time formatted according to RFC-2109. +%% @doc Return the given date and time formatted according to RFC-2109. %% %% This format is used in the <em>set-cookie</em> header sent with %% HTTP responses. diff --git a/src/cowboy_dispatcher.erl b/src/cowboy_dispatcher.erl deleted file mode 100644 index ef6e8ac..0000000 --- a/src/cowboy_dispatcher.erl +++ /dev/null @@ -1,291 +0,0 @@ -%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]> -%% Copyright (c) 2011, Anthony Ramine <[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 Dispatch requests according to a hostname and path. --module(cowboy_dispatcher). - -%% API. --export([match/3]). - --type bindings() :: [{atom(), binary()}]. --type tokens() :: [binary()]. --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()]. - --export_type([bindings/0]). --export_type([tokens/0]). --export_type([dispatch_rules/0]). - --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). --endif. - -%% API. - -%% @doc Match hostname tokens and path tokens against dispatch rules. -%% -%% It is typically used for matching tokens for the hostname and path of -%% the request against a global dispatch rule for your listener. -%% -%% Dispatch rules are a list of <em>{Hostname, PathRules}</em> tuples, with -%% <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, `<<"*">>', 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 -%% corresponding token value and return it. -%% -%% The list of hostname tokens is reversed before matching. For example, if -%% we were to match "www.ninenines.eu", we would first match "eu", then -%% "ninenines", then "www". This means that in the context of hostnames, -%% the <em>'...'</em> atom matches properly the lower levels of the domain -%% as would be expected. -%% -%% When a result is found, this function will return the handler module and -%% options found in the dispatch list, a key-value list of bindings and -%% the tokens that were matched by the <em>'...'</em> atom for both the -%% hostname and path. --spec match(dispatch_rules(), Host::binary() | tokens(), Path::binary()) - -> {ok, module(), any(), bindings(), - HostInfo::undefined | tokens(), - PathInfo::undefined | tokens()} - | {error, notfound, host} | {error, notfound, path} - | {error, badrequest, path}. -match([], _, _) -> - {error, notfound, host}; -match([{'_', PathMatchs}|_Tail], _, Path) -> - match_path(PathMatchs, undefined, Path, []); -match([{HostMatch, PathMatchs}|Tail], Tokens, Path) - when is_list(Tokens) -> - case list_match(Tokens, lists:reverse(HostMatch), []) of - false -> - match(Tail, Tokens, Path); - {true, Bindings, undefined} -> - match_path(PathMatchs, undefined, Path, Bindings); - {true, Bindings, HostInfo} -> - match_path(PathMatchs, lists:reverse(HostInfo), - Path, Bindings) - end; -match(Dispatch, Host, Path) -> - match(Dispatch, split_host(Host), Path). - --spec match_path(dispatch_path(), - HostInfo::undefined | tokens(), binary() | tokens(), bindings()) - -> {ok, module(), any(), bindings(), - HostInfo::undefined | tokens(), - PathInfo::undefined | tokens()} - | {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) -> - {ok, Handler, Opts, Bindings, HostInfo, undefined}; -match_path([{PathMatch, Handler, Opts}|Tail], HostInfo, Tokens, - Bindings) when is_list(Tokens) -> - case list_match(Tokens, PathMatch, []) of - false -> - match_path(Tail, HostInfo, Tokens, Bindings); - {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). - -%% Internal. - -%% @doc Split a hostname into a list of tokens. --spec split_host(binary()) -> tokens(). -split_host(Host) -> - split_host(Host, []). - -split_host(Host, Acc) -> - case binary:match(Host, <<".">>) of - nomatch when Host =:= <<>> -> - Acc; - nomatch -> - [Host|Acc]; - {Pos, _} -> - << Segment:Pos/binary, _:8, Rest/bits >> = Host, - false = byte_size(Segment) == 0, - split_host(Rest, [Segment|Acc]) - end. - -%% @doc Split a path into a list of path segments. -%% -%% Following RFC2396, this function may return path segments containing any -%% character, including <em>/</em> if, and only if, a <em>/</em> was escaped -%% and part of a path segment. --spec split_path(binary()) -> tokens(). -split_path(<< $/, Path/bits >>) -> - split_path(Path, []); -split_path(_) -> - badrequest. - -split_path(Path, 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()) - -> {true, bindings(), undefined | tokens()} | false. -%% Atom '...' matches any trailing path, stop right now. -list_match(List, ['...'], Binds) -> - {true, Binds, List}; -%% Atom '_' matches anything, continue. -list_match([_E|Tail], ['_'|TailMatch], Binds) -> - list_match(Tail, TailMatch, Binds); -%% Both values match, continue. -list_match([E|Tail], [E|TailMatch], Binds) -> - list_match(Tail, TailMatch, Binds); -%% Bind E to the variable name V and continue. -list_match([E|Tail], [V|TailMatch], Binds) when is_atom(V) -> - list_match(Tail, TailMatch, [{V, E}|Binds]); -%% Match complete. -list_match([], [], Binds) -> - {true, Binds, undefined}; -%% Values don't match, stop. -list_match(_List, _Match, _Binds) -> - false. - -%% Tests. - --ifdef(TEST). - -split_host_test_() -> - %% {Host, Result} - Tests = [ - {<<"">>, []}, - {<<"*">>, [<<"*">>]}, - {<<"cowboy.ninenines.eu">>, - [<<"eu">>, <<"ninenines">>, <<"cowboy">>]}, - {<<"ninenines.eu">>, - [<<"eu">>, <<"ninenines">>]}, - {<<"a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z">>, - [<<"z">>, <<"y">>, <<"x">>, <<"w">>, <<"v">>, <<"u">>, <<"t">>, - <<"s">>, <<"r">>, <<"q">>, <<"p">>, <<"o">>, <<"n">>, <<"m">>, - <<"l">>, <<"k">>, <<"j">>, <<"i">>, <<"h">>, <<"g">>, <<"f">>, - <<"e">>, <<"d">>, <<"c">>, <<"b">>, <<"a">>]} - ], - [{H, fun() -> R = split_host(H) end} || {H, R} <- Tests]. - -split_path_test_() -> - %% {Path, Result, QueryString} - Tests = [ - {<<"/">>, []}, - {<<"/extend//cowboy">>, [<<"extend">>, <<>>, <<"cowboy">>]}, - {<<"/users">>, [<<"users">>]}, - {<<"/users/42/friends">>, [<<"users">>, <<"42">>, <<"friends">>]}, - {<<"/users/a+b/c%21d">>, [<<"users">>, <<"a b">>, <<"c!d">>]} - ], - [{P, fun() -> R = split_path(P) end} || {P, R} <- Tests]. - -match_test_() -> - Dispatch = [ - {[<<"www">>, '_', <<"ninenines">>, <<"eu">>], [ - {[<<"users">>, '_', <<"mails">>], match_any_subdomain_users, []} - ]}, - {[<<"ninenines">>, <<"eu">>], [ - {[<<"users">>, id, <<"friends">>], match_extend_users_friends, []}, - {'_', match_extend, []} - ]}, - {[<<"ninenines">>, var], [ - {[<<"threads">>, var], match_duplicate_vars, - [we, {expect, two}, var, here]} - ]}, - {[<<"erlang">>, ext], [ - {'_', match_erlang_ext, []} - ]}, - {'_', [ - {[<<"users">>, id, <<"friends">>], match_users_friends, []}, - {'_', match_any, []} - ]} - ], - %% {Host, Path, Result} - Tests = [ - {<<"any">>, <<"/">>, {ok, match_any, [], []}}, - {<<"www.any.ninenines.eu">>, <<"/users/42/mails">>, - {ok, match_any_subdomain_users, [], []}}, - {<<"www.ninenines.eu">>, <<"/users/42/mails">>, - {ok, match_any, [], []}}, - {<<"www.ninenines.eu">>, <<"/">>, - {ok, match_any, [], []}}, - {<<"www.any.ninenines.eu">>, <<"/not_users/42/mails">>, - {error, notfound, path}}, - {<<"ninenines.eu">>, <<"/">>, - {ok, match_extend, [], []}}, - {<<"ninenines.eu">>, <<"/users/42/friends">>, - {ok, match_extend_users_friends, [], [{id, <<"42">>}]}}, - {<<"erlang.fr">>, '_', - {ok, match_erlang_ext, [], [{ext, <<"fr">>}]}}, - {<<"any">>, <<"/users/444/friends">>, - {ok, match_users_friends, [], [{id, <<"444">>}]}}, - {<<"ninenines.fr">>, <<"/threads/987">>, - {ok, match_duplicate_vars, [we, {expect, two}, var, here], - [{var, <<"fr">>}, {var, <<"987">>}]}} - ], - [{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() -> - {ok, Handler, Opts, Binds, undefined, undefined} - = match(Dispatch, H, P) - end} || {H, P, {ok, Handler, Opts, Binds}} <- Tests]. - -match_info_test_() -> - Dispatch = [ - {[<<"www">>, <<"ninenines">>, <<"eu">>], [ - {[<<"pathinfo">>, <<"is">>, <<"next">>, '...'], match_path, []} - ]}, - {['...', <<"ninenines">>, <<"eu">>], [ - {'_', match_any, []} - ]} - ], - Tests = [ - {<<"ninenines.eu">>, <<"/">>, - {ok, match_any, [], [], [], undefined}}, - {<<"bugs.ninenines.eu">>, <<"/">>, - {ok, match_any, [], [], [<<"bugs">>], undefined}}, - {<<"cowboy.bugs.ninenines.eu">>, <<"/">>, - {ok, match_any, [], [], [<<"cowboy">>, <<"bugs">>], undefined}}, - {<<"www.ninenines.eu">>, <<"/pathinfo/is/next">>, - {ok, match_path, [], [], undefined, []}}, - {<<"www.ninenines.eu">>, <<"/pathinfo/is/next/path_info">>, - {ok, match_path, [], [], undefined, [<<"path_info">>]}}, - {<<"www.ninenines.eu">>, <<"/pathinfo/is/next/foo/bar">>, - {ok, match_path, [], [], undefined, [<<"foo">>, <<"bar">>]}} - ], - [{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() -> - R = match(Dispatch, H, P) - end} || {H, P, R} <- Tests]. - --endif. diff --git a/src/cowboy_handler.erl b/src/cowboy_handler.erl new file mode 100644 index 0000000..7ed7db3 --- /dev/null +++ b/src/cowboy_handler.erl @@ -0,0 +1,220 @@ +%% Copyright (c) 2011-2013, Loïc Hoguin <[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 Handler middleware. +%% +%% Execute the handler given by the <em>handler</em> and <em>handler_opts</em> +%% environment values. The result of this execution is added to the +%% environment under the <em>result</em> value. +%% +%% @see cowboy_http_handler +-module(cowboy_handler). +-behaviour(cowboy_middleware). + +-export([execute/2]). +-export([handler_loop/4]). + +-record(state, { + env :: cowboy_middleware:env(), + hibernate = false :: boolean(), + loop_timeout = infinity :: timeout(), + loop_timeout_ref :: undefined | reference(), + resp_sent = false :: boolean() +}). + +%% @private +-spec execute(Req, Env) + -> {ok, Req, Env} | {error, 500, Req} + | {suspend, ?MODULE, handler_loop, [any()]} + when Req::cowboy_req:req(), Env::cowboy_middleware:env(). +execute(Req, Env) -> + {_, Handler} = lists:keyfind(handler, 1, Env), + {_, HandlerOpts} = lists:keyfind(handler_opts, 1, Env), + handler_init(Req, #state{env=Env}, Handler, HandlerOpts). + +-spec handler_init(Req, #state{}, module(), any()) + -> {ok, Req, cowboy_middleware:env()} + | {error, 500, Req} | {suspend, module(), function(), [any()]} + when Req::cowboy_req:req(). +handler_init(Req, State, Handler, HandlerOpts) -> + Transport = cowboy_req:get(transport, Req), + try Handler:init({Transport:name(), http}, Req, HandlerOpts) of + {ok, Req2, HandlerState} -> + handler_handle(Req2, State, Handler, HandlerState); + {loop, Req2, HandlerState} -> + handler_before_loop(Req2, State#state{hibernate=false}, + Handler, HandlerState); + {loop, Req2, HandlerState, hibernate} -> + handler_before_loop(Req2, State#state{hibernate=true}, + Handler, HandlerState); + {loop, Req2, HandlerState, Timeout} -> + handler_before_loop(Req2, State#state{loop_timeout=Timeout}, + Handler, HandlerState); + {loop, Req2, HandlerState, Timeout, hibernate} -> + handler_before_loop(Req2, State#state{ + hibernate=true, loop_timeout=Timeout}, Handler, HandlerState); + {shutdown, Req2, HandlerState} -> + terminate_request(Req2, State, Handler, HandlerState, + {normal, shutdown}); + %% @todo {upgrade, transport, Module} + {upgrade, protocol, Module} -> + upgrade_protocol(Req, State, Handler, HandlerOpts, Module); + {upgrade, protocol, Module, Req2, HandlerOpts2} -> + upgrade_protocol(Req2, State, Handler, HandlerOpts2, Module) + catch Class:Reason -> + error_logger:error_msg( + "** 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, init, 3, Class, Reason, HandlerOpts, + cowboy_req:to_list(Req), erlang:get_stacktrace()]), + error_terminate(Req, State) + end. + +-spec upgrade_protocol(Req, #state{}, module(), any(), module()) + -> {ok, Req, Env} + | {suspend, module(), atom(), any()} + | {halt, Req} + | {error, cowboy_http:status(), Req} + when Req::cowboy_req:req(), Env::cowboy_middleware:env(). +upgrade_protocol(Req, #state{env=Env}, + Handler, HandlerOpts, Module) -> + Module:upgrade(Req, Env, Handler, HandlerOpts). + +-spec handler_handle(Req, #state{}, module(), any()) + -> {ok, Req, cowboy_middleware:env()} + | {error, 500, Req} + when Req::cowboy_req:req(). +handler_handle(Req, State, Handler, HandlerState) -> + try Handler:handle(Req, HandlerState) of + {ok, Req2, HandlerState2} -> + terminate_request(Req2, State, Handler, HandlerState2, + {normal, shutdown}) + catch Class:Reason -> + error_logger:error_msg( + "** 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, handle, 2, Class, Reason, HandlerState, + cowboy_req:to_list(Req), erlang:get_stacktrace()]), + handler_terminate(Req, Handler, HandlerState, Reason), + error_terminate(Req, State) + end. + +%% We don't listen for Transport closes because that would force us +%% to receive data and buffer it indefinitely. +-spec handler_before_loop(Req, #state{}, module(), any()) + -> {ok, Req, cowboy_middleware:env()} + | {error, 500, Req} | {suspend, module(), function(), [any()]} + when Req::cowboy_req:req(). +handler_before_loop(Req, State=#state{hibernate=true}, Handler, HandlerState) -> + State2 = handler_loop_timeout(State), + {suspend, ?MODULE, handler_loop, + [Req, State2#state{hibernate=false}, Handler, HandlerState]}; +handler_before_loop(Req, State, Handler, HandlerState) -> + State2 = handler_loop_timeout(State), + handler_loop(Req, State2, Handler, HandlerState). + +%% Almost the same code can be found in cowboy_websocket. +-spec handler_loop_timeout(#state{}) -> #state{}. +handler_loop_timeout(State=#state{loop_timeout=infinity}) -> + State#state{loop_timeout_ref=undefined}; +handler_loop_timeout(State=#state{loop_timeout=Timeout, + loop_timeout_ref=PrevRef}) -> + _ = case PrevRef of undefined -> ignore; PrevRef -> + erlang:cancel_timer(PrevRef) end, + TRef = erlang:start_timer(Timeout, self(), ?MODULE), + State#state{loop_timeout_ref=TRef}. + +%% @private +-spec handler_loop(Req, #state{}, module(), any()) + -> {ok, Req, cowboy_middleware:env()} + | {error, 500, Req} | {suspend, module(), function(), [any()]} + when Req::cowboy_req:req(). +handler_loop(Req, State=#state{loop_timeout_ref=TRef}, Handler, HandlerState) -> + receive + {cowboy_req, resp_sent} -> + handler_loop(Req, State#state{resp_sent=true}, + Handler, HandlerState); + {timeout, TRef, ?MODULE} -> + terminate_request(Req, State, Handler, HandlerState, + {normal, timeout}); + {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) -> + handler_loop(Req, State, Handler, HandlerState); + Message -> + handler_call(Req, State, Handler, HandlerState, Message) + end. + +-spec handler_call(Req, #state{}, module(), any(), any()) + -> {ok, Req, cowboy_middleware:env()} + | {error, 500, Req} | {suspend, module(), function(), [any()]} + when Req::cowboy_req:req(). +handler_call(Req, State, Handler, HandlerState, Message) -> + try Handler:info(Message, Req, HandlerState) of + {ok, Req2, HandlerState2} -> + terminate_request(Req2, State, Handler, HandlerState2, + {normal, shutdown}); + {loop, Req2, HandlerState2} -> + handler_before_loop(Req2, State, Handler, HandlerState2); + {loop, Req2, HandlerState2, hibernate} -> + handler_before_loop(Req2, State#state{hibernate=true}, + Handler, HandlerState2) + catch Class:Reason -> + error_logger:error_msg( + "** 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, info, 3, Class, Reason, HandlerState, + cowboy_req:to_list(Req), erlang:get_stacktrace()]), + handler_terminate(Req, Handler, HandlerState, Reason), + error_terminate(Req, State) + end. + +-spec terminate_request(Req, #state{}, module(), any(), + {normal, timeout | shutdown} | {error, atom()}) -> + {ok, Req, cowboy_middleware:env()} when Req::cowboy_req:req(). +terminate_request(Req, #state{env=Env}, Handler, HandlerState, Reason) -> + HandlerRes = handler_terminate(Req, Handler, HandlerState, Reason), + {ok, Req, [{result, HandlerRes}|Env]}. + +-spec handler_terminate(cowboy_req:req(), module(), any(), + {normal, timeout | shutdown} | {error, atom()}) -> ok. +handler_terminate(Req, Handler, HandlerState, Reason) -> + try + Handler:terminate(Reason, cowboy_req:lock(Req), HandlerState) + catch Class:Reason2 -> + error_logger:error_msg( + "** 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, terminate, 3, Class, Reason2, HandlerState, + cowboy_req:to_list(Req), erlang:get_stacktrace()]) + end. + +%% Only send an error reply if there is no resp_sent message. +-spec error_terminate(Req, #state{}) + -> {error, 500, Req} | {halt, Req} when Req::cowboy_req:req(). +error_terminate(Req, #state{resp_sent=true}) -> + %% Close the connection, but do not attempt sending a reply. + {halt, cowboy_req:set([{connection, close}, {resp_state, done}], Req)}; +error_terminate(Req, _) -> + {error, 500, Req}. diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl index 66383cb..a78e090 100644 --- a/src/cowboy_http.erl +++ b/src/cowboy_http.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]> +%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]> %% Copyright (c) 2011, Anthony Ramine <[email protected]> %% %% Permission to use, copy, modify, and/or distribute this software for any @@ -36,6 +36,7 @@ -export([token/2]). -export([token_ci/2]). -export([quoted_string/2]). +-export([authorization/2]). %% Decoding. -export([te_chunked/2]). @@ -801,25 +802,72 @@ qvalue(<< C, Rest/binary >>, Fun, Q, M) qvalue(Data, Fun, Q, _M) -> Fun(Data, Q). +%% @doc Parse authorization value according rfc 2617. +%% Only Basic authorization is supported so far. +-spec authorization(binary(), binary()) -> {binary(), any()} | {error, badarg}. +authorization(UserPass, Type = <<"basic">>) -> + cowboy_http:whitespace(UserPass, + fun(D) -> + authorization_basic_userid(base64:mime_decode(D), + fun(Rest, Userid) -> + authorization_basic_password(Rest, + fun(Password) -> + {Type, {Userid, Password}} + end) + end) + end); +authorization(String, Type) -> + {Type, String}. + +%% @doc Parse user credentials. +-spec authorization_basic_userid(binary(), fun()) -> any(). +authorization_basic_userid(Data, Fun) -> + authorization_basic_userid(Data, Fun, <<>>). + +authorization_basic_userid(<<>>, _Fun, _Acc) -> + {error, badarg}; +authorization_basic_userid(<<C, _Rest/binary>>, _Fun, Acc) + when C < 32; C =:= 127; (C =:=$: andalso Acc =:= <<>>) -> + {error, badarg}; +authorization_basic_userid(<<$:, Rest/binary>>, Fun, Acc) -> + Fun(Rest, Acc); +authorization_basic_userid(<<C, Rest/binary>>, Fun, Acc) -> + authorization_basic_userid(Rest, Fun, <<Acc/binary, C>>). + +-spec authorization_basic_password(binary(), fun()) -> any(). +authorization_basic_password(Data, Fun) -> + authorization_basic_password(Data, Fun, <<>>). + +authorization_basic_password(<<>>, _Fun, <<>>) -> + {error, badarg}; +authorization_basic_password(<<C, _Rest/binary>>, _Fun, _Acc) + when C < 32; C=:= 127 -> + {error, badarg}; +authorization_basic_password(<<>>, Fun, Acc) -> + Fun(Acc); +authorization_basic_password(<<C, Rest/binary>>, Fun, Acc) -> + authorization_basic_password(Rest, Fun, <<Acc/binary, C>>). + %% Decoding. %% @doc Decode a stream of chunks. --spec te_chunked(binary(), {non_neg_integer(), non_neg_integer()}) - -> more | {ok, binary(), {non_neg_integer(), non_neg_integer()}} - | {ok, binary(), binary(), {non_neg_integer(), non_neg_integer()}} - | {done, non_neg_integer(), binary()} | {error, badarg}. -te_chunked(<<>>, _) -> - more; +-spec te_chunked(Bin, TransferState) + -> more | {more, non_neg_integer(), Bin, TransferState} + | {ok, Bin, TransferState} | {ok, Bin, Bin, TransferState} + | {done, non_neg_integer(), Bin} | {error, badarg} + when Bin::binary(), TransferState::{non_neg_integer(), non_neg_integer()}. te_chunked(<< "0\r\n\r\n", Rest/binary >>, {0, Streamed}) -> {done, Streamed, Rest}; te_chunked(Data, {0, Streamed}) -> %% @todo We are expecting an hex size, not a general token. token(Data, - fun (Rest, _) when byte_size(Rest) < 4 -> - more; - (<< "\r\n", Rest/binary >>, BinLen) -> + fun (<< "\r\n", Rest/binary >>, BinLen) -> Len = list_to_integer(binary_to_list(BinLen), 16), te_chunked(Rest, {Len, Streamed}); + %% Chunk size shouldn't take too many bytes, + %% don't try to stream forever. + (Rest, _) when byte_size(Rest) < 16 -> + more; (_, _) -> {error, badarg} end); @@ -827,13 +875,12 @@ te_chunked(Data, {ChunkRem, Streamed}) when byte_size(Data) >= ChunkRem + 2 -> << Chunk:ChunkRem/binary, "\r\n", Rest/binary >> = Data, {ok, Chunk, Rest, {0, Streamed + byte_size(Chunk)}}; te_chunked(Data, {ChunkRem, Streamed}) -> - Size = byte_size(Data), - {ok, Data, {ChunkRem - Size, Streamed + Size}}. + {more, ChunkRem + 2, Data, {ChunkRem, Streamed}}. %% @doc Decode an identity stream. --spec te_identity(binary(), {non_neg_integer(), non_neg_integer()}) - -> {ok, binary(), {non_neg_integer(), non_neg_integer()}} - | {done, binary(), non_neg_integer(), binary()}. +-spec te_identity(Bin, TransferState) + -> {ok, Bin, TransferState} | {done, Bin, non_neg_integer(), Bin} + when Bin::binary(), TransferState::{non_neg_integer(), non_neg_integer()}. te_identity(Data, {Streamed, Total}) when Streamed + byte_size(Data) < Total -> {ok, Data, {Streamed + byte_size(Data), Total}}; @@ -1294,4 +1341,15 @@ urlencode_test_() -> ?_assertEqual(<<"%ff+">>, urlencode(<<255, " ">>)) ]. +http_authorization_test_() -> + [?_assertEqual({<<"basic">>, {<<"Alladin">>, <<"open sesame">>}}, + authorization(<<"QWxsYWRpbjpvcGVuIHNlc2FtZQ==">>, <<"basic">>)), + ?_assertEqual({error, badarg}, + authorization(<<"dXNlcm5hbWUK">>, <<"basic">>)), + ?_assertEqual({error, badarg}, + authorization(<<"_[]@#$%^&*()-AA==">>, <<"basic">>)), + ?_assertEqual({error, badarg}, + authorization(<<"dXNlcjpwYXNzCA==">>, <<"basic">>)) %% user:pass\010 + ]. + -endif. diff --git a/src/cowboy_http_handler.erl b/src/cowboy_http_handler.erl index d686f30..0393153 100644 --- a/src/cowboy_http_handler.erl +++ b/src/cowboy_http_handler.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]> +%% Copyright (c) 2011-2013, Loïc Hoguin <[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 @@ -22,8 +22,8 @@ %% <em>handle/2</em> allows you to handle the request. It receives the %% state previously defined. %% -%% <em>terminate/2</em> allows you to clean up. It receives the state -%% previously defined. +%% <em>terminate/3</em> allows you to clean up. It receives the +%% termination reason and the state previously defined. %% %% There is no required operation to perform in any of these callbacks %% other than returning the proper values. Make sure you always return @@ -33,6 +33,9 @@ -type opts() :: any(). -type state() :: any(). +-type terminate_reason() :: {normal, shutdown} + | {normal, timeout} %% Only occurs in loop handlers. + | {error, atom()}. -callback init({atom(), http}, Req, opts()) -> {ok, Req, state()} @@ -42,7 +45,8 @@ | {loop, Req, state(), timeout(), hibernate} | {shutdown, Req, state()} | {upgrade, protocol, module()} + | {upgrade, protocol, module(), Req, opts()} when Req::cowboy_req:req(). -callback handle(Req, State) -> {ok, Req, State} when Req::cowboy_req:req(), State::state(). --callback terminate(cowboy_req:req(), state()) -> ok. +-callback terminate(terminate_reason(), cowboy_req:req(), state()) -> ok. diff --git a/src/cowboy_loop_handler.erl b/src/cowboy_loop_handler.erl index 5ff86cf..f8d008f 100644 --- a/src/cowboy_loop_handler.erl +++ b/src/cowboy_loop_handler.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]> +%% Copyright (c) 2011-2013, Loïc Hoguin <[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 @@ -23,8 +23,8 @@ %% receive. It receives the message and the state previously defined. %% It can decide to stop the receive loop or continue receiving. %% -%% <em>terminate/2</em> allows you to clean up. It receives the state -%% previously defined. +%% <em>terminate/3</em> allows you to clean up. It receives the +%% termination reason and the state previously defined. %% %% There is no required operation to perform in any of these callbacks %% other than returning the proper values. Make sure you always return @@ -39,6 +39,9 @@ -type opts() :: any(). -type state() :: any(). +-type terminate_reason() :: {normal, shutdown} + | {normal, timeout} + | {error, atom()}. -callback init({atom(), http}, Req, opts()) -> {ok, Req, state()} @@ -48,10 +51,11 @@ | {loop, Req, state(), timeout(), hibernate} | {shutdown, Req, state()} | {upgrade, protocol, module()} + | {upgrade, protocol, module(), Req, opts()} when Req::cowboy_req:req(). -callback info(any(), Req, State) -> {ok, Req, State} | {loop, Req, State} | {loop, Req, State, hibernate} when Req::cowboy_req:req(), State::state(). --callback terminate(cowboy_req:req(), state()) -> ok. +-callback terminate(terminate_reason(), cowboy_req:req(), state()) -> ok. diff --git a/src/cowboy_middleware.erl b/src/cowboy_middleware.erl new file mode 100644 index 0000000..0c1ca77 --- /dev/null +++ b/src/cowboy_middleware.erl @@ -0,0 +1,36 @@ +%% Copyright (c) 2013, Loïc Hoguin <[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 Behaviour for middlewares. +%% +%% Only one function needs to be implemented, <em>execute/2</em>. +%% It receives the Req and the environment and returns them +%% optionally modified. It can decide to stop the processing with +%% or without an error. It is also possible to hibernate the process +%% if needed. +%% +%% A middleware can perform any operation. Make sure you always return +%% the last modified Req so that Cowboy has the up to date information +%% about the request. +-module(cowboy_middleware). + +-type env() :: [{atom(), any()}]. +-export_type([env/0]). + +-callback execute(Req, Env) + -> {ok, Req, Env} + | {suspend, module(), atom(), any()} + | {halt, Req} + | {error, cowboy_http:status(), Req} + when Req::cowboy_req:req(), Env::env(). diff --git a/src/cowboy_protocol.erl b/src/cowboy_protocol.erl index 7344d1f..b479fa9 100644 --- a/src/cowboy_protocol.erl +++ b/src/cowboy_protocol.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]> +%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]> %% Copyright (c) 2011, Anthony Ramine <[email protected]> %% %% Permission to use, copy, modify, and/or distribute this software for any @@ -17,7 +17,10 @@ %% %% The available options are: %% <dl> -%% <dt>dispatch</dt><dd>The dispatch list for this protocol.</dd> +%% <dt>compress</dt><dd>Whether to automatically compress the response +%% body when the conditions are met. Disabled by default.</dd> +%% <dt>env</dt><dd>The environment passed and optionally modified +%% by middlewares.</dd> %% <dt>max_empty_lines</dt><dd>Max number of empty lines before a request. %% Defaults to 5.</dd> %% <dt>max_header_name_length</dt><dd>Max length allowed for header names. @@ -27,23 +30,22 @@ %% <dt>max_headers</dt><dd>Max number of headers allowed. %% Defaults to 100.</dd> %% <dt>max_keepalive</dt><dd>Max number of requests allowed in a single -%% keep-alive session. Defaults to infinity.</dd> +%% keep-alive session. Defaults to 100.</dd> %% <dt>max_request_line_length</dt><dd>Max length allowed for the request %% line. Defaults to 4096.</dd> +%% <dt>middlewares</dt><dd>The list of middlewares to execute when a +%% request is received.</dd> %% <dt>onrequest</dt><dd>Optional fun that allows Req interaction before %% any dispatching is done. Host info, path info and bindings are thus %% not available at this point.</dd> %% <dt>onresponse</dt><dd>Optional fun that allows replacing a response %% sent by the application.</dd> -%% <dt>timeout</dt><dd>Time in milliseconds before an idle -%% connection is closed. Defaults to 5000 milliseconds.</dd> +%% <dt>timeout</dt><dd>Time in milliseconds a client has to send the +%% full request line and headers. Defaults to 5000 milliseconds.</dd> %% </dl> %% %% Note that there is no need to monitor these processes when using Cowboy as %% an application as it already supervises them under the listener supervisor. -%% -%% @see cowboy_dispatcher -%% @see cowboy_http_handler -module(cowboy_protocol). %% API. @@ -52,20 +54,20 @@ %% Internal. -export([init/4]). -export([parse_request/3]). --export([handler_loop/4]). +-export([resume/6]). -type onrequest_fun() :: fun((Req) -> Req). -type onresponse_fun() :: fun((cowboy_http:status(), cowboy_http:headers(), iodata(), Req) -> Req). - -export_type([onrequest_fun/0]). -export_type([onresponse_fun/0]). -record(state, { - listener :: pid(), socket :: inet:socket(), transport :: module(), - dispatch :: cowboy_dispatcher:dispatch_rules(), + middlewares :: [module()], + compress :: boolean(), + env :: cowboy_middleware:env(), onrequest :: undefined | onrequest_fun(), onresponse = undefined :: undefined | onresponse_fun(), max_empty_lines :: non_neg_integer(), @@ -76,9 +78,7 @@ max_header_value_length :: non_neg_integer(), max_headers :: non_neg_integer(), timeout :: timeout(), - hibernate = false :: boolean(), - loop_timeout = infinity :: timeout(), - loop_timeout_ref :: undefined | reference() + until :: non_neg_integer() | infinity }). %% API. @@ -102,24 +102,34 @@ get_value(Key, Opts, Default) -> %% @private -spec init(pid(), inet:socket(), module(), any()) -> ok. init(ListenerPid, Socket, Transport, Opts) -> - Dispatch = get_value(dispatch, Opts, []), + Compress = get_value(compress, Opts, false), MaxEmptyLines = get_value(max_empty_lines, Opts, 5), MaxHeaderNameLength = get_value(max_header_name_length, Opts, 64), MaxHeaderValueLength = get_value(max_header_value_length, Opts, 4096), MaxHeaders = get_value(max_headers, Opts, 100), - MaxKeepalive = get_value(max_keepalive, Opts, infinity), + MaxKeepalive = get_value(max_keepalive, Opts, 100), MaxRequestLineLength = get_value(max_request_line_length, Opts, 4096), + Middlewares = get_value(middlewares, Opts, [cowboy_router, cowboy_handler]), + Env = [{listener, ListenerPid}|get_value(env, Opts, [])], OnRequest = get_value(onrequest, Opts, undefined), OnResponse = get_value(onresponse, Opts, undefined), Timeout = get_value(timeout, Opts, 5000), ok = ranch:accept_ack(ListenerPid), - wait_request(<<>>, #state{listener=ListenerPid, socket=Socket, - transport=Transport, dispatch=Dispatch, + wait_request(<<>>, #state{socket=Socket, transport=Transport, + middlewares=Middlewares, compress=Compress, env=Env, max_empty_lines=MaxEmptyLines, max_keepalive=MaxKeepalive, max_request_line_length=MaxRequestLineLength, max_header_name_length=MaxHeaderNameLength, max_header_value_length=MaxHeaderValueLength, max_headers=MaxHeaders, - timeout=Timeout, onrequest=OnRequest, onresponse=OnResponse}, 0). + onrequest=OnRequest, onresponse=OnResponse, + timeout=Timeout, until=until(Timeout)}, 0). + +-spec until(timeout()) -> non_neg_integer() | infinity. +until(infinity) -> + infinity; +until(Timeout) -> + {Me, S, Mi} = os:timestamp(), + Me * 1000000000 + S * 1000 + Mi div 1000 + Timeout. %% Request parsing. %% @@ -128,10 +138,24 @@ init(ListenerPid, Socket, Transport, Opts) -> %% right after the header parsing is finished and the code becomes %% more interesting past that point. +-spec recv(inet:socket(), module(), non_neg_integer() | infinity) + -> {ok, binary()} | {error, closed | timeout | atom()}. +recv(Socket, Transport, infinity) -> + Transport:recv(Socket, 0, infinity); +recv(Socket, Transport, Until) -> + {Me, S, Mi} = os:timestamp(), + Now = Me * 1000000000 + S * 1000 + Mi div 1000, + Timeout = Until - Now, + if Timeout < 0 -> + {error, timeout}; + true -> + Transport:recv(Socket, 0, Timeout) + end. + -spec wait_request(binary(), #state{}, non_neg_integer()) -> ok. wait_request(Buffer, State=#state{socket=Socket, transport=Transport, - timeout=Timeout}, ReqEmpty) -> - case Transport:recv(Socket, 0, Timeout) of + until=Until}, ReqEmpty) -> + case recv(Socket, Transport, Until) of {ok, Data} -> parse_request(<< Buffer/binary, Data/binary >>, State, ReqEmpty); {error, _} -> @@ -222,8 +246,8 @@ wait_header(_, State=#state{max_headers=MaxHeaders}, _, _, _, _, _, Headers) when length(Headers) >= MaxHeaders -> error_terminate(400, State); wait_header(Buffer, State=#state{socket=Socket, transport=Transport, - timeout=Timeout}, M, P, Q, F, V, H) -> - case Transport:recv(Socket, 0, Timeout) of + until=Until}, M, P, Q, F, V, H) -> + case recv(Socket, Transport, Until) of {ok, Data} -> parse_header(<< Buffer/binary, Data/binary >>, State, M, P, Q, F, V, H); @@ -294,9 +318,9 @@ parse_hd_name_ws(<< C, Rest/bits >>, S, M, P, Q, F, V, H, Name) -> end. wait_hd_before_value(Buffer, State=#state{ - socket=Socket, transport=Transport, timeout=Timeout}, + socket=Socket, transport=Transport, until=Until}, M, P, Q, F, V, H, N) -> - case Transport:recv(Socket, 0, Timeout) of + case recv(Socket, Transport, Until) of {ok, Data} -> parse_hd_before_value(<< Buffer/binary, Data/binary >>, State, M, P, Q, F, V, H, N); @@ -326,9 +350,9 @@ parse_hd_before_value(Buffer, State=#state{ %% to change the other arguments' position and trigger costy %% operations for no reasons. wait_hd_value(_, State=#state{ - socket=Socket, transport=Transport, timeout=Timeout}, + socket=Socket, transport=Transport, until=Until}, M, P, Q, F, V, H, N, SoFar) -> - case Transport:recv(Socket, 0, Timeout) of + case recv(Socket, Transport, Until) of {ok, Data} -> parse_hd_value(Data, State, M, P, Q, F, V, H, N, SoFar); {error, timeout} -> @@ -341,9 +365,9 @@ wait_hd_value(_, State=#state{ %% to check for multilines allows us to avoid a few tests in %% the critical path, but forces us to have a special function. wait_hd_value_nl(_, State=#state{ - socket=Socket, transport=Transport, timeout=Timeout}, + socket=Socket, transport=Transport, until=Until}, M, P, Q, F, V, Headers, Name, SoFar) -> - case Transport:recv(Socket, 0, Timeout) of + case recv(Socket, Transport, Until) of {ok, << C, Data/bits >>} when C =:= $\s; C =:= $\t -> parse_hd_value(Data, State, M, P, Q, F, V, Headers, Name, SoFar); {ok, Data} -> @@ -437,197 +461,86 @@ parse_host(<< C, Rest/bits >>, Acc) -> request(Buffer, State=#state{socket=Socket, transport=Transport, req_keepalive=ReqKeepalive, max_keepalive=MaxKeepalive, - onresponse=OnResponse}, + compress=Compress, onresponse=OnResponse}, Method, Path, Query, Fragment, Version, Headers, Host, Port) -> Req = cowboy_req:new(Socket, Transport, Method, Path, Query, Fragment, Version, Headers, Host, Port, Buffer, ReqKeepalive < MaxKeepalive, - OnResponse), - onrequest(Req, State, Host). + Compress, OnResponse), + onrequest(Req, State). %% 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()) -> ok. -onrequest(Req, State=#state{onrequest=undefined}, Host) -> - dispatch(Req, State, Host, cowboy_req:get(path, Req)); -onrequest(Req, State=#state{onrequest=OnRequest}, Host) -> +-spec onrequest(cowboy_req:req(), #state{}) -> ok. +onrequest(Req, State=#state{onrequest=undefined}) -> + execute(Req, State); +onrequest(Req, State=#state{onrequest=OnRequest}) -> Req2 = OnRequest(Req), case cowboy_req:get(resp_state, Req2) of - waiting -> dispatch(Req2, State, Host, cowboy_req:get(path, Req2)); + waiting -> execute(Req2, State); _ -> next_request(Req2, State, ok) end. --spec dispatch(cowboy_req:req(), #state{}, binary(), binary()) -> ok. -dispatch(Req, State=#state{dispatch=Dispatch}, Host, Path) -> - case cowboy_dispatcher:match(Dispatch, Host, Path) of - {ok, Handler, Opts, Bindings, HostInfo, PathInfo} -> - Req2 = cowboy_req:set_bindings(HostInfo, PathInfo, Bindings, Req), - handler_init(Req2, State, Handler, Opts); - {error, notfound, host} -> - error_terminate(400, Req, State); - {error, badrequest, path} -> - error_terminate(400, Req, State); - {error, notfound, path} -> - error_terminate(404, Req, State) - end. +-spec execute(cowboy_req:req(), #state{}) -> ok. +execute(Req, State=#state{middlewares=Middlewares, env=Env}) -> + execute(Req, State, Env, Middlewares). --spec handler_init(cowboy_req:req(), #state{}, module(), any()) -> ok. -handler_init(Req, State=#state{transport=Transport}, Handler, Opts) -> - try Handler:init({Transport:name(), http}, Req, Opts) of - {ok, Req2, HandlerState} -> - handler_handle(Req2, State, Handler, HandlerState); - {loop, Req2, HandlerState} -> - handler_before_loop(Req2, State#state{hibernate=false}, - Handler, HandlerState); - {loop, Req2, HandlerState, hibernate} -> - handler_before_loop(Req2, State#state{hibernate=true}, - Handler, HandlerState); - {loop, Req2, HandlerState, Timeout} -> - handler_before_loop(Req2, State#state{loop_timeout=Timeout}, - Handler, HandlerState); - {loop, Req2, HandlerState, Timeout, hibernate} -> - handler_before_loop(Req2, State#state{ - hibernate=true, loop_timeout=Timeout}, Handler, HandlerState); - {shutdown, Req2, HandlerState} -> - handler_terminate(Req2, Handler, HandlerState); - %% @todo {upgrade, transport, Module} - {upgrade, protocol, 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, Req, State), - error_logger:error_msg( - "** 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, init, 3, Class, Reason, Opts, - cowboy_req:to_list(Req), erlang:get_stacktrace()]) - end. - --spec upgrade_protocol(cowboy_req:req(), #state{}, module(), any(), module()) +-spec execute(cowboy_req:req(), #state{}, cowboy_middleware:env(), [module()]) -> ok. -upgrade_protocol(Req, State=#state{listener=ListenerPid}, - Handler, Opts, Module) -> - case Module:upgrade(ListenerPid, Handler, Opts, Req) of - {UpgradeRes, Req2} -> next_request(Req2, State, UpgradeRes); - _Any -> terminate(State) +execute(Req, State, Env, []) -> + next_request(Req, State, get_value(result, Env, ok)); +execute(Req, State, Env, [Middleware|Tail]) -> + case Middleware:execute(Req, Env) of + {ok, Req2, Env2} -> + execute(Req2, State, Env2, Tail); + {suspend, Module, Function, Args} -> + erlang:hibernate(?MODULE, resume, + [State, Env, Tail, Module, Function, Args]); + {halt, Req2} -> + next_request(Req2, State, ok); + {error, Code, Req2} -> + error_terminate(Code, Req2, State) end. --spec handler_handle(cowboy_req:req(), #state{}, module(), any()) -> ok. -handler_handle(Req, State, Handler, HandlerState) -> - try Handler:handle(Req, HandlerState) of - {ok, Req2, HandlerState2} -> - terminate_request(Req2, State, Handler, HandlerState2) - catch Class:Reason -> - error_logger:error_msg( - "** 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, handle, 2, Class, Reason, HandlerState, - cowboy_req:to_list(Req), erlang:get_stacktrace()]), - handler_terminate(Req, Handler, HandlerState), - error_terminate(500, Req, State) +-spec resume(#state{}, cowboy_middleware:env(), [module()], + module(), module(), [any()]) -> ok. +resume(State, Env, Tail, Module, Function, Args) -> + case apply(Module, Function, Args) of + {ok, Req2, Env2} -> + execute(Req2, State, Env2, Tail); + {suspend, Module2, Function2, Args2} -> + erlang:hibernate(?MODULE, resume, + [State, Env, Tail, Module2, Function2, Args2]); + {halt, Req2} -> + next_request(Req2, State, ok); + {error, Code, Req2} -> + error_terminate(Code, Req2, State) end. -%% We don't listen for Transport closes because that would force us -%% to receive data and buffer it indefinitely. --spec handler_before_loop(cowboy_req:req(), #state{}, module(), any()) -> ok. -handler_before_loop(Req, State=#state{hibernate=true}, Handler, HandlerState) -> - State2 = handler_loop_timeout(State), - catch erlang:hibernate(?MODULE, handler_loop, - [Req, State2#state{hibernate=false}, Handler, HandlerState]), - ok; -handler_before_loop(Req, State, Handler, HandlerState) -> - State2 = handler_loop_timeout(State), - handler_loop(Req, State2, Handler, HandlerState). - -%% Almost the same code can be found in cowboy_websocket. --spec handler_loop_timeout(#state{}) -> #state{}. -handler_loop_timeout(State=#state{loop_timeout=infinity}) -> - State#state{loop_timeout_ref=undefined}; -handler_loop_timeout(State=#state{loop_timeout=Timeout, - loop_timeout_ref=PrevRef}) -> - _ = case PrevRef of undefined -> ignore; PrevRef -> - erlang:cancel_timer(PrevRef) end, - TRef = erlang:start_timer(Timeout, self(), ?MODULE), - State#state{loop_timeout_ref=TRef}. - -%% @private --spec handler_loop(cowboy_req:req(), #state{}, module(), any()) -> ok. -handler_loop(Req, State=#state{loop_timeout_ref=TRef}, Handler, HandlerState) -> - receive - {timeout, TRef, ?MODULE} -> - terminate_request(Req, State, Handler, HandlerState); - {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) -> - handler_loop(Req, State, Handler, HandlerState); - Message -> - handler_call(Req, State, Handler, HandlerState, Message) - end. - --spec handler_call(cowboy_req:req(), #state{}, module(), any(), any()) -> ok. -handler_call(Req, State, Handler, HandlerState, Message) -> - try Handler:info(Message, Req, HandlerState) of - {ok, Req2, HandlerState2} -> - terminate_request(Req2, State, Handler, HandlerState2); - {loop, Req2, HandlerState2} -> - handler_before_loop(Req2, State, Handler, HandlerState2); - {loop, Req2, HandlerState2, hibernate} -> - handler_before_loop(Req2, State#state{hibernate=true}, - Handler, HandlerState2) - catch Class:Reason -> - error_logger:error_msg( - "** 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, info, 3, Class, Reason, HandlerState, - cowboy_req:to_list(Req), erlang:get_stacktrace()]), - handler_terminate(Req, Handler, HandlerState), - error_terminate(500, Req, State) - end. - --spec handler_terminate(cowboy_req:req(), module(), any()) -> ok. -handler_terminate(Req, Handler, HandlerState) -> - try - Handler:terminate(cowboy_req:lock(Req), HandlerState) - catch Class:Reason -> - error_logger:error_msg( - "** 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, terminate, 2, Class, Reason, HandlerState, - cowboy_req:to_list(Req), erlang:get_stacktrace()]) - end. - --spec terminate_request(cowboy_req:req(), #state{}, module(), any()) -> ok. -terminate_request(Req, State, Handler, HandlerState) -> - HandlerRes = handler_terminate(Req, Handler, HandlerState), - next_request(Req, State, HandlerRes). - -spec next_request(cowboy_req:req(), #state{}, any()) -> ok. -next_request(Req, State=#state{req_keepalive=Keepalive}, HandlerRes) -> +next_request(Req, State=#state{req_keepalive=Keepalive, timeout=Timeout}, + HandlerRes) -> cowboy_req:ensure_response(Req, 204), - {BodyRes, [Buffer, Connection]} = case cowboy_req:skip_body(Req) of - {ok, Req2} -> {ok, cowboy_req:get([buffer, connection], Req2)}; - {error, _} -> {close, [<<>>, close]} - end, - %% Flush the resp_sent message before moving on. - receive {cowboy_req, resp_sent} -> ok after 0 -> ok end, - case {HandlerRes, BodyRes, Connection} of - {ok, ok, keepalive} -> - ?MODULE:parse_request(Buffer, State#state{ - req_keepalive=Keepalive + 1}, 0); - _Closed -> - terminate(State) + %% If we are going to close the connection, + %% we do not want to attempt to skip the body. + case cowboy_req:get(connection, Req) of + close -> + terminate(State); + _ -> + Buffer = case cowboy_req:skip_body(Req) of + {ok, Req2} -> cowboy_req:get(buffer, Req2); + _ -> close + end, + %% Flush the resp_sent message before moving on. + receive {cowboy_req, resp_sent} -> ok after 0 -> ok end, + if HandlerRes =:= ok, Buffer =/= close -> + ?MODULE:parse_request(Buffer, + State#state{req_keepalive=Keepalive + 1, + until=until(Timeout)}, 0); + true -> + terminate(State) + end end. %% Only send an error reply if there is no resp_sent message. @@ -644,13 +557,13 @@ error_terminate(Code, Req, State) -> %% Only send an error reply if there is no resp_sent message. -spec error_terminate(cowboy_http:status(), #state{}) -> ok. error_terminate(Code, State=#state{socket=Socket, transport=Transport, - onresponse=OnResponse}) -> + compress=Compress, onresponse=OnResponse}) -> receive {cowboy_req, resp_sent} -> ok after 0 -> _ = cowboy_req:reply(Code, cowboy_req:new(Socket, Transport, <<"GET">>, <<>>, <<>>, <<>>, {1, 1}, [], <<>>, undefined, - <<>>, false, OnResponse)), + <<>>, false, Compress, OnResponse)), ok end, terminate(State). diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl index 4a9e1a7..5cb7aa3 100644 --- a/src/cowboy_req.erl +++ b/src/cowboy_req.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]> +%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]> %% Copyright (c) 2011, Anthony Ramine <[email protected]> %% %% Permission to use, copy, modify, and/or distribute this software for any @@ -42,7 +42,7 @@ -module(cowboy_req). %% Request API. --export([new/13]). +-export([new/14]). -export([method/1]). -export([version/1]). -export([peer/1]). @@ -89,6 +89,7 @@ -export([set_resp_cookie/4]). -export([set_resp_header/3]). -export([set_resp_body/2]). +-export([set_resp_body_fun/2]). -export([set_resp_body_fun/3]). -export([has_resp_header/2]). -export([has_resp_body/1]). @@ -111,7 +112,6 @@ -export([compact/1]). -export([lock/1]). -export([to_list/1]). --export([transport/1]). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). @@ -123,7 +123,7 @@ -type cookie_opts() :: [cookie_option()]. -export_type([cookie_opts/0]). --type resp_body_fun() :: fun(() -> {sent, non_neg_integer()}). +-type resp_body_fun() :: fun((inet:socket(), module()) -> ok). -record(http_req, { %% Transport. @@ -137,14 +137,14 @@ version = {1, 1} :: cowboy_http:version(), peer = undefined :: undefined | {inet:ip_address(), inet:port_number()}, host = undefined :: undefined | binary(), - host_info = undefined :: undefined | cowboy_dispatcher:tokens(), + host_info = undefined :: undefined | cowboy_router:tokens(), port = undefined :: undefined | inet:port_number(), path = undefined :: binary(), - path_info = undefined :: undefined | cowboy_dispatcher:tokens(), + path_info = undefined :: undefined | cowboy_router:tokens(), qs = undefined :: binary(), qs_vals = undefined :: undefined | list({binary(), binary() | true}), fragment = undefined :: binary(), - bindings = undefined :: undefined | cowboy_dispatcher:bindings(), + bindings = undefined :: undefined | cowboy_router:bindings(), headers = [] :: cowboy_http:headers(), p_headers = [] :: [any()], %% @todo Improve those specs. cookies = undefined :: undefined | [{binary(), binary()}], @@ -156,12 +156,15 @@ buffer = <<>> :: binary(), %% Response. + resp_compress = false :: boolean(), resp_state = waiting :: locked | waiting | chunks | done, resp_headers = [] :: cowboy_http:headers(), - resp_body = <<>> :: iodata() | {non_neg_integer(), resp_body_fun()}, + resp_body = <<>> :: iodata() | resp_body_fun() + | {non_neg_integer(), resp_body_fun()}, %% Functions. - onresponse = undefined :: undefined | cowboy_protocol:onresponse_fun() + onresponse = undefined :: undefined | already_called + | cowboy_protocol:onresponse_fun() }). -opaque req() :: #http_req{}. @@ -178,16 +181,16 @@ %% in an optimized way and add the parsed value to p_headers' cache. -spec new(inet:socket(), module(), binary(), binary(), binary(), binary(), cowboy_http:version(), cowboy_http:headers(), binary(), - inet:port_number() | undefined, binary(), boolean(), + inet:port_number() | undefined, binary(), boolean(), boolean(), undefined | cowboy_protocol:onresponse_fun()) -> req(). new(Socket, Transport, Method, Path, Query, Fragment, Version, Headers, Host, Port, Buffer, CanKeepalive, - OnResponse) -> + Compress, OnResponse) -> Req = #http_req{socket=Socket, transport=Transport, pid=self(), method=Method, path=Path, qs=Query, fragment=Fragment, version=Version, headers=Headers, host=Host, port=Port, buffer=Buffer, - onresponse=OnResponse}, + resp_compress=Compress, onresponse=OnResponse}, case CanKeepalive and (Version =:= {1, 1}) of false -> Req#http_req{connection=close}; @@ -253,7 +256,7 @@ host(Req) -> %% @doc Return the extra host information obtained from partially matching %% the hostname using <em>'...'</em>. -spec host_info(Req) - -> {cowboy_dispatcher:tokens() | undefined, Req} when Req::req(). + -> {cowboy_router:tokens() | undefined, Req} when Req::req(). host_info(Req) -> {Req#http_req.host_info, Req}. @@ -270,7 +273,7 @@ path(Req) -> %% @doc Return the extra path information obtained from partially matching %% the patch using <em>'...'</em>. -spec path_info(Req) - -> {cowboy_dispatcher:tokens() | undefined, Req} when Req::req(). + -> {cowboy_router:tokens() | undefined, Req} when Req::req(). path_info(Req) -> {Req#http_req.path_info, Req}. @@ -438,6 +441,11 @@ parse_header(Name, Req, Default) when Name =:= <<"accept-language">> -> fun (Value) -> cowboy_http:nonempty_list(Value, fun cowboy_http:language_range/2) end); +parse_header(Name, Req, Default) when Name =:= <<"authorization">> -> + parse_header(Name, Req, Default, + fun (Value) -> + cowboy_http:token_ci(Value, fun cowboy_http:authorization/2) + end); 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">> -> @@ -456,6 +464,11 @@ parse_header(Name, Req, Default) when Name =:= <<"if-modified-since">>; Name =:= <<"if-unmodified-since">> -> parse_header(Name, Req, Default, fun cowboy_http:http_date/1); +parse_header(Name, Req, Default) when Name =:= <<"sec-websocket-protocol">> -> + parse_header(Name, Req, Default, + fun (Value) -> + cowboy_http:nonempty_list(Value, fun cowboy_http:token/2) + end); %% @todo Extension parameters. parse_header(Name, Req, Default) when Name =:= <<"transfer-encoding">> -> parse_header(Name, Req, Default, @@ -548,11 +561,10 @@ set_meta(Name, Value, Req=#http_req{meta=Meta}) -> %% Request Body API. %% @doc Return whether the request message has a body. --spec has_body(Req) -> {boolean(), Req} when Req::req(). +-spec has_body(cowboy_req:req()) -> boolean(). has_body(Req) -> - Has = lists:keymember(<<"content-length">>, 1, Req#http_req.headers) orelse - lists:keymember(<<"transfer-encoding">>, 1, Req#http_req.headers), - {Has, Req}. + lists:keymember(<<"content-length">>, 1, Req#http_req.headers) orelse + lists:keymember(<<"transfer-encoding">>, 1, Req#http_req.headers). %% @doc Return the request message body length, if known. %% @@ -632,17 +644,18 @@ stream_body(Req=#http_req{buffer=Buffer, body_state={stream, _, _, _}}) when Buffer =/= <<>> -> transfer_decode(Buffer, Req#http_req{buffer= <<>>}); stream_body(Req=#http_req{body_state={stream, _, _, _}}) -> - stream_body_recv(Req); + stream_body_recv(0, Req); stream_body(Req=#http_req{body_state=done}) -> {done, Req}. --spec stream_body_recv(Req) +-spec stream_body_recv(non_neg_integer(), Req) -> {ok, binary(), Req} | {error, atom()} when Req::req(). -stream_body_recv(Req=#http_req{ +stream_body_recv(Length, Req=#http_req{ transport=Transport, socket=Socket, buffer=Buffer}) -> %% @todo Allow configuring the timeout. - case Transport:recv(Socket, 0, 5000) of - {ok, Data} -> transfer_decode(<< Buffer/binary, Data/binary >>, Req); + case Transport:recv(Socket, Length, 5000) of + {ok, Data} -> transfer_decode(<< Buffer/binary, Data/binary >>, + Req#http_req{buffer= <<>>}); {error, Reason} -> {error, Reason} end. @@ -660,7 +673,10 @@ transfer_decode(Data, Req=#http_req{ {stream, TransferDecode, TransferState2, ContentDecode}}); %% @todo {header(s) for chunked more -> - stream_body_recv(Req#http_req{buffer=Data}); + stream_body_recv(0, Req#http_req{buffer=Data}); + {more, Length, Rest, TransferState2} -> + stream_body_recv(Length, Req#http_req{buffer=Rest, body_state= + {stream, TransferDecode, TransferState2, ContentDecode}}); {done, Length, Rest} -> Req2 = transfer_decode_done(Length, Rest, Req), {done, Req2}; @@ -721,7 +737,6 @@ skip_body(Req) -> %% @doc Return the full body sent with the request, parsed as an %% application/x-www-form-urlencoded string. Essentially a POST query string. -%% @todo We need an option to limit the size of the body for QS too. -spec body_qs(Req) -> {ok, [{binary(), binary() | true}], Req} | {error, atom()} when Req::req(). @@ -758,7 +773,6 @@ multipart_data(Req=#http_req{multipart={Length, Cont}}) -> multipart_data(Req=#http_req{body_state=done}) -> {eof, Req}. -%% @todo Typespecs. multipart_data(Req, Length, {headers, Headers, Cont}) -> {headers, Headers, Req#http_req{multipart={Length, Cont}}}; multipart_data(Req, Length, {body, Data, Cont}) -> @@ -822,20 +836,33 @@ set_resp_header(Name, Value, Req=#http_req{resp_headers=RespHeaders}) -> set_resp_body(Body, Req) -> Req#http_req{resp_body=Body}. +%% @doc Add a body stream function to the response. +%% +%% The body set here is ignored if the response is later sent using +%% anything other than reply/2 or reply/3. +%% +%% Setting a response stream function without a length means that the +%% body will be sent until the connection is closed. Cowboy will make +%% sure that the connection is closed with no extra step required. +%% +%% To inform the client that a body has been sent with this request, +%% Cowboy will add a "Transfer-Encoding: identity" header to the +%% response. +-spec set_resp_body_fun(resp_body_fun(), Req) -> Req when Req::req(). +set_resp_body_fun(StreamFun, Req) -> + Req#http_req{resp_body=StreamFun}. + %% @doc Add a body function to the response. %% -%% The response body may also be set to a content-length - stream-function pair. -%% If the response body is of this type normal response headers will be sent. -%% After the response headers has been sent the body function is applied. -%% The body function is expected to write the response body directly to the -%% socket using the transport module. +%% The body set here is ignored if the response is later sent using +%% anything other than reply/2 or reply/3. %% -%% If the body function crashes while writing the response body or writes fewer -%% bytes than declared the behaviour is undefined. The body set here is ignored -%% if the response is later sent using anything other than `reply/2' or -%% `reply/3'. +%% Cowboy will call the given response stream function after sending the +%% headers. This function must send the specified number of bytes to the +%% socket it will receive as argument. %% -%% @see cowboy_req:transport/1. +%% If the body function crashes while writing the response body or writes +%% fewer bytes than declared the behaviour is undefined. -spec set_resp_body_fun(non_neg_integer(), resp_body_fun(), Req) -> Req when Req::req(). set_resp_body_fun(StreamLen, StreamFun, Req) -> @@ -848,6 +875,8 @@ has_resp_header(Name, #http_req{resp_headers=RespHeaders}) -> %% @doc Return whether a body has been set for the response. -spec has_resp_body(req()) -> boolean(). +has_resp_body(#http_req{resp_body=RespBody}) when is_function(RespBody) -> + true; has_resp_body(#http_req{resp_body={Length, _}}) -> Length > 0; has_resp_body(#http_req{resp_body=RespBody}) -> @@ -876,35 +905,93 @@ reply(Status, Headers, Req=#http_req{resp_body=Body}) -> iodata() | {non_neg_integer() | resp_body_fun()}, Req) -> {ok, Req} when Req::req(). reply(Status, Headers, Body, Req=#http_req{ + socket=Socket, transport=Transport, version=Version, connection=Connection, - method=Method, resp_state=waiting, resp_headers=RespHeaders}) -> + method=Method, resp_compress=Compress, + resp_state=waiting, resp_headers=RespHeaders}) -> RespConn = response_connection(Headers, Connection), HTTP11Headers = case Version of {1, 1} -> [{<<"connection">>, atom_to_connection(Connection)}]; _ -> [] end, case Body of + BodyFun when is_function(BodyFun) -> + %% We stream the response body until we close the connection. + {RespType, Req2} = response(Status, Headers, RespHeaders, [ + {<<"connection">>, <<"close">>}, + {<<"date">>, cowboy_clock:rfc1123()}, + {<<"server">>, <<"Cowboy">>}, + {<<"transfer-encoding">>, <<"identity">>} + ], <<>>, Req#http_req{connection=close}), + if RespType =/= hook, Method =/= <<"HEAD">> -> + BodyFun(Socket, Transport); + true -> ok + end; {ContentLength, BodyFun} -> + %% We stream the response body for ContentLength bytes. {RespType, Req2} = response(Status, Headers, RespHeaders, [ {<<"content-length">>, integer_to_list(ContentLength)}, {<<"date">>, cowboy_clock:rfc1123()}, {<<"server">>, <<"Cowboy">>} |HTTP11Headers], <<>>, Req), - if RespType =/= hook, Method =/= <<"HEAD">> -> BodyFun(); + if RespType =/= hook, Method =/= <<"HEAD">> -> + BodyFun(Socket, Transport); true -> ok end; + _ when Compress -> + Req2 = reply_may_compress(Status, Headers, Body, Req, + RespHeaders, HTTP11Headers, Method); _ -> - {_, Req2} = response(Status, Headers, RespHeaders, [ - {<<"content-length">>, integer_to_list(iolist_size(Body))}, - {<<"date">>, cowboy_clock:rfc1123()}, - {<<"server">>, <<"Cowboy">>} - |HTTP11Headers], - case Method of <<"HEAD">> -> <<>>; _ -> Body end, - Req) + Req2 = reply_no_compress(Status, Headers, Body, Req, + RespHeaders, HTTP11Headers, Method, iolist_size(Body)) end, {ok, Req2#http_req{connection=RespConn, resp_state=done, resp_headers=[], resp_body= <<>>}}. +reply_may_compress(Status, Headers, Body, Req, + RespHeaders, HTTP11Headers, Method) -> + BodySize = iolist_size(Body), + {ok, Encodings, Req2} + = cowboy_req:parse_header(<<"accept-encoding">>, Req), + CanGzip = (BodySize > 300) + andalso (false =:= lists:keyfind(<<"content-encoding">>, + 1, Headers)) + andalso (false =:= lists:keyfind(<<"content-encoding">>, + 1, RespHeaders)) + andalso (false =:= lists:keyfind(<<"transfer-encoding">>, + 1, Headers)) + andalso (false =:= lists:keyfind(<<"transfer-encoding">>, + 1, RespHeaders)) + andalso (Encodings =/= undefined) + andalso (false =/= lists:keyfind(<<"gzip">>, 1, Encodings)), + case CanGzip of + true -> + GzBody = zlib:gzip(Body), + {_, Req3} = response(Status, Headers, RespHeaders, [ + {<<"content-length">>, integer_to_list(byte_size(GzBody))}, + {<<"content-encoding">>, <<"gzip">>}, + {<<"date">>, cowboy_clock:rfc1123()}, + {<<"server">>, <<"Cowboy">>} + |HTTP11Headers], + case Method of <<"HEAD">> -> <<>>; _ -> GzBody end, + Req2), + Req3; + false -> + reply_no_compress(Status, Headers, Body, Req, + RespHeaders, HTTP11Headers, Method, BodySize) + end. + +reply_no_compress(Status, Headers, Body, Req, + RespHeaders, HTTP11Headers, Method, BodySize) -> + {_, Req2} = response(Status, Headers, RespHeaders, [ + {<<"content-length">>, integer_to_list(BodySize)}, + {<<"date">>, cowboy_clock:rfc1123()}, + {<<"server">>, <<"Cowboy">>} + |HTTP11Headers], + case Method of <<"HEAD">> -> <<>>; _ -> Body end, + Req), + Req2. + %% @equiv chunked_reply(Status, [], Req) -spec chunked_reply(cowboy_http:status(), Req) -> {ok, Req} when Req::req(). chunked_reply(Status, Req) -> @@ -1044,8 +1131,8 @@ set([{transport, Val}|Tail], Req) -> set(Tail, Req#http_req{transport=Val}); set([{version, Val}|Tail], Req) -> set(Tail, Req#http_req{version=Val}). %% @private --spec set_bindings(cowboy_dispatcher:tokens(), cowboy_dispatcher:tokens(), - cowboy_dispatcher:bindings(), Req) -> Req when Req::req(). +-spec set_bindings(cowboy_router:tokens(), cowboy_router:tokens(), + cowboy_router:bindings(), Req) -> Req when Req::req(). set_bindings(HostInfo, PathInfo, Bindings, Req) -> Req#http_req{host_info=HostInfo, path_info=PathInfo, bindings=Bindings}. @@ -1077,18 +1164,6 @@ lock(Req) -> to_list(Req) -> lists:zip(record_info(fields, http_req), tl(tuple_to_list(Req))). -%% @doc Return the transport module and socket associated with a request. -%% -%% This exposes the same socket interface used internally by the HTTP protocol -%% implementation to developers that needs low level access to the socket. -%% -%% It is preferred to use this in conjuction with the stream function support -%% in `set_resp_body_fun/3' if this is used to write a response body directly -%% to the socket. This ensures that the response headers are set correctly. --spec transport(req()) -> {ok, module(), inet:socket()}. -transport(#http_req{transport=Transport, socket=Socket}) -> - {ok, Transport, Socket}. - %% Internal. -spec response(cowboy_http:status(), cowboy_http:headers(), @@ -1097,13 +1172,17 @@ transport(#http_req{transport=Transport, socket=Socket}) -> response(Status, Headers, RespHeaders, DefaultHeaders, Body, Req=#http_req{ socket=Socket, transport=Transport, version=Version, pid=ReqPid, onresponse=OnResponse}) -> - FullHeaders = response_merge_headers(Headers, RespHeaders, DefaultHeaders), + FullHeaders = case OnResponse of + already_called -> Headers; + _ -> response_merge_headers(Headers, RespHeaders, DefaultHeaders) + end, Req2 = case OnResponse of + already_called -> Req; undefined -> Req; OnResponse -> OnResponse(Status, FullHeaders, Body, %% Don't call 'onresponse' from the hook itself. Req#http_req{resp_headers=[], resp_body= <<>>, - onresponse=undefined}) + onresponse=already_called}) end, ReplyType = case Req2#http_req.resp_state of waiting -> diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl index 06655a4..a49d622 100644 --- a/src/cowboy_rest.erl +++ b/src/cowboy_rest.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]> +%% Copyright (c) 2011-2013, Loïc Hoguin <[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 @@ -23,6 +23,7 @@ -export([upgrade/4]). -record(state, { + env :: cowboy_middleware:env(), method = undefined :: binary(), %% Handler. @@ -54,31 +55,31 @@ %% You do not need to call this function manually. To upgrade to the REST %% protocol, you simply need to return <em>{upgrade, protocol, {@module}}</em> %% in your <em>cowboy_http_handler:init/3</em> handler function. --spec upgrade(pid(), module(), any(), Req) - -> {ok, Req} | close when Req::cowboy_req:req(). -upgrade(_ListenerPid, Handler, Opts, Req) -> +-spec upgrade(Req, Env, module(), any()) + -> {ok, Req, Env} | {error, 500, Req} + when Req::cowboy_req:req(), Env::cowboy_middleware:env(). +upgrade(Req, Env, Handler, HandlerOpts) -> try Method = cowboy_req:get(method, Req), case erlang:function_exported(Handler, rest_init, 2) of true -> - case Handler:rest_init(Req, Opts) of + case Handler:rest_init(Req, HandlerOpts) of {ok, Req2, HandlerState} -> - service_available(Req2, #state{method=Method, + service_available(Req2, #state{env=Env, method=Method, handler=Handler, handler_state=HandlerState}) end; false -> - service_available(Req, #state{method=Method, + service_available(Req, #state{env=Env, method=Method, handler=Handler}) end catch Class:Reason -> - PLReq = cowboy_req:to_list(Req), error_logger:error_msg( "** 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, rest_init, 2, Class, Reason, Opts, PLReq, erlang:get_stacktrace()]), - {ok, _Req2} = cowboy_req:reply(500, Req), - close + [Handler, rest_init, 2, Class, Reason, HandlerOpts, + cowboy_req:to_list(Req), erlang:get_stacktrace()]), + {error, 500, Req} end. service_available(Req, State) -> @@ -90,7 +91,8 @@ known_methods(Req, State=#state{method=Method}) -> no_call when Method =:= <<"HEAD">>; Method =:= <<"GET">>; Method =:= <<"POST">>; Method =:= <<"PUT">>; Method =:= <<"DELETE">>; Method =:= <<"TRACE">>; - Method =:= <<"CONNECT">>; Method =:= <<"OPTIONS">> -> + Method =:= <<"CONNECT">>; Method =:= <<"OPTIONS">>; + Method =:= <<"PATCH">> -> next(Req, State, fun uri_too_long/2); no_call -> next(Req, State, 501); @@ -643,6 +645,8 @@ method(Req, State=#state{method= <<"POST">>}) -> post_is_create(Req, State); method(Req, State=#state{method= <<"PUT">>}) -> is_conflict(Req, State); +method(Req, State=#state{method= <<"PATCH">>}) -> + patch_resource(Req, State); method(Req, State=#state{method=Method}) when Method =:= <<"GET">>; Method =:= <<"HEAD">> -> set_resp_body(Req, State); @@ -666,6 +670,8 @@ post_is_create(Req, State) -> %% (including the leading /). create_path(Req, State) -> case call(Req, State, create_path) of + no_call -> + put_resource(Req, State, fun created_path/2); {halt, Req2, HandlerState} -> terminate(Req2, State#state{handler_state=HandlerState}); {Path, Req2, HandlerState} -> @@ -677,6 +683,23 @@ create_path(Req, State) -> State2, 303) end. +%% Called after content_types_accepted is called for POST methods +%% when create_path did not exist. Expects the full path to +%% be returned and MUST exist in the case that create_path +%% does not. +created_path(Req, State) -> + case call(Req, State, created_path) of + {halt, Req2, HandlerState} -> + terminate(Req2, State#state{handler_state=HandlerState}); + {Path, Req2, HandlerState} -> + {HostURL, Req3} = cowboy_req:host_url(Req2), + State2 = State#state{handler_state=HandlerState}, + Req4 = cowboy_req:set_resp_header( + <<"Location">>, << HostURL/binary, Path/binary >>, Req3), + respond(cowboy_req:set_meta(put_path, Path, Req4), + State2, 303) + end. + %% process_post should return true when the POST body could be processed %% and false when it hasn't, in which case a 500 error is sent. process_post(Req, State) -> @@ -707,6 +730,9 @@ put_resource(Req, State) -> %% may be different from the request path, and is stored as request metadata. %% It is always defined past this point. It can be retrieved as demonstrated: %% {PutPath, Req2} = cowboy_req:meta(put_path, Req) +%% +%%content_types_accepted SHOULD return a different list +%% for each HTTP method. put_resource(Req, State, OnTrue) -> case call(Req, State, content_types_accepted) of no_call -> @@ -721,6 +747,27 @@ put_resource(Req, State, OnTrue) -> choose_content_type(Req3, State2, OnTrue, ContentType, CTA2) end. +%% content_types_accepted should return a list of media types and their +%% associated callback functions in the same format as content_types_provided. +%% +%% The callback will then be called and is expected to process the content +%% pushed to the resource in the request body. +%% +%% content_types_accepted SHOULD return a different list +%% for each HTTP method. +patch_resource(Req, State) -> + case call(Req, State, content_types_accepted) of + no_call -> + respond(Req, State, 415); + {halt, Req2, HandlerState} -> + terminate(Req2, State#state{handler_state=HandlerState}); + {CTM, Req2, HandlerState} -> + State2 = State#state{handler_state=HandlerState}, + {ok, ContentType, Req3} + = cowboy_req:parse_header(<<"content-type">>, Req2), + choose_content_type(Req3, State2, 204, ContentType, CTM) + end. + %% The special content type '*' will always match. It can be used as a %% catch-all content type for accepting any kind of request content. %% Note that because it will always match, it should be the last of the @@ -738,15 +785,14 @@ choose_content_type(Req, "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} -> - State2 = State#state{handler_state=HandlerState}, + {error, 500, Req}; + {halt, Req2, HandlerState2} -> + terminate(Req2, State#state{handler_state=HandlerState2}); + {true, Req2, HandlerState2} -> + State2 = State#state{handler_state=HandlerState2}, next(Req2, State2, OnTrue); - {false, Req2, HandlerState} -> - State2 = State#state{handler_state=HandlerState}, + {false, Req2, HandlerState2} -> + State2 = State#state{handler_state=HandlerState2}, respond(Req2, State2, 422) end; choose_content_type(Req, State, OnTrue, ContentType, [_Any|Tail]) -> @@ -790,15 +836,16 @@ set_resp_body(Req, State=#state{handler=Handler, handler_state=HandlerState, "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} -> - State5 = State4#state{handler_state=HandlerState}, + {error, 500, Req5}; + {halt, Req6, HandlerState2} -> + terminate(Req6, State4#state{handler_state=HandlerState2}); + {Body, Req6, HandlerState2} -> + State5 = State4#state{handler_state=HandlerState2}, Req7 = case Body of - {stream, Len, Fun1} -> - cowboy_req:set_resp_body_fun(Len, Fun1, Req6); + {stream, StreamFun} -> + cowboy_req:set_resp_body_fun(StreamFun, Req6); + {stream, Len, StreamFun} -> + cowboy_req:set_resp_body_fun(Len, StreamFun, Req6); _Contents -> cowboy_req:set_resp_body(Body, Req6) end, @@ -845,12 +892,6 @@ generate_etag(Req, State=#state{etag=undefined}) -> case call(Req, State, generate_etag) of no_call -> {undefined, Req, State#state{etag=no_call}}; - %% Previously the return value from the generate_etag/2 callback was set - %% as the value of the ETag header in the response. Therefore the only - %% valid return type was `binary()'. If a handler returns a `binary()' - %% it must be mapped to the expected type or it'll always fail to - %% compare equal to any entity tags present in the request headers. - %% @todo Remove support for binary return values after 0.6. {Etag, Req2, HandlerState} when is_binary(Etag) -> [Etag2] = cowboy_http:entity_tag_match(Etag), {Etag2, Req2, State#state{handler_state=HandlerState, etag=Etag2}}; @@ -915,10 +956,11 @@ respond(Req, State, StatusCode) -> {ok, Req2} = cowboy_req:reply(StatusCode, Req), terminate(Req2, State). -terminate(Req, #state{handler=Handler, handler_state=HandlerState}) -> +terminate(Req, #state{env=Env, handler=Handler, + handler_state=HandlerState}) -> case erlang:function_exported(Handler, rest_terminate, 2) of true -> ok = Handler:rest_terminate( cowboy_req:lock(Req), HandlerState); false -> ok end, - {ok, Req}. + {ok, Req, [{result, ok}|Env]}. diff --git a/src/cowboy_router.erl b/src/cowboy_router.erl new file mode 100644 index 0000000..a4597ed --- /dev/null +++ b/src/cowboy_router.erl @@ -0,0 +1,565 @@ +%% Copyright (c) 2011-2013, Loïc Hoguin <[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 Routing middleware. +%% +%% Resolve the handler to be used for the request based on the +%% routing information found in the <em>dispatch</em> environment value. +%% When found, the handler module and associated data are added to +%% the environment as the <em>handler</em> and <em>handler_opts</em> values +%% respectively. +%% +%% If the route cannot be found, processing stops with either +%% a 400 or a 404 reply. +-module(cowboy_router). +-behaviour(cowboy_middleware). + +-export([compile/1]). +-export([execute/2]). + +-type bindings() :: [{atom(), binary()}]. +-type tokens() :: [binary()]. +-export_type([bindings/0]). +-export_type([tokens/0]). + +-type constraints() :: [{atom(), int} + | {atom(), function, fun ((binary()) -> true | {true, any()} | false)}]. +-export_type([constraints/0]). + +-type route_match() :: binary() | string(). +-type route_path() :: {Path::route_match(), Handler::module(), Opts::any()} + | {Path::route_match(), constraints(), Handler::module(), Opts::any()}. +-type route_rule() :: {Host::route_match(), Paths::[route_path()]} + | {Host::route_match(), constraints(), Paths::[route_path()]}. +-opaque routes() :: [route_rule()]. +-export_type([routes/0]). + +-type dispatch_match() :: '_' | <<_:8>> | [binary() | '_' | '...' | atom()]. +-type dispatch_path() :: {dispatch_match(), module(), any()}. +-type dispatch_rule() :: {Host::dispatch_match(), Paths::[dispatch_path()]}. +-opaque dispatch_rules() :: [dispatch_rule()]. +-export_type([dispatch_rules/0]). + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). +-endif. + +%% @doc Compile a list of routes into the dispatch format used +%% by Cowboy's routing. +-spec compile(routes()) -> dispatch_rules(). +compile(Routes) -> + compile(Routes, []). + +compile([], Acc) -> + lists:reverse(Acc); +compile([{Host, Paths}|Tail], Acc) -> + compile([{Host, [], Paths}|Tail], Acc); +compile([{HostMatch, Constraints, Paths}|Tail], Acc) -> + HostRules = case HostMatch of + '_' -> '_'; + _ -> compile_host(HostMatch) + end, + PathRules = compile_paths(Paths, []), + Hosts = case HostRules of + '_' -> [{'_', Constraints, PathRules}]; + _ -> [{R, Constraints, PathRules} || R <- HostRules] + end, + compile(Tail, Hosts ++ Acc). + +compile_host(HostMatch) when is_list(HostMatch) -> + compile_host(unicode:characters_to_binary(HostMatch)); +compile_host(HostMatch) when is_binary(HostMatch) -> + compile_rules(HostMatch, $., [], [], <<>>). + +compile_paths([], Acc) -> + lists:reverse(Acc); +compile_paths([{PathMatch, Handler, Opts}|Tail], Acc) -> + compile_paths([{PathMatch, [], Handler, Opts}|Tail], Acc); +compile_paths([{PathMatch, Constraints, Handler, Opts}|Tail], Acc) + when is_list(PathMatch) -> + compile_paths([{unicode:characters_to_binary(PathMatch), + Constraints, Handler, Opts}|Tail], Acc); +compile_paths([{'_', Constraints, Handler, Opts}|Tail], Acc) -> + compile_paths(Tail, [{'_', Constraints, Handler, Opts}] ++ Acc); +compile_paths([{<< $/, PathMatch/binary >>, Constraints, Handler, Opts}|Tail], + Acc) -> + PathRules = compile_rules(PathMatch, $/, [], [], <<>>), + Paths = [{lists:reverse(R), Constraints, Handler, Opts} || R <- PathRules], + compile_paths(Tail, Paths ++ Acc). + +compile_rules(<<>>, _, Segments, Rules, <<>>) -> + [Segments|Rules]; +compile_rules(<<>>, _, Segments, Rules, Acc) -> + [[Acc|Segments]|Rules]; +compile_rules(<< S, Rest/binary >>, S, Segments, Rules, <<>>) -> + compile_rules(Rest, S, Segments, Rules, <<>>); +compile_rules(<< S, Rest/binary >>, S, Segments, Rules, Acc) -> + compile_rules(Rest, S, [Acc|Segments], Rules, <<>>); +compile_rules(<< $:, Rest/binary >>, S, Segments, Rules, <<>>) -> + {NameBin, Rest2} = compile_binding(Rest, S, <<>>), + Name = binary_to_atom(NameBin, utf8), + compile_rules(Rest2, S, Segments, Rules, Name); +compile_rules(<< $:, _/binary >>, _, _, _, _) -> + erlang:error(badarg); +compile_rules(<< $[, $., $., $., $], Rest/binary >>, S, Segments, Rules, Acc) + when Acc =:= <<>> -> + compile_rules(Rest, S, ['...'|Segments], Rules, Acc); +compile_rules(<< $[, $., $., $., $], Rest/binary >>, S, Segments, Rules, Acc) -> + compile_rules(Rest, S, ['...', Acc|Segments], Rules, Acc); +compile_rules(<< $[, S, Rest/binary >>, S, Segments, Rules, Acc) -> + compile_brackets(Rest, S, [Acc|Segments], Rules); +compile_rules(<< $[, Rest/binary >>, S, Segments, Rules, <<>>) -> + compile_brackets(Rest, S, Segments, Rules); +%% Open bracket in the middle of a segment. +compile_rules(<< $[, _/binary >>, _, _, _, _) -> + erlang:error(badarg); +%% Missing an open bracket. +compile_rules(<< $], _/binary >>, _, _, _, _) -> + erlang:error(badarg); +compile_rules(<< C, Rest/binary >>, S, Segments, Rules, Acc) -> + compile_rules(Rest, S, Segments, Rules, << Acc/binary, C >>). + +%% Everything past $: until $. or $[ or $] or end of binary +%% is the binding name. +compile_binding(<<>>, _, <<>>) -> + erlang:error(badarg); +compile_binding(Rest = <<>>, _, Acc) -> + {Acc, Rest}; +compile_binding(Rest = << C, _/binary >>, S, Acc) + when C =:= S; C =:= $[; C =:= $] -> + {Acc, Rest}; +compile_binding(<< C, Rest/binary >>, S, Acc) -> + compile_binding(Rest, S, << Acc/binary, C >>). + +compile_brackets(Rest, S, Segments, Rules) -> + {Bracket, Rest2} = compile_brackets_split(Rest, <<>>, 0), + Rules1 = compile_rules(Rest2, S, Segments, [], <<>>), + Rules2 = compile_rules(<< Bracket/binary, Rest2/binary >>, + S, Segments, [], <<>>), + Rules ++ Rules2 ++ Rules1. + +%% Missing a close bracket. +compile_brackets_split(<<>>, _, _) -> + erlang:error(badarg); +%% Make sure we don't confuse the closing bracket we're looking for. +compile_brackets_split(<< C, Rest/binary >>, Acc, N) when C =:= $[ -> + compile_brackets_split(Rest, << Acc/binary, C >>, N + 1); +compile_brackets_split(<< C, Rest/binary >>, Acc, N) when C =:= $], N > 0 -> + compile_brackets_split(Rest, << Acc/binary, C >>, N - 1); +%% That's the right one. +compile_brackets_split(<< $], Rest/binary >>, Acc, 0) -> + {Acc, Rest}; +compile_brackets_split(<< C, Rest/binary >>, Acc, N) -> + compile_brackets_split(Rest, << Acc/binary, C >>, N). + +%% @private +-spec execute(Req, Env) + -> {ok, Req, Env} | {error, 400 | 404, Req} + when Req::cowboy_req:req(), Env::cowboy_middleware:env(). +execute(Req, Env) -> + {_, Dispatch} = lists:keyfind(dispatch, 1, Env), + [Host, Path] = cowboy_req:get([host, path], Req), + case match(Dispatch, Host, Path) of + {ok, Handler, HandlerOpts, Bindings, HostInfo, PathInfo} -> + Req2 = cowboy_req:set_bindings(HostInfo, PathInfo, Bindings, Req), + {ok, Req2, [{handler, Handler}, {handler_opts, HandlerOpts}|Env]}; + {error, notfound, host} -> + {error, 400, Req}; + {error, badrequest, path} -> + {error, 400, Req}; + {error, notfound, path} -> + {error, 404, Req} + end. + +%% Internal. + +%% @doc Match hostname tokens and path tokens against dispatch rules. +%% +%% It is typically used for matching tokens for the hostname and path of +%% the request against a global dispatch rule for your listener. +%% +%% Dispatch rules are a list of <em>{Hostname, PathRules}</em> tuples, with +%% <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, `<<"*">>', 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 +%% corresponding token value and return it. +%% +%% The list of hostname tokens is reversed before matching. For example, if +%% we were to match "www.ninenines.eu", we would first match "eu", then +%% "ninenines", then "www". This means that in the context of hostnames, +%% the <em>'...'</em> atom matches properly the lower levels of the domain +%% as would be expected. +%% +%% When a result is found, this function will return the handler module and +%% options found in the dispatch list, a key-value list of bindings and +%% the tokens that were matched by the <em>'...'</em> atom for both the +%% hostname and path. +-spec match(dispatch_rules(), Host::binary() | tokens(), Path::binary()) + -> {ok, module(), any(), bindings(), + HostInfo::undefined | tokens(), + PathInfo::undefined | tokens()} + | {error, notfound, host} | {error, notfound, path} + | {error, badrequest, path}. +match([], _, _) -> + {error, notfound, host}; +%% If the host is '_' then there can be no constraints. +match([{'_', [], PathMatchs}|_Tail], _, Path) -> + match_path(PathMatchs, undefined, Path, []); +match([{HostMatch, Constraints, PathMatchs}|Tail], Tokens, Path) + when is_list(Tokens) -> + case list_match(Tokens, HostMatch, []) of + false -> + match(Tail, Tokens, Path); + {true, Bindings, HostInfo} -> + HostInfo2 = case HostInfo of + undefined -> undefined; + _ -> lists:reverse(HostInfo) + end, + case check_constraints(Constraints, Bindings) of + {ok, Bindings2} -> + match_path(PathMatchs, HostInfo2, Path, Bindings2); + nomatch -> + match(Tail, Tokens, Path) + end + end; +match(Dispatch, Host, Path) -> + match(Dispatch, split_host(Host), Path). + +-spec match_path([dispatch_path()], + HostInfo::undefined | tokens(), binary() | tokens(), bindings()) + -> {ok, module(), any(), bindings(), + HostInfo::undefined | tokens(), + PathInfo::undefined | tokens()} + | {error, notfound, path} | {error, badrequest, path}. +match_path([], _, _, _) -> + {error, notfound, path}; +%% If the path is '_' then there can be no constraints. +match_path([{'_', [], Handler, Opts}|_Tail], HostInfo, _, Bindings) -> + {ok, Handler, Opts, Bindings, HostInfo, undefined}; +match_path([{<<"*">>, _Constraints, Handler, Opts}|_Tail], HostInfo, <<"*">>, Bindings) -> + {ok, Handler, Opts, Bindings, HostInfo, undefined}; +match_path([{PathMatch, Constraints, Handler, Opts}|Tail], HostInfo, Tokens, + Bindings) when is_list(Tokens) -> + case list_match(Tokens, PathMatch, Bindings) of + false -> + match_path(Tail, HostInfo, Tokens, Bindings); + {true, PathBinds, PathInfo} -> + case check_constraints(Constraints, PathBinds) of + {ok, PathBinds2} -> + {ok, Handler, Opts, PathBinds2, HostInfo, PathInfo}; + nomatch -> + match_path(Tail, HostInfo, Tokens, Bindings) + end + end; +match_path(_Dispatch, _HostInfo, badrequest, _Bindings) -> + {error, badrequest, path}; +match_path(Dispatch, HostInfo, Path, Bindings) -> + match_path(Dispatch, HostInfo, split_path(Path), Bindings). + +check_constraints([], Bindings) -> + {ok, Bindings}; +check_constraints([Constraint|Tail], Bindings) -> + Name = element(1, Constraint), + case lists:keyfind(Name, 1, Bindings) of + false -> + check_constraints(Tail, Bindings); + {_, Value} -> + case check_constraint(Constraint, Value) of + true -> + check_constraints(Tail, Bindings); + {true, Value2} -> + Bindings2 = lists:keyreplace(Name, 1, Bindings, + {Name, Value2}), + check_constraints(Tail, Bindings2); + false -> + nomatch + end + end. + +check_constraint({_, int}, Value) -> + try {true, list_to_integer(binary_to_list(Value))} + catch _:_ -> false + end; +check_constraint({_, function, Fun}, Value) -> + Fun(Value). + +%% @doc Split a hostname into a list of tokens. +-spec split_host(binary()) -> tokens(). +split_host(Host) -> + split_host(Host, []). + +split_host(Host, Acc) -> + case binary:match(Host, <<".">>) of + nomatch when Host =:= <<>> -> + Acc; + nomatch -> + [Host|Acc]; + {Pos, _} -> + << Segment:Pos/binary, _:8, Rest/bits >> = Host, + false = byte_size(Segment) == 0, + split_host(Rest, [Segment|Acc]) + end. + +%% @doc Split a path into a list of path segments. +%% +%% Following RFC2396, this function may return path segments containing any +%% character, including <em>/</em> if, and only if, a <em>/</em> was escaped +%% and part of a path segment. +-spec split_path(binary()) -> tokens(). +split_path(<< $/, Path/bits >>) -> + split_path(Path, []); +split_path(_) -> + badrequest. + +split_path(Path, 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(), dispatch_match(), bindings()) + -> {true, bindings(), undefined | tokens()} | false. +%% Atom '...' matches any trailing path, stop right now. +list_match(List, ['...'], Binds) -> + {true, Binds, List}; +%% Atom '_' matches anything, continue. +list_match([_E|Tail], ['_'|TailMatch], Binds) -> + list_match(Tail, TailMatch, Binds); +%% Both values match, continue. +list_match([E|Tail], [E|TailMatch], Binds) -> + list_match(Tail, TailMatch, Binds); +%% Bind E to the variable name V and continue, +%% unless V was already defined and E isn't identical to the previous value. +list_match([E|Tail], [V|TailMatch], Binds) when is_atom(V) -> + case lists:keyfind(V, 1, Binds) of + {_, E} -> + list_match(Tail, TailMatch, Binds); + {_, _} -> + false; + false -> + list_match(Tail, TailMatch, [{V, E}|Binds]) + end; +%% Match complete. +list_match([], [], Binds) -> + {true, Binds, undefined}; +%% Values don't match, stop. +list_match(_List, _Match, _Binds) -> + false. + +%% Tests. + +-ifdef(TEST). + +compile_test_() -> + %% {Routes, Result} + Tests = [ + %% Match any host and path. + {[{'_', [{'_', h, o}]}], + [{'_', [], [{'_', [], h, o}]}]}, + {[{"cowboy.example.org", + [{"/", ha, oa}, {"/path/to/resource", hb, ob}]}], + [{[<<"org">>, <<"example">>, <<"cowboy">>], [], [ + {[], [], ha, oa}, + {[<<"path">>, <<"to">>, <<"resource">>], [], hb, ob}]}]}, + {[{'_', [{"/path/to/resource/", h, o}]}], + [{'_', [], [{[<<"path">>, <<"to">>, <<"resource">>], [], h, o}]}]}, + {[{"cowboy.example.org.", [{'_', h, o}]}], + [{[<<"org">>, <<"example">>, <<"cowboy">>], [], [{'_', [], h, o}]}]}, + {[{".cowboy.example.org", [{'_', h, o}]}], + [{[<<"org">>, <<"example">>, <<"cowboy">>], [], [{'_', [], h, o}]}]}, + {[{":subdomain.example.org", [{"/hats/:name/prices", h, o}]}], + [{[<<"org">>, <<"example">>, subdomain], [], [ + {[<<"hats">>, name, <<"prices">>], [], h, o}]}]}, + {[{"ninenines.:_", [{"/hats/:_", h, o}]}], + [{['_', <<"ninenines">>], [], [{[<<"hats">>, '_'], [], h, o}]}]}, + {[{"[www.]ninenines.eu", + [{"/horses", h, o}, {"/hats/[page/:number]", h, o}]}], [ + {[<<"eu">>, <<"ninenines">>], [], [ + {[<<"horses">>], [], h, o}, + {[<<"hats">>], [], h, o}, + {[<<"hats">>, <<"page">>, number], [], h, o}]}, + {[<<"eu">>, <<"ninenines">>, <<"www">>], [], [ + {[<<"horses">>], [], h, o}, + {[<<"hats">>], [], h, o}, + {[<<"hats">>, <<"page">>, number], [], h, o}]}]}, + {[{'_', [{"/hats/[page/[:number]]", h, o}]}], [{'_', [], [ + {[<<"hats">>], [], h, o}, + {[<<"hats">>, <<"page">>], [], h, o}, + {[<<"hats">>, <<"page">>, number], [], h, o}]}]}, + {[{"[...]ninenines.eu", [{"/hats/[...]", h, o}]}], + [{[<<"eu">>, <<"ninenines">>, '...'], [], [ + {[<<"hats">>, '...'], [], h, o}]}]} + ], + [{lists:flatten(io_lib:format("~p", [Rt])), + fun() -> Rs = compile(Rt) end} || {Rt, Rs} <- Tests]. + +split_host_test_() -> + %% {Host, Result} + Tests = [ + {<<"">>, []}, + {<<"*">>, [<<"*">>]}, + {<<"cowboy.ninenines.eu">>, + [<<"eu">>, <<"ninenines">>, <<"cowboy">>]}, + {<<"ninenines.eu">>, + [<<"eu">>, <<"ninenines">>]}, + {<<"a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z">>, + [<<"z">>, <<"y">>, <<"x">>, <<"w">>, <<"v">>, <<"u">>, <<"t">>, + <<"s">>, <<"r">>, <<"q">>, <<"p">>, <<"o">>, <<"n">>, <<"m">>, + <<"l">>, <<"k">>, <<"j">>, <<"i">>, <<"h">>, <<"g">>, <<"f">>, + <<"e">>, <<"d">>, <<"c">>, <<"b">>, <<"a">>]} + ], + [{H, fun() -> R = split_host(H) end} || {H, R} <- Tests]. + +split_path_test_() -> + %% {Path, Result, QueryString} + Tests = [ + {<<"/">>, []}, + {<<"/extend//cowboy">>, [<<"extend">>, <<>>, <<"cowboy">>]}, + {<<"/users">>, [<<"users">>]}, + {<<"/users/42/friends">>, [<<"users">>, <<"42">>, <<"friends">>]}, + {<<"/users/a+b/c%21d">>, [<<"users">>, <<"a b">>, <<"c!d">>]} + ], + [{P, fun() -> R = split_path(P) end} || {P, R} <- Tests]. + +match_test_() -> + Dispatch = [ + {[<<"eu">>, <<"ninenines">>, '_', <<"www">>], [], [ + {[<<"users">>, '_', <<"mails">>], [], match_any_subdomain_users, []} + ]}, + {[<<"eu">>, <<"ninenines">>], [], [ + {[<<"users">>, id, <<"friends">>], [], match_extend_users_friends, []}, + {'_', [], match_extend, []} + ]}, + {[var, <<"ninenines">>], [], [ + {[<<"threads">>, var], [], match_duplicate_vars, + [we, {expect, two}, var, here]} + ]}, + {[ext, <<"erlang">>], [], [ + {'_', [], match_erlang_ext, []} + ]}, + {'_', [], [ + {[<<"users">>, id, <<"friends">>], [], match_users_friends, []}, + {'_', [], match_any, []} + ]} + ], + %% {Host, Path, Result} + Tests = [ + {<<"any">>, <<"/">>, {ok, match_any, [], []}}, + {<<"www.any.ninenines.eu">>, <<"/users/42/mails">>, + {ok, match_any_subdomain_users, [], []}}, + {<<"www.ninenines.eu">>, <<"/users/42/mails">>, + {ok, match_any, [], []}}, + {<<"www.ninenines.eu">>, <<"/">>, + {ok, match_any, [], []}}, + {<<"www.any.ninenines.eu">>, <<"/not_users/42/mails">>, + {error, notfound, path}}, + {<<"ninenines.eu">>, <<"/">>, + {ok, match_extend, [], []}}, + {<<"ninenines.eu">>, <<"/users/42/friends">>, + {ok, match_extend_users_friends, [], [{id, <<"42">>}]}}, + {<<"erlang.fr">>, '_', + {ok, match_erlang_ext, [], [{ext, <<"fr">>}]}}, + {<<"any">>, <<"/users/444/friends">>, + {ok, match_users_friends, [], [{id, <<"444">>}]}} + ], + [{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() -> + {ok, Handler, Opts, Binds, undefined, undefined} + = match(Dispatch, H, P) + end} || {H, P, {ok, Handler, Opts, Binds}} <- Tests]. + +match_info_test_() -> + Dispatch = [ + {[<<"eu">>, <<"ninenines">>, <<"www">>], [], [ + {[<<"pathinfo">>, <<"is">>, <<"next">>, '...'], [], match_path, []} + ]}, + {[<<"eu">>, <<"ninenines">>, '...'], [], [ + {'_', [], match_any, []} + ]} + ], + Tests = [ + {<<"ninenines.eu">>, <<"/">>, + {ok, match_any, [], [], [], undefined}}, + {<<"bugs.ninenines.eu">>, <<"/">>, + {ok, match_any, [], [], [<<"bugs">>], undefined}}, + {<<"cowboy.bugs.ninenines.eu">>, <<"/">>, + {ok, match_any, [], [], [<<"cowboy">>, <<"bugs">>], undefined}}, + {<<"www.ninenines.eu">>, <<"/pathinfo/is/next">>, + {ok, match_path, [], [], undefined, []}}, + {<<"www.ninenines.eu">>, <<"/pathinfo/is/next/path_info">>, + {ok, match_path, [], [], undefined, [<<"path_info">>]}}, + {<<"www.ninenines.eu">>, <<"/pathinfo/is/next/foo/bar">>, + {ok, match_path, [], [], undefined, [<<"foo">>, <<"bar">>]}} + ], + [{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() -> + R = match(Dispatch, H, P) + end} || {H, P, R} <- Tests]. + +match_constraints_test() -> + Dispatch = [{'_', [], + [{[<<"path">>, value], [{value, int}], match, []}]}], + {ok, _, [], [{value, 123}], _, _} = match(Dispatch, + <<"ninenines.eu">>, <<"/path/123">>), + {ok, _, [], [{value, 123}], _, _} = match(Dispatch, + <<"ninenines.eu">>, <<"/path/123/">>), + {error, notfound, path} = match(Dispatch, + <<"ninenines.eu">>, <<"/path/NaN/">>), + Dispatch2 = [{'_', [], + [{[<<"path">>, username], [{username, function, + fun(Value) -> Value =:= cowboy_bstr:to_lower(Value) end}], + match, []}]}], + {ok, _, [], [{username, <<"essen">>}], _, _} = match(Dispatch2, + <<"ninenines.eu">>, <<"/path/essen">>), + {error, notfound, path} = match(Dispatch2, + <<"ninenines.eu">>, <<"/path/ESSEN">>), + ok. + +match_same_bindings_test() -> + Dispatch = [{[same, same], [], [{'_', [], match, []}]}], + {ok, _, [], [{same, <<"eu">>}], _, _} = match(Dispatch, + <<"eu.eu">>, <<"/">>), + {error, notfound, host} = match(Dispatch, + <<"ninenines.eu">>, <<"/">>), + Dispatch2 = [{[<<"eu">>, <<"ninenines">>, user], [], + [{[<<"path">>, user], [], match, []}]}], + {ok, _, [], [{user, <<"essen">>}], _, _} = match(Dispatch2, + <<"essen.ninenines.eu">>, <<"/path/essen">>), + {ok, _, [], [{user, <<"essen">>}], _, _} = match(Dispatch2, + <<"essen.ninenines.eu">>, <<"/path/essen/">>), + {error, notfound, path} = match(Dispatch2, + <<"essen.ninenines.eu">>, <<"/path/notessen">>), + Dispatch3 = [{'_', [], [{[same, same], [], match, []}]}], + {ok, _, [], [{same, <<"path">>}], _, _} = match(Dispatch3, + <<"ninenines.eu">>, <<"/path/path">>), + {error, notfound, path} = match(Dispatch3, + <<"ninenines.eu">>, <<"/path/to">>), + ok. + +-endif. diff --git a/src/cowboy_static.erl b/src/cowboy_static.erl index 55d01c7..373ea52 100644 --- a/src/cowboy_static.erl +++ b/src/cowboy_static.erl @@ -289,7 +289,7 @@ forbidden(Req, #state{fileinfo={ok, #file_info{access=Access}}}=State) -> -spec last_modified(Req, #state{}) -> {calendar:datetime(), Req, #state{}} when Req::cowboy_req:req(). last_modified(Req, #state{fileinfo={ok, #file_info{mtime=Modified}}}=State) -> - {Modified, Req, State}. + {erlang:localtime_to_universaltime(Modified), Req, State}. %% @private Generate the ETag header value for this file. @@ -321,8 +321,14 @@ content_types_provided(Req, #state{filepath=Filepath, -spec file_contents(cowboy_req:req(), #state{}) -> tuple(). file_contents(Req, #state{filepath=Filepath, fileinfo={ok, #file_info{size=Filesize}}}=State) -> - {ok, Transport, Socket} = cowboy_req:transport(Req), - Writefile = fun() -> Transport:sendfile(Socket, Filepath) end, + Writefile = fun(Socket, Transport) -> + %% Transport:sendfile/2 may return {error, closed} + %% if the connection is closed while sending the file. + case Transport:sendfile(Socket, Filepath) of + {ok, _} -> ok; + {error, closed} -> ok + end + end, {{stream, Filesize, Writefile}, Req, State}. diff --git a/src/cowboy_sup.erl b/src/cowboy_sup.erl index 00fcc5e..0e4e59a 100644 --- a/src/cowboy_sup.erl +++ b/src/cowboy_sup.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]> +%% Copyright (c) 2011-2013, Loïc Hoguin <[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 diff --git a/src/cowboy_websocket.erl b/src/cowboy_websocket.erl index 8c02ac7..debb69f 100644 --- a/src/cowboy_websocket.erl +++ b/src/cowboy_websocket.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]> +%% Copyright (c) 2011-2013, Loïc Hoguin <[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 @@ -12,7 +12,10 @@ %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -%% @doc WebSocket protocol implementation. +%% @doc Websocket protocol implementation. +%% +%% Cowboy supports versions 7 through 17 of the Websocket drafts. +%% It also supports RFC6455, the proposed standard for Websocket. -module(cowboy_websocket). %% API. @@ -21,52 +24,52 @@ %% Internal. -export([handler_loop/4]). +-type close_code() :: 1000..4999. +-export_type([close_code/0]). + -type frame() :: close | ping | pong | {text | binary | close | ping | pong, binary()} - | {close, 1000..4999, binary()}. + | {close, close_code(), binary()}. -export_type([frame/0]). -type opcode() :: 0 | 1 | 2 | 8 | 9 | 10. -type mask_key() :: 0..16#ffffffff. - -%% The websocket_data/4 function may be called multiple times for a message. -%% The websocket_dispatch/4 function is only called once for each message. --type frag_state() :: - undefined | %% no fragmentation has been seen. - {nofin, opcode()} | %% first fragment has been seen. - {nofin, opcode(), binary()} | %% first fragment has been unmasked. - {fin, opcode(), binary()}. %% last fragment has been seen. +-type frag_state() :: undefined + | {nofin, opcode(), binary()} | {fin, opcode(), binary()}. -record(state, { + env :: cowboy_middleware:env(), socket = undefined :: inet:socket(), transport = undefined :: module(), - version :: 0 | 7 | 8 | 13, handler :: module(), - opts :: any(), - challenge = undefined :: undefined | binary() | {binary(), binary()}, + handler_opts :: any(), + key = undefined :: undefined | binary(), timeout = infinity :: timeout(), timeout_ref = undefined :: undefined | reference(), messages = undefined :: undefined | {atom(), atom(), atom()}, hibernate = false :: boolean(), - eop :: undefined | tuple(), %% hixie-76 specific. - origin = undefined :: undefined | binary(), %% hixie-76 specific. - frag_state = undefined :: frag_state() + frag_state = undefined :: frag_state(), + utf8_state = <<>> :: binary() }). -%% @doc Upgrade a HTTP request to the WebSocket protocol. +%% @doc Upgrade an HTTP request to the Websocket protocol. %% -%% You do not need to call this function manually. To upgrade to the WebSocket +%% You do not need to call this function manually. To upgrade to the Websocket %% protocol, you simply need to return <em>{upgrade, protocol, {@module}}</em> %% in your <em>cowboy_http_handler:init/3</em> handler function. --spec upgrade(pid(), module(), any(), cowboy_req:req()) -> closed. -upgrade(ListenerPid, Handler, Opts, Req) -> +-spec upgrade(Req, Env, module(), any()) + -> {ok, Req, Env} | {error, 400, Req} + | {suspend, module(), atom(), [any()]} + when Req::cowboy_req:req(), Env::cowboy_middleware:env(). +upgrade(Req, Env, Handler, HandlerOpts) -> + {_, ListenerPid} = lists:keyfind(listener, 1, Env), ranch_listener:remove_connection(ListenerPid), - {ok, Transport, Socket} = cowboy_req:transport(Req), - State = #state{socket=Socket, transport=Transport, - handler=Handler, opts=Opts}, + [Socket, Transport] = cowboy_req:get([socket, transport], Req), + State = #state{env=Env, socket=Socket, transport=Transport, + handler=Handler, handler_opts=HandlerOpts}, case catch websocket_upgrade(State, Req) of {ok, State2, Req2} -> handler_init(State2, Req2); - {'EXIT', _Reason} -> upgrade_error(Req) + {'EXIT', _Reason} -> upgrade_error(Req, Env) end. -spec websocket_upgrade(#state{}, Req) @@ -79,41 +82,21 @@ websocket_upgrade(State, Req) -> {ok, [<<"websocket">>], Req3} = cowboy_req:parse_header(<<"upgrade">>, Req2), {Version, Req4} = cowboy_req:header(<<"sec-websocket-version">>, Req3), - websocket_upgrade(Version, State, Req4). - -%% @todo Handle the Sec-Websocket-Protocol header. -%% @todo Reply a proper error, don't die, if a required header is undefined. --spec websocket_upgrade(undefined | <<_:8>>, #state{}, Req) - -> {ok, #state{}, Req} when Req::cowboy_req:req(). -%% No version given. Assuming hixie-76 draft. -%% -%% We need to wait to send a reply back before trying to read the -%% third part of the challenge key, because proxies will wait for -%% a reply before sending it. Therefore we calculate the challenge -%% key only in websocket_handshake/3. -websocket_upgrade(undefined, State, Req) -> - {Origin, Req2} = cowboy_req:header(<<"origin">>, Req), - {Key1, Req3} = cowboy_req:header(<<"sec-websocket-key1">>, Req2), - {Key2, Req4} = cowboy_req:header(<<"sec-websocket-key2">>, Req3), - false = lists:member(undefined, [Origin, Key1, Key2]), - EOP = binary:compile_pattern(<< 255 >>), - {ok, State#state{version=0, origin=Origin, challenge={Key1, Key2}, - eop=EOP}, cowboy_req:set_meta(websocket_version, 0, Req4)}; -%% Versions 7 and 8. Implementation follows the hybi 7 through 17 drafts. -websocket_upgrade(Version, State, Req) - when Version =:= <<"7">>; Version =:= <<"8">>; - Version =:= <<"13">> -> - {Key, Req2} = cowboy_req:header(<<"sec-websocket-key">>, Req), - false = Key =:= undefined, - Challenge = hybi_challenge(Key), IntVersion = list_to_integer(binary_to_list(Version)), - {ok, State#state{version=IntVersion, challenge=Challenge}, - cowboy_req:set_meta(websocket_version, IntVersion, Req2)}. - --spec handler_init(#state{}, cowboy_req:req()) -> closed. -handler_init(State=#state{transport=Transport, handler=Handler, opts=Opts}, - Req) -> - try Handler:websocket_init(Transport:name(), Req, Opts) of + true = (IntVersion =:= 7) orelse (IntVersion =:= 8) + orelse (IntVersion =:= 13), + {Key, Req5} = cowboy_req:header(<<"sec-websocket-key">>, Req4), + false = Key =:= undefined, + {ok, State#state{key=Key}, + cowboy_req:set_meta(websocket_version, IntVersion, Req5)}. + +-spec handler_init(#state{}, Req) + -> {ok, Req, cowboy_middleware:env()} | {error, 400, Req} + | {suspend, module(), atom(), [any()]} + when Req::cowboy_req:req(). +handler_init(State=#state{env=Env, transport=Transport, + handler=Handler, handler_opts=HandlerOpts}, Req) -> + try Handler:websocket_init(Transport:name(), Req, HandlerOpts) of {ok, Req2, HandlerState} -> websocket_handshake(State, Req2, HandlerState); {ok, Req2, HandlerState, hibernate} -> @@ -127,60 +110,36 @@ handler_init(State=#state{transport=Transport, handler=Handler, opts=Opts}, hibernate=true}, Req2, HandlerState); {shutdown, Req2} -> cowboy_req:ensure_response(Req2, 400), - closed + {ok, Req2, [{result, closed}|Env]} catch Class:Reason -> - upgrade_error(Req), error_logger:error_msg( "** 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, websocket_init, 3, Class, Reason, Opts, - cowboy_req:to_list(Req),erlang:get_stacktrace()]) + [Handler, websocket_init, 3, Class, Reason, HandlerOpts, + cowboy_req:to_list(Req),erlang:get_stacktrace()]), + upgrade_error(Req, Env) end. --spec upgrade_error(cowboy_req:req()) -> closed. -upgrade_error(Req) -> +%% Only send an error reply if there is no resp_sent message. +-spec upgrade_error(Req, Env) -> {ok, Req, Env} | {error, 400, Req} + when Req::cowboy_req:req(), Env::cowboy_middleware:env(). +upgrade_error(Req, Env) -> receive - {cowboy_req, resp_sent} -> closed + {cowboy_req, resp_sent} -> + {ok, Req, [{result, closed}|Env]} after 0 -> - _ = cowboy_req:reply(400, [], [], Req), - closed + {error, 400, Req} end. --spec websocket_handshake(#state{}, cowboy_req:req(), any()) -> closed. -websocket_handshake(State=#state{socket=Socket, transport=Transport, - version=0, origin=Origin, challenge={Key1, Key2}}, - Req, HandlerState) -> - {<< "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}], - Req1), - %% Flush the resp_sent message before moving on. - receive {cowboy_req, resp_sent} -> ok after 0 -> ok end, - %% We replied with a proper response. Proxies should be happy enough, - %% we can now read the 8 last bytes of the challenge keys and send - %% the challenge response directly to the socket. - %% - %% We use a trick here to read exactly 8 bytes of the body regardless - %% of what's in the buffer. - {ok, Req3} = cowboy_req:init_stream( - fun cowboy_http:te_identity/2, {0, 8}, - fun cowboy_http:ce_identity/1, Req2), - case cowboy_req:body(Req3) of - {ok, Key3, Req4} -> - Challenge = hixie76_challenge(Key1, Key2, Key3), - Transport:send(Socket, Challenge), - handler_before_loop(State#state{messages=Transport:messages()}, - Req4, HandlerState, <<>>); - _Any -> - %% 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}, +-spec websocket_handshake(#state{}, Req, any()) + -> {ok, Req, cowboy_middleware:env()} + | {suspend, module(), atom(), [any()]} + when Req::cowboy_req:req(). +websocket_handshake(State=#state{transport=Transport, key=Key}, Req, HandlerState) -> + Challenge = base64:encode(crypto:sha( + << Key/binary, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" >>)), {ok, Req2} = cowboy_req:upgrade_reply( 101, [{<<"upgrade">>, <<"websocket">>}, @@ -189,17 +148,19 @@ websocket_handshake(State=#state{transport=Transport, challenge=Challenge}, %% Flush the resp_sent message before moving on. receive {cowboy_req, resp_sent} -> ok after 0 -> ok end, State2 = handler_loop_timeout(State), - handler_before_loop(State2#state{messages=Transport:messages()}, - Req2, HandlerState, <<>>). + handler_before_loop(State2#state{key=undefined, + messages=Transport:messages()}, Req2, HandlerState, <<>>). --spec handler_before_loop(#state{}, cowboy_req:req(), any(), binary()) -> closed. +-spec handler_before_loop(#state{}, Req, any(), binary()) + -> {ok, Req, cowboy_middleware:env()} + | {suspend, module(), atom(), [any()]} + when Req::cowboy_req:req(). handler_before_loop(State=#state{ socket=Socket, transport=Transport, hibernate=true}, Req, HandlerState, SoFar) -> Transport:setopts(Socket, [{active, once}]), - catch erlang:hibernate(?MODULE, handler_loop, - [State#state{hibernate=false}, Req, HandlerState, SoFar]), - closed; + {suspend, ?MODULE, handler_loop, + [State#state{hibernate=false}, Req, HandlerState, SoFar]}; handler_before_loop(State=#state{socket=Socket, transport=Transport}, Req, HandlerState, SoFar) -> Transport:setopts(Socket, [{active, once}]), @@ -215,10 +176,12 @@ handler_loop_timeout(State=#state{timeout=Timeout, timeout_ref=PrevRef}) -> State#state{timeout_ref=TRef}. %% @private --spec handler_loop(#state{}, cowboy_req:req(), any(), binary()) -> closed. -handler_loop(State=#state{ - socket=Socket, messages={OK, Closed, Error}, timeout_ref=TRef}, - Req, HandlerState, SoFar) -> +-spec handler_loop(#state{}, Req, any(), binary()) + -> {ok, Req, cowboy_middleware:env()} + | {suspend, module(), atom(), [any()]} + when Req::cowboy_req:req(). +handler_loop(State=#state{socket=Socket, messages={OK, Closed, Error}, + timeout_ref=TRef}, Req, HandlerState, SoFar) -> receive {OK, Socket, Data} -> State2 = handler_loop_timeout(State), @@ -237,191 +200,298 @@ handler_loop(State=#state{ SoFar, websocket_info, Message, fun handler_before_loop/4) end. --spec websocket_data(#state{}, cowboy_req:req(), any(), binary()) -> closed. -%% No more data. -websocket_data(State, Req, HandlerState, <<>>) -> - handler_before_loop(State, Req, HandlerState, <<>>); -%% hixie-76 close frame. -websocket_data(State=#state{version=0}, Req, HandlerState, - << 255, 0, _Rest/binary >>) -> - websocket_close(State, Req, HandlerState, {normal, closed}); -%% hixie-76 data frame. We only support the frame type 0, same as the specs. -websocket_data(State=#state{version=0, eop=EOP}, Req, HandlerState, - Data = << 0, _/binary >>) -> - case binary:match(Data, EOP) of - {Pos, 1} -> - Pos2 = Pos - 1, - << 0, Payload:Pos2/binary, 255, Rest/bits >> = Data, - handler_call(State, Req, HandlerState, - Rest, websocket_handle, {text, Payload}, fun websocket_data/4); - nomatch -> - %% @todo We probably should allow limiting frame length. - handler_before_loop(State, Req, HandlerState, Data) - end; -%% incomplete hybi data frame. -websocket_data(State=#state{version=Version}, Req, HandlerState, Data) - when Version =/= 0, byte_size(Data) =:= 1 -> - handler_before_loop(State, Req, HandlerState, Data); -%% 7 bit payload length prefix exists +%% All frames passing through this function are considered valid, +%% with the only exception of text and close frames with a payload +%% which may still contain errors. +-spec websocket_data(#state{}, Req, any(), binary()) + -> {ok, Req, cowboy_middleware:env()} + | {suspend, module(), atom(), [any()]} + when Req::cowboy_req:req(). +%% RSV bits MUST be 0 unless an extension is negotiated +%% that defines meanings for non-zero values. +websocket_data(State, Req, HandlerState, << _:1, Rsv:3, _/bits >>) + when Rsv =/= 0 -> + websocket_close(State, Req, HandlerState, {error, badframe}); +%% Invalid opcode. Note that these opcodes may be used by extensions. +websocket_data(State, Req, HandlerState, << _:4, Opcode:4, _/bits >>) + when Opcode > 2, Opcode =/= 8, Opcode =/= 9, Opcode =/= 10 -> + websocket_close(State, Req, HandlerState, {error, badframe}); +%% Control frames MUST NOT be fragmented. +websocket_data(State, Req, HandlerState, << 0:1, _:3, Opcode:4, _/bits >>) + when Opcode >= 8 -> + websocket_close(State, Req, HandlerState, {error, badframe}); +%% A frame MUST NOT use the zero opcode unless fragmentation was initiated. +websocket_data(State=#state{frag_state=undefined}, Req, HandlerState, + << _:4, 0:4, _/bits >>) -> + websocket_close(State, Req, HandlerState, {error, badframe}); +%% Non-control opcode when expecting control message or next fragment. +websocket_data(State=#state{frag_state={nofin, _, _}}, Req, HandlerState, + << _:4, Opcode:4, _/bits >>) + when Opcode =/= 0, Opcode < 8 -> + websocket_close(State, Req, HandlerState, {error, badframe}); +%% Close control frame length MUST be 0 or >= 2. +websocket_data(State, Req, HandlerState, << _:4, 8:4, _:1, 1:7, _/bits >>) -> + websocket_close(State, Req, HandlerState, {error, badframe}); +%% Close control frame with incomplete close code. Need more data. websocket_data(State, Req, HandlerState, - << Fin:1, Rsv:3, Opcode:4, Mask:1, PayloadLen:7, Rest/bits >> - = Data) when PayloadLen < 126 -> + Data = << _:4, 8:4, 1:1, Len:7, _/bits >>) + when Len > 1, byte_size(Data) < 8 -> + handler_before_loop(State, Req, HandlerState, Data); +%% 7 bits payload length. +websocket_data(State, Req, HandlerState, << Fin:1, _Rsv:3, Opcode:4, 1:1, + Len:7, MaskKey:32, Rest/bits >>) + when Len < 126 -> websocket_data(State, Req, HandlerState, - Fin, Rsv, Opcode, Mask, PayloadLen, Rest, Data); -%% 7+16 bits payload length prefix exists -websocket_data(State, Req, HandlerState, - << Fin:1, Rsv:3, Opcode:4, Mask:1, 126:7, PayloadLen:16, Rest/bits >> - = Data) when PayloadLen > 125 -> + Opcode, Len, MaskKey, Rest, Fin); +%% 16 bits payload length. +websocket_data(State, Req, HandlerState, << Fin:1, _Rsv:3, Opcode:4, 1:1, + 126:7, Len:16, MaskKey:32, Rest/bits >>) + when Len > 125, Opcode < 8 -> websocket_data(State, Req, HandlerState, - Fin, Rsv, Opcode, Mask, PayloadLen, Rest, Data); -%% 7+16 bits payload length prefix missing -websocket_data(State, Req, HandlerState, - << _Fin:1, _Rsv:3, _Opcode:4, _Mask:1, 126:7, Rest/bits >> - = Data) when byte_size(Rest) < 2 -> - handler_before_loop(State, Req, HandlerState, Data); -%% 7+64 bits payload length prefix exists -websocket_data(State, Req, HandlerState, - << Fin:1, Rsv:3, Opcode:4, Mask:1, 127:7, 0:1, PayloadLen:63, - Rest/bits >> = Data) when PayloadLen > 16#FFFF -> + Opcode, Len, MaskKey, Rest, Fin); +%% 63 bits payload length. +websocket_data(State, Req, HandlerState, << Fin:1, _Rsv:3, Opcode:4, 1:1, + 127:7, 0:1, Len:63, MaskKey:32, Rest/bits >>) + when Len > 16#ffff, Opcode < 8 -> websocket_data(State, Req, HandlerState, - Fin, Rsv, Opcode, Mask, PayloadLen, Rest, Data); -%% 7+64 bits payload length prefix missing -websocket_data(State, Req, HandlerState, - << _Fin:1, _Rsv:3, _Opcode:4, _Mask:1, 127:7, Rest/bits >> - = Data) when byte_size(Rest) < 8 -> - handler_before_loop(State, Req, HandlerState, Data); -%% invalid payload length prefix. -websocket_data(State, Req, HandlerState, _Data) -> - websocket_close(State, Req, HandlerState, {error, badframe}). - --spec websocket_data(#state{}, cowboy_req:req(), any(), non_neg_integer(), - non_neg_integer(), non_neg_integer(), non_neg_integer(), - non_neg_integer(), binary(), binary()) -> closed. -%% A fragmented message MUST start a non-zero opcode. -websocket_data(State=#state{frag_state=undefined}, Req, HandlerState, - _Fin=0, _Rsv=0, _Opcode=0, _Mask, _PayloadLen, _Rest, _Buffer) -> + Opcode, Len, MaskKey, Rest, Fin); +%% When payload length is over 63 bits, the most significant bit MUST be 0. +websocket_data(State, Req, HandlerState, << _:8, 1:1, 127:7, 1:1, _/bits >>) -> websocket_close(State, Req, HandlerState, {error, badframe}); -%% A control message MUST NOT be fragmented. -websocket_data(State, Req, HandlerState, _Fin=0, _Rsv=0, Opcode, _Mask, - _PayloadLen, _Rest, _Buffer) when Opcode >= 8 -> +%% All frames sent from the client to the server are masked. +websocket_data(State, Req, HandlerState, << _:8, 0:1, _/bits >>) -> websocket_close(State, Req, HandlerState, {error, badframe}); -%% The opcode is only included in the first message fragment. -websocket_data(State=#state{frag_state=undefined}, Req, HandlerState, - _Fin=0, _Rsv=0, Opcode, Mask, PayloadLen, Rest, Data) -> - websocket_before_unmask( - State#state{frag_state={nofin, Opcode}}, Req, HandlerState, - Data, Rest, 0, Mask, PayloadLen); -%% non-control opcode when expecting control message or next fragment. -websocket_data(State=#state{frag_state={nofin, _, _}}, Req, HandlerState, _Fin, - _Rsv=0, Opcode, _Mask, _Ln, _Rest, _Data) when Opcode > 0, Opcode < 8 -> +%% For the next two clauses, it can be one of the following: +%% +%% * The minimal number of bytes MUST be used to encode the length +%% * All control frames MUST have a payload length of 125 bytes or less +websocket_data(State, Req, HandlerState, << _:9, 126:7, _:48, _/bits >>) -> websocket_close(State, Req, HandlerState, {error, badframe}); -%% If the first message fragment was incomplete, retry unmasking. -websocket_data(State=#state{frag_state={nofin, Opcode}}, Req, HandlerState, - _Fin=0, _Rsv=0, Opcode, Mask, PayloadLen, Rest, Data) -> - websocket_before_unmask( - State#state{frag_state={nofin, Opcode}}, Req, HandlerState, - Data, Rest, 0, Mask, PayloadLen); -%% if the opcode is zero and the fin flag is zero, unmask and await next. -websocket_data(State=#state{frag_state={nofin, _Opcode, _Payloads}}, Req, - HandlerState, _Fin=0, _Rsv=0, _Opcode2=0, Mask, PayloadLen, Rest, - Data) -> - websocket_before_unmask( - State, Req, HandlerState, Data, Rest, 0, Mask, PayloadLen); -%% when the last fragment is seen. Update the fragmentation status. -websocket_data(State=#state{frag_state={nofin, Opcode, Payloads}}, Req, - HandlerState, _Fin=1, _Rsv=0, _Opcode=0, Mask, PayloadLen, Rest, - Data) -> - websocket_before_unmask( - State#state{frag_state={fin, Opcode, Payloads}}, - Req, HandlerState, Data, Rest, 0, Mask, PayloadLen); -%% control messages MUST NOT use 7+16 bits or 7+64 bits payload length prefixes -websocket_data(State, Req, HandlerState, _Fin, _Rsv, Opcode, _Mask, PayloadLen, - _Rest, _Data) when Opcode >= 8, PayloadLen > 125 -> - websocket_close(State, Req, HandlerState, {error, badframe}); -%% unfragmented message. unmask and dispatch the message. -websocket_data(State=#state{version=Version}, Req, HandlerState, _Fin=1, _Rsv=0, - Opcode, Mask, PayloadLen, Rest, Data) when Version =/= 0 -> - websocket_before_unmask( - State, Req, HandlerState, Data, Rest, Opcode, Mask, PayloadLen); -%% Something was wrong with the frame. Close the connection. -websocket_data(State, Req, HandlerState, _Fin, _Rsv, _Opcode, _Mask, - _PayloadLen, _Rest, _Data) -> - websocket_close(State, Req, HandlerState, {error, badframe}). - -%% hybi routing depending on whether unmasking is needed. --spec websocket_before_unmask(#state{}, cowboy_req:req(), any(), binary(), - binary(), opcode(), 0 | 1, non_neg_integer() | undefined) -> closed. -websocket_before_unmask(State, Req, HandlerState, Data, - Rest, Opcode, Mask, PayloadLen) -> - case {Mask, PayloadLen} of - {0, 0} -> - websocket_dispatch(State, Req, HandlerState, Rest, Opcode, <<>>); - {1, N} when N + 4 > byte_size(Rest); N =:= undefined -> - %% @todo We probably should allow limiting frame length. - handler_before_loop(State, Req, HandlerState, Data); - {1, _N} -> - << MaskKey:32, Payload:PayloadLen/binary, Rest2/bits >> = Rest, - websocket_unmask(State, Req, HandlerState, Rest2, - Opcode, Payload, MaskKey) - end. - -%% hybi unmasking. --spec websocket_unmask(#state{}, cowboy_req:req(), any(), binary(), - opcode(), binary(), mask_key()) -> closed. -websocket_unmask(State, Req, HandlerState, RemainingData, - Opcode, Payload, MaskKey) -> - websocket_unmask(State, Req, HandlerState, RemainingData, - Opcode, Payload, MaskKey, <<>>). - --spec websocket_unmask(#state{}, cowboy_req:req(), any(), binary(), - opcode(), binary(), mask_key(), binary()) -> closed. -websocket_unmask(State, Req, HandlerState, RemainingData, - Opcode, << O:32, Rest/bits >>, MaskKey, Acc) -> +websocket_data(State, Req, HandlerState, << _:9, 127:7, _:96, _/bits >>) -> + websocket_close(State, Req, HandlerState, {error, badframe}); +%% Need more data. +websocket_data(State, Req, HandlerState, Data) -> + handler_before_loop(State, Req, HandlerState, Data). + +%% Initialize or update fragmentation state. +-spec websocket_data(#state{}, Req, any(), + opcode(), non_neg_integer(), mask_key(), binary(), 0 | 1) + -> {ok, Req, cowboy_middleware:env()} + | {suspend, module(), atom(), [any()]} + when Req::cowboy_req:req(). +%% The opcode is only included in the first frame fragment. +websocket_data(State=#state{frag_state=undefined}, Req, HandlerState, + Opcode, Len, MaskKey, Data, 0) -> + websocket_payload(State#state{frag_state={nofin, Opcode, <<>>}}, + Req, HandlerState, 0, Len, MaskKey, <<>>, Data); +%% Subsequent frame fragments. +websocket_data(State=#state{frag_state={nofin, _, _}}, Req, HandlerState, + 0, Len, MaskKey, Data, 0) -> + websocket_payload(State, Req, HandlerState, + 0, Len, MaskKey, <<>>, Data); +%% Final frame fragment. +websocket_data(State=#state{frag_state={nofin, Opcode, SoFar}}, + Req, HandlerState, 0, Len, MaskKey, Data, 1) -> + websocket_payload(State#state{frag_state={fin, Opcode, SoFar}}, + Req, HandlerState, 0, Len, MaskKey, <<>>, Data); +%% Unfragmented frame. +websocket_data(State, Req, HandlerState, Opcode, Len, MaskKey, Data, 1) -> + websocket_payload(State, Req, HandlerState, + Opcode, Len, MaskKey, <<>>, Data). + +-spec websocket_payload(#state{}, Req, any(), + opcode(), non_neg_integer(), mask_key(), binary(), binary()) + -> {ok, Req, cowboy_middleware:env()} + | {suspend, module(), atom(), [any()]} + when Req::cowboy_req:req(). +%% Close control frames with a payload MUST contain a valid close code. +websocket_payload(State, Req, HandlerState, + Opcode=8, Len, MaskKey, <<>>, << MaskedCode:2/binary, Rest/bits >>) -> + Unmasked = << Code:16 >> = websocket_unmask(MaskedCode, MaskKey, <<>>), + if Code < 1000; Code =:= 1004; Code =:= 1005; Code =:= 1006; + (Code > 1011) and (Code < 3000); Code > 4999 -> + websocket_close(State, Req, HandlerState, {error, badframe}); + true -> + websocket_payload(State, Req, HandlerState, + Opcode, Len - 2, MaskKey, Unmasked, Rest) + end; +%% Text frames and close control frames MUST have a payload that is valid UTF-8. +websocket_payload(State=#state{utf8_state=Incomplete}, + Req, HandlerState, Opcode, Len, MaskKey, Unmasked, Data) + when (byte_size(Data) < Len) andalso ((Opcode =:= 1) orelse + ((Opcode =:= 8) andalso (Unmasked =/= <<>>))) -> + Unmasked2 = websocket_unmask(Data, + rotate_mask_key(MaskKey, byte_size(Unmasked)), <<>>), + case is_utf8(<< Incomplete/binary, Unmasked2/binary >>) of + false -> + websocket_close(State, Req, HandlerState, {error, badencoding}); + Utf8State -> + websocket_payload_loop(State#state{utf8_state=Utf8State}, + Req, HandlerState, Opcode, Len - byte_size(Data), MaskKey, + << Unmasked/binary, Unmasked2/binary >>) + end; +websocket_payload(State=#state{utf8_state=Incomplete}, + Req, HandlerState, Opcode, Len, MaskKey, Unmasked, Data) + when Opcode =:= 1; (Opcode =:= 8) and (Unmasked =/= <<>>) -> + << End:Len/binary, Rest/bits >> = Data, + Unmasked2 = websocket_unmask(End, + rotate_mask_key(MaskKey, byte_size(Unmasked)), <<>>), + case is_utf8(<< Incomplete/binary, Unmasked2/binary >>) of + <<>> -> + websocket_dispatch(State#state{utf8_state= <<>>}, + Req, HandlerState, Rest, Opcode, + << Unmasked/binary, Unmasked2/binary >>); + _ -> + websocket_close(State, Req, HandlerState, {error, badencoding}) + end; +%% Fragmented text frames may cut payload in the middle of UTF-8 codepoints. +websocket_payload(State=#state{frag_state={_, 1, _}, utf8_state=Incomplete}, + Req, HandlerState, Opcode=0, Len, MaskKey, Unmasked, Data) + when byte_size(Data) < Len -> + Unmasked2 = websocket_unmask(Data, + rotate_mask_key(MaskKey, byte_size(Unmasked)), <<>>), + case is_utf8(<< Incomplete/binary, Unmasked2/binary >>) of + false -> + websocket_close(State, Req, HandlerState, {error, badencoding}); + Utf8State -> + websocket_payload_loop(State#state{utf8_state=Utf8State}, + Req, HandlerState, Opcode, Len - byte_size(Data), MaskKey, + << Unmasked/binary, Unmasked2/binary >>) + end; +websocket_payload(State=#state{frag_state={Fin, 1, _}, utf8_state=Incomplete}, + Req, HandlerState, Opcode=0, Len, MaskKey, Unmasked, Data) -> + << End:Len/binary, Rest/bits >> = Data, + Unmasked2 = websocket_unmask(End, + rotate_mask_key(MaskKey, byte_size(Unmasked)), <<>>), + case is_utf8(<< Incomplete/binary, Unmasked2/binary >>) of + <<>> -> + websocket_dispatch(State#state{utf8_state= <<>>}, + Req, HandlerState, Rest, Opcode, + << Unmasked/binary, Unmasked2/binary >>); + Utf8State when is_binary(Utf8State), Fin =:= nofin -> + websocket_dispatch(State#state{utf8_state=Utf8State}, + Req, HandlerState, Rest, Opcode, + << Unmasked/binary, Unmasked2/binary >>); + _ -> + websocket_close(State, Req, HandlerState, {error, badencoding}) + end; +%% Other frames have a binary payload. +websocket_payload(State, Req, HandlerState, + Opcode, Len, MaskKey, Unmasked, Data) + when byte_size(Data) < Len -> + Unmasked2 = websocket_unmask(Data, + rotate_mask_key(MaskKey, byte_size(Unmasked)), Unmasked), + websocket_payload_loop(State, Req, HandlerState, + Opcode, Len - byte_size(Data), MaskKey, Unmasked2); +websocket_payload(State, Req, HandlerState, + Opcode, Len, MaskKey, Unmasked, Data) -> + << End:Len/binary, Rest/bits >> = Data, + Unmasked2 = websocket_unmask(End, + rotate_mask_key(MaskKey, byte_size(Unmasked)), Unmasked), + websocket_dispatch(State, Req, HandlerState, Rest, Opcode, Unmasked2). + +-spec websocket_unmask(B, mask_key(), B) -> B when B::binary(). +websocket_unmask(<<>>, _, Unmasked) -> + Unmasked; +websocket_unmask(<< O:32, Rest/bits >>, MaskKey, Acc) -> T = O bxor MaskKey, - websocket_unmask(State, Req, HandlerState, RemainingData, - Opcode, Rest, MaskKey, << Acc/binary, T:32 >>); -websocket_unmask(State, Req, HandlerState, RemainingData, - Opcode, << O:24 >>, MaskKey, Acc) -> + websocket_unmask(Rest, MaskKey, << Acc/binary, T:32 >>); +websocket_unmask(<< O:24 >>, MaskKey, Acc) -> << MaskKey2:24, _:8 >> = << MaskKey:32 >>, T = O bxor MaskKey2, - websocket_dispatch(State, Req, HandlerState, RemainingData, - Opcode, << Acc/binary, T:24 >>); -websocket_unmask(State, Req, HandlerState, RemainingData, - Opcode, << O:16 >>, MaskKey, Acc) -> + << Acc/binary, T:24 >>; +websocket_unmask(<< O:16 >>, MaskKey, Acc) -> << MaskKey2:16, _:16 >> = << MaskKey:32 >>, T = O bxor MaskKey2, - websocket_dispatch(State, Req, HandlerState, RemainingData, - Opcode, << Acc/binary, T:16 >>); -websocket_unmask(State, Req, HandlerState, RemainingData, - Opcode, << O:8 >>, MaskKey, Acc) -> + << Acc/binary, T:16 >>; +websocket_unmask(<< O:8 >>, MaskKey, Acc) -> << MaskKey2:8, _:24 >> = << MaskKey:32 >>, T = O bxor MaskKey2, - websocket_dispatch(State, Req, HandlerState, RemainingData, - Opcode, << Acc/binary, T:8 >>); -websocket_unmask(State, Req, HandlerState, RemainingData, - Opcode, <<>>, _MaskKey, Acc) -> - websocket_dispatch(State, Req, HandlerState, RemainingData, - Opcode, Acc). + << Acc/binary, T:8 >>. + +%% Because we unmask on the fly we need to continue from the right mask byte. +-spec rotate_mask_key(mask_key(), non_neg_integer()) -> mask_key(). +rotate_mask_key(MaskKey, UnmaskedLen) -> + Left = UnmaskedLen rem 4, + Right = 4 - Left, + (MaskKey bsl (Left * 8)) + (MaskKey bsr (Right * 8)). + +%% Returns <<>> if the argument is valid UTF-8, false if not, +%% or the incomplete part of the argument if we need more data. +-spec is_utf8(binary()) -> false | binary(). +is_utf8(Valid = <<>>) -> + Valid; +is_utf8(<< _/utf8, Rest/binary >>) -> + is_utf8(Rest); +%% 2 bytes. Codepages C0 and C1 are invalid; fail early. +is_utf8(<< 2#1100000:7, _/bits >>) -> + false; +is_utf8(Incomplete = << 2#110:3, _:5 >>) -> + Incomplete; +%% 3 bytes. +is_utf8(Incomplete = << 2#1110:4, _:4 >>) -> + Incomplete; +is_utf8(Incomplete = << 2#1110:4, _:4, 2#10:2, _:6 >>) -> + Incomplete; +%% 4 bytes. Codepage F4 may have invalid values greater than 0x10FFFF. +is_utf8(<< 2#11110100:8, 2#10:2, High:6, _/bits >>) when High >= 2#10000 -> + false; +is_utf8(Incomplete = << 2#11110:5, _:3 >>) -> + Incomplete; +is_utf8(Incomplete = << 2#11110:5, _:3, 2#10:2, _:6 >>) -> + Incomplete; +is_utf8(Incomplete = << 2#11110:5, _:3, 2#10:2, _:6, 2#10:2, _:6 >>) -> + Incomplete; +%% Invalid. +is_utf8(_) -> + false. + +-spec websocket_payload_loop(#state{}, Req, any(), + opcode(), non_neg_integer(), mask_key(), binary()) + -> {ok, Req, cowboy_middleware:env()} + | {suspend, module(), atom(), [any()]} + when Req::cowboy_req:req(). +websocket_payload_loop(State=#state{socket=Socket, transport=Transport, + messages={OK, Closed, Error}, timeout_ref=TRef}, + Req, HandlerState, Opcode, Len, MaskKey, Unmasked) -> + Transport:setopts(Socket, [{active, once}]), + receive + {OK, Socket, Data} -> + State2 = handler_loop_timeout(State), + websocket_payload(State2, Req, HandlerState, + Opcode, Len, MaskKey, Unmasked, Data); + {Closed, Socket} -> + handler_terminate(State, Req, HandlerState, {error, closed}); + {Error, Socket, Reason} -> + handler_terminate(State, Req, HandlerState, {error, Reason}); + {timeout, TRef, ?MODULE} -> + websocket_close(State, Req, HandlerState, {normal, timeout}); + {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) -> + websocket_payload_loop(State, Req, HandlerState, + Opcode, Len, MaskKey, Unmasked); + Message -> + handler_call(State, Req, HandlerState, + <<>>, websocket_info, Message, + fun (State2, Req2, HandlerState2, _) -> + websocket_payload_loop(State2, Req2, HandlerState2, + Opcode, Len, MaskKey, Unmasked) + end) + end. -%% hybi dispatching. --spec websocket_dispatch(#state{}, cowboy_req:req(), any(), binary(), - opcode(), binary()) -> closed. -%% First frame of a fragmented message unmasked. Expect intermediate or last. -websocket_dispatch(State=#state{frag_state={nofin, Opcode}}, Req, HandlerState, - RemainingData, 0, Payload) -> - websocket_data(State#state{frag_state={nofin, Opcode, Payload}}, - Req, HandlerState, RemainingData); -%% Intermediate frame of a fragmented message unmasked. Add payload to buffer. -websocket_dispatch(State=#state{frag_state={nofin, Opcode, Payloads}}, Req, - HandlerState, RemainingData, 0, Payload) -> +-spec websocket_dispatch(#state{}, Req, any(), binary(), opcode(), binary()) + -> {ok, Req, cowboy_middleware:env()} + | {suspend, module(), atom(), [any()]} + when Req::cowboy_req:req(). +%% Continuation frame. +websocket_dispatch(State=#state{frag_state={nofin, Opcode, SoFar}}, + Req, HandlerState, RemainingData, 0, Payload) -> websocket_data(State#state{frag_state={nofin, Opcode, - <<Payloads/binary, Payload/binary>>}}, Req, HandlerState, - RemainingData); -%% Last frame of a fragmented message unmasked. Dispatch to handler. -websocket_dispatch(State=#state{frag_state={fin, Opcode, Payloads}}, Req, - HandlerState, RemainingData, 0, Payload) -> + << SoFar/binary, Payload/binary >>}}, Req, HandlerState, RemainingData); +%% Last continuation frame. +websocket_dispatch(State=#state{frag_state={fin, Opcode, SoFar}}, + Req, HandlerState, RemainingData, 0, Payload) -> websocket_dispatch(State#state{frag_state=undefined}, Req, HandlerState, - RemainingData, Opcode, <<Payloads/binary, Payload/binary>>); + RemainingData, Opcode, << SoFar/binary, Payload/binary >>); %% Text frame. websocket_dispatch(State, Req, HandlerState, RemainingData, 1, Payload) -> handler_call(State, Req, HandlerState, RemainingData, @@ -431,13 +501,15 @@ websocket_dispatch(State, Req, HandlerState, RemainingData, 2, Payload) -> handler_call(State, Req, HandlerState, RemainingData, websocket_handle, {binary, Payload}, fun websocket_data/4); %% Close control frame. -%% @todo Handle the optional Payload. -websocket_dispatch(State, Req, HandlerState, _RemainingData, 8, _Payload) -> - websocket_close(State, Req, HandlerState, {normal, closed}); +websocket_dispatch(State, Req, HandlerState, _RemainingData, 8, <<>>) -> + websocket_close(State, Req, HandlerState, {remote, closed}); +websocket_dispatch(State, Req, HandlerState, _RemainingData, 8, + << Code:16, Payload/bits >>) -> + websocket_close(State, Req, HandlerState, {remote, Code, Payload}); %% Ping control frame. Send a pong back and forward the ping to the handler. websocket_dispatch(State=#state{socket=Socket, transport=Transport}, Req, HandlerState, RemainingData, 9, Payload) -> - Len = hybi_payload_length(byte_size(Payload)), + Len = payload_length_to_binary(byte_size(Payload)), Transport:send(Socket, << 1:1, 0:3, 10:4, 0:1, Len/bits, Payload/binary >>), handler_call(State, Req, HandlerState, RemainingData, websocket_handle, {ping, Payload}, fun websocket_data/4); @@ -446,10 +518,12 @@ websocket_dispatch(State, Req, HandlerState, RemainingData, 10, Payload) -> handler_call(State, Req, HandlerState, RemainingData, websocket_handle, {pong, Payload}, fun websocket_data/4). --spec handler_call(#state{}, cowboy_req:req(), any(), binary(), - atom(), any(), fun()) -> closed. -handler_call(State=#state{handler=Handler, opts=Opts}, Req, HandlerState, - RemainingData, Callback, Message, NextState) -> +-spec handler_call(#state{}, Req, any(), binary(), atom(), any(), fun()) + -> {ok, Req, cowboy_middleware:env()} + | {suspend, module(), atom(), [any()]} + when Req::cowboy_req:req(). +handler_call(State=#state{handler=Handler, handler_opts=HandlerOpts}, Req, + HandlerState, RemainingData, Callback, Message, NextState) -> try Handler:Callback(Message, Req, HandlerState) of {ok, Req2, HandlerState2} -> NextState(State, Req2, HandlerState2, RemainingData); @@ -515,7 +589,7 @@ handler_call(State=#state{handler=Handler, opts=Opts}, Req, HandlerState, " 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, 3, Class, Reason, Message, Opts, + [Handler, Callback, 3, Class, Reason, Message, HandlerOpts, HandlerState, PLReq, erlang:get_stacktrace()]), websocket_close(State, Req, HandlerState, {error, handler}) end. @@ -528,13 +602,6 @@ 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}) -> - Transport:send(Socket, [0, Payload, 255]); -%% 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), @@ -554,7 +621,7 @@ websocket_send({Type = close, StatusCode, Payload}, #state{ Len = 2 + iolist_size(Payload), %% Control packets must not be > 125 in length. true = Len =< 125, - BinLen = hybi_payload_length(Len), + BinLen = payload_length_to_binary(Len), Transport:send(Socket, [<< 1:1, 0:3, Opcode:4, 0:1, BinLen/bits, StatusCode:16 >>, Payload]), shutdown; @@ -567,7 +634,7 @@ websocket_send({Type, Payload}, #state{socket=Socket, transport=Transport}) -> true -> true end, - BinLen = hybi_payload_length(Len), + BinLen = payload_length_to_binary(Len), Transport:send(Socket, [<< 1:1, 0:3, Opcode:4, 0:1, BinLen/bits >>, Payload]). @@ -582,20 +649,32 @@ websocket_send_many([Frame|Tail], State) -> Error -> Error end. --spec websocket_close(#state{}, cowboy_req:req(), any(), {atom(), atom()}) - -> closed. -websocket_close(State=#state{socket=Socket, transport=Transport, version=0}, - Req, HandlerState, Reason) -> - Transport:send(Socket, << 255, 0 >>), - handler_terminate(State, Req, HandlerState, Reason); +-spec websocket_close(#state{}, Req, any(), + {atom(), atom()} | {remote, close_code(), binary()}) + -> {ok, Req, cowboy_middleware:env()} + when Req::cowboy_req:req(). websocket_close(State=#state{socket=Socket, transport=Transport}, Req, HandlerState, Reason) -> - Transport:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>), + case Reason of + {normal, _} -> + Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1000:16 >>); + {error, badframe} -> + Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1002:16 >>); + {error, badencoding} -> + Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1007:16 >>); + {error, handler} -> + Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1011:16 >>); + {remote, closed} -> + Transport:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>); + {remote, Code, _} -> + Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, Code:16 >>) + end, handler_terminate(State, Req, HandlerState, Reason). --spec handler_terminate(#state{}, cowboy_req:req(), - any(), atom() | {atom(), atom()}) -> closed. -handler_terminate(#state{handler=Handler, opts=Opts}, +-spec handler_terminate(#state{}, Req, any(), atom() | {atom(), atom()}) + -> {ok, Req, cowboy_middleware:env()} + when Req::cowboy_req:req(). +handler_terminate(#state{env=Env, handler=Handler, handler_opts=HandlerOpts}, Req, HandlerState, TerminateReason) -> try Handler:websocket_terminate(TerminateReason, Req, HandlerState) @@ -606,35 +685,14 @@ handler_terminate(#state{handler=Handler, opts=Opts}, " 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, websocket_terminate, 3, Class, Reason, TerminateReason, Opts, - HandlerState, PLReq, erlang:get_stacktrace()]) + [Handler, websocket_terminate, 3, Class, Reason, TerminateReason, + HandlerOpts, HandlerState, PLReq, erlang:get_stacktrace()]) end, - closed. - -%% hixie-76 specific. - --spec hixie76_challenge(binary(), binary(), binary()) -> binary(). -hixie76_challenge(Key1, Key2, Key3) -> - IntKey1 = hixie76_key_to_integer(Key1), - IntKey2 = hixie76_key_to_integer(Key2), - erlang:md5(<< IntKey1:32, IntKey2:32, Key3/binary >>). - --spec hixie76_key_to_integer(binary()) -> integer(). -hixie76_key_to_integer(Key) -> - Number = list_to_integer([C || << C >> <= Key, C >= $0, C =< $9]), - Spaces = length([C || << C >> <= Key, C =:= 32]), - Number div Spaces. - -%% hybi specific. - --spec hybi_challenge(binary()) -> binary(). -hybi_challenge(Key) -> - Bin = << Key/binary, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" >>, - base64:encode(crypto:sha(Bin)). + {ok, Req, [{result, closed}|Env]}. --spec hybi_payload_length(0..16#7fffffffffffffff) +-spec payload_length_to_binary(0..16#7fffffffffffffff) -> << _:7 >> | << _:23 >> | << _:71 >>. -hybi_payload_length(N) -> +payload_length_to_binary(N) -> case N of N when N =< 125 -> << N:7 >>; N when N =< 16#ffff -> << 126:7, N:16 >>; diff --git a/src/cowboy_websocket_handler.erl b/src/cowboy_websocket_handler.erl index 6d7f9de..bd2ed5a 100644 --- a/src/cowboy_websocket_handler.erl +++ b/src/cowboy_websocket_handler.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]> +%% Copyright (c) 2011-2013, Loïc Hoguin <[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 @@ -50,7 +50,7 @@ -type opts() :: any(). -type state() :: any(). --type terminate_reason() :: {normal, closed} +-type terminate_reason() :: {normal, shutdown} | {normal, timeout} | {error, closed} | {error, badframe} |