diff options
author | Loïc Hoguin <[email protected]> | 2013-01-03 22:47:51 +0100 |
---|---|---|
committer | Loïc Hoguin <[email protected]> | 2013-01-03 22:47:51 +0100 |
commit | 1b3f510b7e8d5413901ba72adfe361773f3e9097 (patch) | |
tree | 314d24a8bbbdfc1e326cac28193ab6d9f7dc3b61 /src | |
parent | 73d86057f2f9d6b3de5fb12e23b2cd65be50e226 (diff) | |
download | cowboy-1b3f510b7e8d5413901ba72adfe361773f3e9097.tar.gz cowboy-1b3f510b7e8d5413901ba72adfe361773f3e9097.tar.bz2 cowboy-1b3f510b7e8d5413901ba72adfe361773f3e9097.zip |
Add middleware support
Middlewares allow customizing the request processing.
All existing Cowboy project are incompatible with this commit.
You need to change `{dispatch, Dispatch}` in the protocol options
to `{env, [{dispatch, Dispatch}]}` to fix your code.
Diffstat (limited to 'src')
-rw-r--r-- | src/cowboy_handler.erl | 201 | ||||
-rw-r--r-- | src/cowboy_middleware.erl | 36 | ||||
-rw-r--r-- | src/cowboy_protocol.erl | 220 | ||||
-rw-r--r-- | src/cowboy_rest.erl | 34 | ||||
-rw-r--r-- | src/cowboy_router.erl | 49 | ||||
-rw-r--r-- | src/cowboy_websocket.erl | 137 |
6 files changed, 439 insertions, 238 deletions
diff --git a/src/cowboy_handler.erl b/src/cowboy_handler.erl new file mode 100644 index 0000000..cc871d9 --- /dev/null +++ b/src/cowboy_handler.erl @@ -0,0 +1,201 @@ +%% 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() +}). + +%% @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); + %% @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, 500, Req} + 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) + 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, 500, Req} + 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 + {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(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); + {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, 500, Req} + end. + +-spec terminate_request(Req, #state{}, module(), any()) -> + {ok, Req, cowboy_middleware:env()} when Req::cowboy_req:req(). +terminate_request(Req, #state{env=Env}, Handler, HandlerState) -> + HandlerRes = handler_terminate(Req, Handler, HandlerState), + {ok, Req, [{result, HandlerRes}|Env]}. + +-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. 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..b82fa2b 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,8 @@ %% %% The available options are: %% <dl> -%% <dt>dispatch</dt><dd>The dispatch list for this protocol.</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. @@ -30,6 +31,8 @@ %% keep-alive session. Defaults to infinity.</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> @@ -41,9 +44,6 @@ %% %% 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 +52,19 @@ %% 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()], + env :: cowboy_middleware:env(), onrequest :: undefined | onrequest_fun(), onresponse = undefined :: undefined | onresponse_fun(), max_empty_lines :: non_neg_integer(), @@ -75,10 +74,7 @@ max_header_name_length :: non_neg_integer(), 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() + timeout :: timeout() }). %% API. @@ -102,19 +98,20 @@ get_value(Key, Opts, Default) -> %% @private -spec init(pid(), inet:socket(), module(), any()) -> ok. init(ListenerPid, Socket, Transport, Opts) -> - Dispatch = get_value(dispatch, Opts, []), 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), 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, env=Env, max_empty_lines=MaxEmptyLines, max_keepalive=MaxKeepalive, max_request_line_length=MaxRequestLineLength, max_header_name_length=MaxHeaderNameLength, @@ -442,177 +439,58 @@ request(Buffer, State=#state{socket=Socket, transport=Transport, Req = cowboy_req:new(Socket, Transport, Method, Path, Query, Fragment, Version, Headers, Host, Port, Buffer, ReqKeepalive < MaxKeepalive, OnResponse), - onrequest(Req, State, Host). + 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 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 execute(cowboy_req:req(), #state{}) -> ok. +execute(Req, State=#state{middlewares=Middlewares, env=Env}) -> + execute(Req, State, Env, Middlewares). --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) -> cowboy_req:ensure_response(Req, 204), diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl index c6b53bd..511cd20 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) -> @@ -738,8 +739,7 @@ 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; + {error, 500, Req}; {halt, Req2, HandlerState} -> terminate(Req2, State#state{handler_state=HandlerState}); {true, Req2, HandlerState} -> @@ -790,8 +790,7 @@ 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; + {error, 500, Req5}; {halt, Req6, HandlerState} -> terminate(Req6, State4#state{handler_state=HandlerState}); {Body, Req6, HandlerState} -> @@ -915,10 +914,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..35c9396 --- /dev/null +++ b/src/cowboy_router.erl @@ -0,0 +1,49 @@ +%% 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. +%% +%% @see cowboy_dispatcher +-module(cowboy_router). +-behaviour(cowboy_middleware). + +-export([execute/2]). + +%% @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 cowboy_dispatcher: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. diff --git a/src/cowboy_websocket.erl b/src/cowboy_websocket.erl index 8c02ac7..abcc015 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 @@ -38,11 +38,12 @@ {fin, opcode(), binary()}. %% last fragment has been seen. -record(state, { + env :: cowboy_middleware:env(), socket = undefined :: inet:socket(), transport = undefined :: module(), version :: 0 | 7 | 8 | 13, handler :: module(), - opts :: any(), + handler_opts :: any(), challenge = undefined :: undefined | binary() | {binary(), binary()}, timeout = infinity :: timeout(), timeout_ref = undefined :: undefined | reference(), @@ -58,15 +59,19 @@ %% 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}, + 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) @@ -110,10 +115,13 @@ websocket_upgrade(Version, State, Req) {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 +-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,27 +135,31 @@ 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) -> +-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. +-spec websocket_handshake(#state{}, Req, any()) + -> {ok, Req, cowboy_middleware:env()} + | {suspend, module(), atom(), [any()]} + when Req::cowboy_req:req(). websocket_handshake(State=#state{socket=Socket, transport=Transport, version=0, origin=Origin, challenge={Key1, Key2}}, Req, HandlerState) -> @@ -192,14 +204,16 @@ websocket_handshake(State=#state{transport=Transport, challenge=Challenge}, handler_before_loop(State2#state{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,7 +229,10 @@ 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. +-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) -> @@ -237,7 +254,10 @@ handler_loop(State=#state{ SoFar, websocket_info, Message, fun handler_before_loop/4) end. --spec websocket_data(#state{}, cowboy_req:req(), any(), binary()) -> closed. +-spec websocket_data(#state{}, Req, any(), binary()) + -> {ok, Req, cowboy_middleware:env()} + | {suspend, module(), atom(), [any()]} + when Req::cowboy_req:req(). %% No more data. websocket_data(State, Req, HandlerState, <<>>) -> handler_before_loop(State, Req, HandlerState, <<>>); @@ -294,9 +314,11 @@ websocket_data(State, Req, HandlerState, websocket_data(State, Req, HandlerState, _Data) -> websocket_close(State, Req, HandlerState, {error, badframe}). --spec websocket_data(#state{}, cowboy_req:req(), any(), non_neg_integer(), +-spec websocket_data(#state{}, Req, any(), non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer(), - non_neg_integer(), binary(), binary()) -> closed. + non_neg_integer(), binary(), binary()) + -> {ok, Req, cowboy_middleware:env()} + when Req::cowboy_req:req(). %% 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) -> @@ -349,8 +371,11 @@ websocket_data(State, Req, HandlerState, _Fin, _Rsv, _Opcode, _Mask, 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. +-spec websocket_before_unmask(#state{}, Req, any(), binary(), + binary(), opcode(), 0 | 1, non_neg_integer() | undefined) + -> {ok, Req, cowboy_middleware:env()} + | {suspend, module(), atom(), [any()]} + when Req::cowboy_req:req(). websocket_before_unmask(State, Req, HandlerState, Data, Rest, Opcode, Mask, PayloadLen) -> case {Mask, PayloadLen} of @@ -366,15 +391,21 @@ websocket_before_unmask(State, Req, HandlerState, Data, end. %% hybi unmasking. --spec websocket_unmask(#state{}, cowboy_req:req(), any(), binary(), - opcode(), binary(), mask_key()) -> closed. +-spec websocket_unmask(#state{}, Req, any(), binary(), + opcode(), binary(), mask_key()) + -> {ok, Req, cowboy_middleware:env()} + | {suspend, module(), atom(), [any()]} + when Req::cowboy_req:req(). 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. +-spec websocket_unmask(#state{}, Req, any(), binary(), + opcode(), binary(), mask_key(), binary()) + -> {ok, Req, cowboy_middleware:env()} + | {suspend, module(), atom(), [any()]} + when Req::cowboy_req:req(). websocket_unmask(State, Req, HandlerState, RemainingData, Opcode, << O:32, Rest/bits >>, MaskKey, Acc) -> T = O bxor MaskKey, @@ -404,8 +435,10 @@ websocket_unmask(State, Req, HandlerState, RemainingData, Opcode, Acc). %% hybi dispatching. --spec websocket_dispatch(#state{}, cowboy_req:req(), any(), binary(), - opcode(), binary()) -> closed. +-spec websocket_dispatch(#state{}, Req, any(), binary(), opcode(), binary()) + -> {ok, Req, cowboy_middleware:env()} + | {suspend, module(), atom(), [any()]} + when Req::cowboy_req:req(). %% First frame of a fragmented message unmasked. Expect intermediate or last. websocket_dispatch(State=#state{frag_state={nofin, Opcode}}, Req, HandlerState, RemainingData, 0, Payload) -> @@ -446,10 +479,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 +550,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. @@ -582,8 +617,9 @@ websocket_send_many([Frame|Tail], State) -> Error -> Error end. --spec websocket_close(#state{}, cowboy_req:req(), any(), {atom(), atom()}) - -> closed. +-spec websocket_close(#state{}, Req, any(), {atom(), atom()}) + -> {ok, Req, cowboy_middleware:env()} + when Req::cowboy_req:req(). websocket_close(State=#state{socket=Socket, transport=Transport, version=0}, Req, HandlerState, Reason) -> Transport:send(Socket, << 255, 0 >>), @@ -593,9 +629,10 @@ websocket_close(State=#state{socket=Socket, transport=Transport}, Transport:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>), 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,10 +643,10 @@ 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. + {ok, Req, [{result, closed}|Env]}. %% hixie-76 specific. |