Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extra routing layer #639

Merged
merged 12 commits into from
Feb 3, 2016
29 changes: 14 additions & 15 deletions apps/ejabberd/src/ejabberd_cowboy.erl
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
-export([start/2,
stop/1]).

%% helper for internal use
-export([handler/1]).

-include("ejabberd.hrl").

%%--------------------------------------------------------------------
Expand All @@ -45,22 +48,13 @@
socket_type() ->
independent.

start_listener({Port, IP, tcp}, Opts) ->
%% ejabberd_listener brutally kills its children, and doesn't provide any
%% mechanism for doing a clean shutdown. To work around this, we could
%% start two linked processes: one to be killed, and another to trap the
%% exit signal and shut down Cowboy cleanly. However, a simpler solution is
%% to manually configure supervision and not use brutal_kill. Calling
%% supervisor:start_child(ejabberd_listeners, ...) would hang since we're
%% running in the ejabberd_listeners process and start_child() is
%% synchronous. So, simply use ejabberd_sup as the supervisor instead.
IPPort = [inet_parse:ntoa(IP), <<"_">>, integer_to_list(Port)],
ChildSpec = {cowboy_ref(IPPort), {?MODULE, start_link, [IPPort]}, permanent,
start_listener({Port, IP, tcp}=Listener, Opts) ->
IPPort = handler(Listener),
ChildSpec = {Listener, {?MODULE, start_link, [IPPort]}, transient,
infinity, worker, [?MODULE]},
supervisor:start_child(ejabberd_sup, ChildSpec),
start_cowboy(IPPort, [{port, Port}, {ip, IP} | Opts]),
%% Tell ejabberd_listener not to supervise us
ignore.
{ok, Pid} = supervisor:start_child(ejabberd_listeners, ChildSpec),
{ok, _} = start_cowboy(IPPort, [{port, Port}, {ip, IP} | Opts]),
{ok, Pid}.

%% @doc gen_server for handling shutdown when started via ejabberd_listener
-spec start_link(_) -> 'ignore' | {'error',_} | {'ok',pid()}.
Expand All @@ -80,6 +74,10 @@ code_change(_OldVsn, Ref, _Extra) ->
terminate(_Reason, Ref) ->
stop_cowboy(Ref).

-spec handler({inet:ip_address(), integer, tcp}) -> iolist().
handler({Port, IP, tcp}) ->
[inet_parse:ntoa(IP), <<"_">>, integer_to_list(Port)].

%%--------------------------------------------------------------------
%% gen_mod API
%%--------------------------------------------------------------------
Expand Down Expand Up @@ -125,6 +123,7 @@ stop_cowboy(Ref) ->
cowboy:stop_listener(cowboy_ref(Ref)),
ok.


cowboy_ref(Ref) ->
ModRef = [?MODULE_STRING, <<"_">>, Ref],
list_to_atom(binary_to_list(iolist_to_binary(ModRef))).
Expand Down
29 changes: 16 additions & 13 deletions apps/ejabberd/src/ejabberd_listener.erl
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
-type portnum() :: inet:port_number().
-type port_ip_proto() :: portnum() | {portnum(), addr() | proto()} | {portnum(), addr(), proto()}.

-spec start_link() -> 'ignore' | {'error', _} | {'ok', pid()}.
-spec start_link() -> 'ignore' | {'error',_} | {'ok',pid()}.
start_link() ->
supervisor:start_link({local, ejabberd_listeners}, ?MODULE, []).

Expand Down Expand Up @@ -94,11 +94,8 @@ report_duplicated_portips(L) ->
Module :: atom() | tuple(),
Opts :: [any()]) -> any().
start(Port, Module, Opts) ->
%% Check if the module is an ejabberd listener or an independent listener
case Module:socket_type() of
independent -> Module:start_listener(Port, Opts);
_ -> start_dependent(Port, Module, Opts)
end.
%% at this point, Module:socket_type() must not be 'independent'
start_dependent(Port, Module, Opts).

-spec start_dependent(Port :: _,
Module :: atom() | tuple(),
Expand Down Expand Up @@ -375,13 +372,19 @@ start_module_sup(_PortIPProto, Module) ->
-spec start_listener_sup(port_ip_proto(), Module :: atom(), Opts :: [any()])
-> {'error',_} | {'ok','undefined' | pid()} | {'ok','undefined' | pid(),_}.
start_listener_sup(PortIPProto, Module, Opts) ->
ChildSpec = {PortIPProto,
{?MODULE, start, [PortIPProto, Module, Opts]},
transient,
brutal_kill,
worker,
[?MODULE]},
supervisor:start_child(ejabberd_listeners, ChildSpec).
case Module:socket_type() of
independent ->
Module:start_listener(PortIPProto, Opts);
_ ->

ChildSpec = {PortIPProto,
{?MODULE, start, [PortIPProto, Module, Opts]},
transient,
brutal_kill,
worker,
[?MODULE]},
supervisor:start_child(ejabberd_listeners, ChildSpec)
end.

-spec stop_listeners() -> 'ok'.
stop_listeners() ->
Expand Down
196 changes: 196 additions & 0 deletions apps/ejabberd/src/mod_cowboy.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
%%%===================================================================
%%% @copyright (C) 2014, Erlang Solutions Ltd.
%%% @doc HTTP routing layer for MongooseIM's Cowboy listener
%%% @end
%%%===================================================================
-module(mod_cowboy).

-behaviour(cowboy_http_handler).
-behaviour(cowboy_websocket_handler).

%% common callbacks
-export([init/3]).

%% cowboy_http_handler callbacks
-export([handle/2,
terminate/3]).

%% cowboy_websocket_handler callbacks
-export([websocket_init/3,
websocket_handle/3,
websocket_info/3,
websocket_terminate/3]).

-record(state, {handler, handler_state, handler_opts}).

-type option() :: {atom(), any()}.
-type state() :: #state{}.

%%--------------------------------------------------------------------
%% common callback
%%--------------------------------------------------------------------
-spec init({atom(), http}, cowboy_req:req(), [option()])
-> {ok, cowboy_req:req(), state()} |
{shutdown, cowboy_req:req(), state()} |
{upgrade, protocol, cowboy_websocket, cowboy_req:req(), state()}.
init(Transport, Req, Opts) ->
case protocol(Req) of
{ws, Req1} ->
{upgrade, protocol, cowboy_websocket, Req1, Opts};
{http, Req1} ->
handle_http_init(Transport, Req1, Opts);
_ ->
{ok, Req1} = cowboy_req:reply(404, Req),
{shutdown, Req1, #state{}}
end.

%%--------------------------------------------------------------------
%% cowboy_http_handler callbacks
%%--------------------------------------------------------------------
-spec handle(cowboy_req:req(), state()) -> {ok, cowboy_req:req(), state()}.
handle(Req, #state{handler=Handler, handler_state=HandlerState}=State) ->
{ok, Req1, HandlerState1} = Handler:handle(Req, HandlerState),
{ok, Req1, update_handler_state(State, HandlerState1)}.

-spec terminate(any(), cowboy_req:req(), state()) -> ok.
terminate(_Reason, _Req, #state{handler=undefined}) ->
ok;
terminate(Reason, Req, #state{handler=Handler, handler_state=HandlerState}) ->
Handler:terminate(Reason, Req, HandlerState).

%%--------------------------------------------------------------------
%% cowboy_websocket_handler callbacks
%%--------------------------------------------------------------------
websocket_init(Transport, Req, Opts) ->
handle_ws_init(Transport, Req, Opts).

websocket_handle(InFrame, Req,
#state{handler=Handler, handler_state=HandlerState}=State) ->
case Handler:websocket_handle(InFrame, Req, HandlerState) of
{ok, Req1, HandlerState1} ->
{ok, Req1, update_handler_state(State, HandlerState1)};
{ok, Req1, HandlerState1, hibernate} ->
{ok, Req1, update_handler_state(State, HandlerState1), hibernate};
{reply, OutFrame, Req1, HandlerState1} ->
{reply, OutFrame, Req1, update_handler_state(State, HandlerState1)};
{reply, OutFrame, Req1, HandlerState1, hibernate} ->
{reply, OutFrame, Req1, update_handler_state(State, HandlerState1),
hibernate};
{shutdown, Req1, HandlerState1} ->
{shutdown, Req1, update_handler_state(State, HandlerState1)}
end.

websocket_info(Info, Req,
#state{handler=Handler, handler_state=HandlerState}=State) ->
case Handler:websocket_info(Info, Req, HandlerState) of
{ok, Req1, HandlerState1} ->
{ok, Req1, update_handler_state(State, HandlerState1)};
{ok, Req1, HandlerState1, hibernate} ->
{ok, Req1, update_handler_state(State, HandlerState1), hibernate};
{reply, OutFrame, Req1, HandlerState1} ->
{reply, OutFrame, Req1, update_handler_state(State, HandlerState1)};
{reply, OutFrame, Req1, HandlerState1, hibernate} ->
{reply, OutFrame, Req1, update_handler_state(State, HandlerState1),
hibernate};
{shutdown, Req1, HandlerState1} ->
{shutdown, Req1, update_handler_state(State, HandlerState1)}
end.

websocket_terminate(_Reason, _Req, #state{handler=undefined}) ->
ok;
websocket_terminate(Reason, Req,
#state{handler=Handler, handler_state=HandlerState}) ->
Handler:websocket_terminate(Reason, Req, HandlerState).

%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
handle_http_init(Transport, Req, Opts) ->
case http_handler(Opts) of
{Handler, HandlerOpts} ->
init_http_handler(Handler, Transport, Req, HandlerOpts);
_ ->
{ok, Req1} = cowboy_req:reply(404, Req),
{shutdown, Req1, #state{}}
end.

handle_ws_init(Transport, Req, Opts) ->
{Protocol, Req1} = ws_protocol(Req),
case ws_handler(Protocol, Opts) of
{Handler, HandlerOpts} ->
init_ws_handler(Handler, Transport, Req1, HandlerOpts);
_ ->
{ok, Req2} = cowboy_req:reply(404, Req1),
{shutdown, Req2}
end.

init_http_handler(Handler, Transport, Req, Opts) ->
case Handler:init(Transport, Req, Opts) of
{ok, Req1, HandlerState} ->
{ok, Req1, init_state(Handler, Opts, HandlerState)};
{shutdown, Req1, HandlerState} ->
{shutdown, Req1, init_state(Handler, Opts, HandlerState)}
end.

init_ws_handler(Handler, Transport, Req, Opts) ->
case Handler:websocket_init(Transport, Req, Opts) of
{ok, Req1, HandlerState} ->
{ok, Req1, init_state(Handler, Opts, HandlerState)};
{ok, Req1, HandlerState, hibernate} ->
{ok, Req1, init_state(Handler, Opts, HandlerState), hibernate};
{ok, Req1, HandlerState, Timeout} ->
{ok, Req1, init_state(Handler, Opts, HandlerState), Timeout};
{ok, Req1, HandlerState, Timeout, hibernate} ->
{ok, Req1, init_state(Handler, Opts, HandlerState),
Timeout, hibernate};
{shutdown, Req1} ->
{shutdown, Req1}
end.

http_handler(Handlers) ->
case lists:keyfind(http, 1, Handlers) of
{http, Handler, Opts} ->
{Handler, Opts};
{http, Handler} ->
{Handler, []};
_ ->
undefined
end.

ws_handler(undefined, _) ->
undefined;
ws_handler(_Protocol, []) ->
undefined;
ws_handler(Protocol, [{ws, ProtocolAtom, Handler}|Tail]) ->
case atom_to_binary(ProtocolAtom, utf8) of
Protocol -> {Handler, []};
_ -> ws_handler(Protocol, Tail)
end;
ws_handler(Protocol, [{ws, ProtocolAtom, Handler, Opts}|Tail]) ->
case atom_to_binary(ProtocolAtom, utf8) of
Protocol -> {Handler, Opts};
_ -> ws_handler(Protocol, Tail)
end;
ws_handler(Protocol, [_|Tail]) ->
ws_handler(Protocol, Tail).

protocol(Req) ->
case cowboy_req:header(<<"upgrade">>, Req) of
{<<"websocket">>, Req1} ->
{ws, Req1};
{undefined, Req1} ->
{http, Req1};
{_, Req1} ->
{undefined, Req1}
end.

ws_protocol(Req) ->
cowboy_req:header(<<"sec-websocket-protocol">>, Req).

init_state(Handler, Opts, State) ->
#state{handler = Handler,
handler_opts = Opts,
handler_state = State}.

update_handler_state(State, HandlerState) ->
State#state{handler_state = HandlerState}.
2 changes: 1 addition & 1 deletion apps/ejabberd/test/auth_http_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ get_password(_Config) ->
end,
false = ejabberd_auth_http:get_password(<<"anakin">>, ?DOMAIN1),
<<>> = ejabberd_auth_http:get_password_s(<<"anakin">>, ?DOMAIN1).

is_user_exists(_Config) ->
true = ejabberd_auth_http:does_user_exist(<<"alice">>, ?DOMAIN1),
false = ejabberd_auth_http:does_user_exist(<<"madhatter">>, ?DOMAIN1).
Expand Down
Loading