diff options
Diffstat (limited to 'src/mod_delegation.erl')
-rw-r--r-- | src/mod_delegation.erl | 380 |
1 files changed, 380 insertions, 0 deletions
diff --git a/src/mod_delegation.erl b/src/mod_delegation.erl new file mode 100644 index 000000000..508cb84b6 --- /dev/null +++ b/src/mod_delegation.erl @@ -0,0 +1,380 @@ +%%%------------------------------------------------------------------- +%%% File : mod_delegation.erl +%%% Author : Anna Mukharram <amuhar3@gmail.com> +%%% Purpose : XEP-0355: Namespace Delegation +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%------------------------------------------------------------------- +-module(mod_delegation). + +-author('amuhar3@gmail.com'). + +-protocol({xep, 0355, '0.4.1'}). + +-behaviour(gen_server). +-behaviour(gen_mod). + +%% API +-export([start/2, stop/1, reload/3, mod_opt_type/1, depends/2, mod_options/1]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). +-export([component_connected/1, component_disconnected/2, + ejabberd_local/1, ejabberd_sm/1, decode_iq_subel/1, + disco_local_features/5, disco_sm_features/5, + disco_local_identity/5, disco_sm_identity/5]). + +-include("logger.hrl"). +-include("xmpp.hrl"). +-include("translate.hrl"). + +-type route_type() :: ejabberd_sm | ejabberd_local. +-type delegations() :: #{{binary(), route_type()} => {binary(), disco_info()}}. +-record(state, {server_host = <<"">> :: binary()}). + +%%%=================================================================== +%%% API +%%%=================================================================== +start(Host, Opts) -> + gen_mod:start_child(?MODULE, Host, Opts). + +stop(Host) -> + gen_mod:stop_child(?MODULE, Host). + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +mod_opt_type(namespaces) -> + econf:and_then( + econf:map( + econf:binary(), + econf:options( + #{filtering => econf:list(econf:binary()), + access => econf:acl()})), + fun(L) -> + lists:map( + fun({NS, Opts}) -> + Attrs = proplists:get_value(filtering, Opts, []), + Access = proplists:get_value(access, Opts, none), + {NS, Attrs, Access} + end, L) + end). + +-spec mod_options(binary()) -> [{namespaces, + [{binary(), [binary()], acl:acl()}]} | + {atom(), term()}]. +mod_options(_Host) -> + [{namespaces, []}]. + +depends(_, _) -> + []. + +-spec decode_iq_subel(xmpp_element() | xmlel()) -> xmpp_element() | xmlel(). +%% Tell gen_iq_handler not to auto-decode IQ payload +decode_iq_subel(El) -> + El. + +-spec component_connected(binary()) -> ok. +component_connected(Host) -> + lists:foreach( + fun(ServerHost) -> + Proc = gen_mod:get_module_proc(ServerHost, ?MODULE), + gen_server:cast(Proc, {component_connected, Host}) + end, ejabberd_option:hosts()). + +-spec component_disconnected(binary(), binary()) -> ok. +component_disconnected(Host, _Reason) -> + lists:foreach( + fun(ServerHost) -> + Proc = gen_mod:get_module_proc(ServerHost, ?MODULE), + gen_server:cast(Proc, {component_disconnected, Host}) + end, ejabberd_option:hosts()). + +-spec ejabberd_local(iq()) -> iq(). +ejabberd_local(IQ) -> + process_iq(IQ, ejabberd_local). + +-spec ejabberd_sm(iq()) -> iq(). +ejabberd_sm(IQ) -> + process_iq(IQ, ejabberd_sm). + +-spec disco_local_features(mod_disco:features_acc(), jid(), jid(), + binary(), binary()) -> mod_disco:features_acc(). +disco_local_features(Acc, From, To, Node, Lang) -> + disco_features(Acc, From, To, Node, Lang, ejabberd_local). + +-spec disco_sm_features(mod_disco:features_acc(), jid(), jid(), + binary(), binary()) -> mod_disco:features_acc(). +disco_sm_features(Acc, From, To, Node, Lang) -> + disco_features(Acc, From, To, Node, Lang, ejabberd_sm). + +-spec disco_local_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. +disco_local_identity(Acc, From, To, Node, Lang) -> + disco_identity(Acc, From, To, Node, Lang, ejabberd_local). + +-spec disco_sm_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. +disco_sm_identity(Acc, From, To, Node, Lang) -> + disco_identity(Acc, From, To, Node, Lang, ejabberd_sm). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +init([Host|_]) -> + process_flag(trap_exit, true), + catch ets:new(?MODULE, + [named_table, public, + {heir, erlang:group_leader(), none}]), + ejabberd_hooks:add(component_connected, ?MODULE, + component_connected, 50), + ejabberd_hooks:add(component_disconnected, ?MODULE, + component_disconnected, 50), + ejabberd_hooks:add(disco_local_features, Host, ?MODULE, + disco_local_features, 50), + ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, + disco_sm_features, 50), + ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, + disco_local_identity, 50), + ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE, + disco_sm_identity, 50), + {ok, #state{server_host = Host}}. + +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +handle_cast({component_connected, Host}, State) -> + ServerHost = State#state.server_host, + To = jid:make(Host), + NSAttrsAccessList = mod_delegation_opt:namespaces(ServerHost), + lists:foreach( + fun({NS, _Attrs, Access}) -> + case acl:match_rule(ServerHost, Access, To) of + allow -> + send_disco_queries(ServerHost, Host, NS); + deny -> + ?DEBUG("Denied delegation for ~ts on ~ts", [Host, NS]) + end + end, NSAttrsAccessList), + {noreply, State}; +handle_cast({component_disconnected, Host}, State) -> + ServerHost = State#state.server_host, + Delegations = + maps:filter( + fun({NS, Type}, {H, _}) when H == Host -> + ?INFO_MSG("Remove delegation of namespace '~ts' " + "from external component '~ts'", + [NS, Host]), + gen_iq_handler:remove_iq_handler(Type, ServerHost, NS), + false; + (_, _) -> + true + end, get_delegations(ServerHost)), + set_delegations(ServerHost, Delegations), + {noreply, State}; +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info({iq_reply, ResIQ, {disco_info, Type, Host, NS}}, State) -> + case ResIQ of + #iq{type = result, sub_els = [SubEl]} -> + try xmpp:decode(SubEl) of + #disco_info{} = Info -> + ServerHost = State#state.server_host, + process_disco_info(ServerHost, Type, Host, NS, Info) + catch _:{xmpp_codec, _} -> + ok + end; + _ -> + ok + end, + {noreply, State}; +handle_info({iq_reply, ResIQ, #iq{} = IQ}, State) -> + process_iq_result(IQ, ResIQ), + {noreply, State}; +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, State) -> + ServerHost = State#state.server_host, + case gen_mod:is_loaded_elsewhere(ServerHost, ?MODULE) of + false -> + ejabberd_hooks:delete(component_connected, ?MODULE, + component_connected, 50), + ejabberd_hooks:delete(component_disconnected, ?MODULE, + component_disconnected, 50); + true -> + ok + end, + ejabberd_hooks:delete(disco_local_features, ServerHost, ?MODULE, + disco_local_features, 50), + ejabberd_hooks:delete(disco_sm_features, ServerHost, ?MODULE, + disco_sm_features, 50), + ejabberd_hooks:delete(disco_local_identity, ServerHost, ?MODULE, + disco_local_identity, 50), + ejabberd_hooks:delete(disco_sm_identity, ServerHost, ?MODULE, + disco_sm_identity, 50), + lists:foreach( + fun({NS, Type}) -> + gen_iq_handler:remove_iq_handler(Type, ServerHost, NS) + end, maps:keys(get_delegations(ServerHost))), + ets:delete(?MODULE, ServerHost). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec get_delegations(binary()) -> delegations(). +get_delegations(Host) -> + try ets:lookup_element(?MODULE, Host, 2) + catch _:badarg -> #{} + end. + +-spec set_delegations(binary(), delegations()) -> true. +set_delegations(ServerHost, Delegations) -> + case maps:size(Delegations) of + 0 -> ets:delete(?MODULE, ServerHost); + _ -> ets:insert(?MODULE, {ServerHost, Delegations}) + end. + +-spec process_iq(iq(), route_type()) -> ignore | iq(). +process_iq(#iq{to = To, lang = Lang, sub_els = [SubEl]} = IQ, Type) -> + LServer = To#jid.lserver, + NS = xmpp:get_ns(SubEl), + Delegations = get_delegations(LServer), + case maps:find({NS, Type}, Delegations) of + {ok, {Host, _}} -> + Delegation = #delegation{ + forwarded = #forwarded{sub_els = [IQ]}}, + NewFrom = jid:make(LServer), + NewTo = jid:make(Host), + ejabberd_router:route_iq( + #iq{type = set, + from = NewFrom, + to = NewTo, + sub_els = [Delegation]}, + IQ, gen_mod:get_module_proc(LServer, ?MODULE)), + ignore; + error -> + Txt = ?T("Failed to map delegated namespace to external component"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + end. + +-spec process_iq_result(iq(), iq()) -> ok. +process_iq_result(#iq{from = From, to = To, id = ID, lang = Lang} = IQ, + #iq{type = result} = ResIQ) -> + try + CodecOpts = ejabberd_config:codec_options(), + #delegation{forwarded = #forwarded{sub_els = [SubEl]}} = + xmpp:get_subtag(ResIQ, #delegation{}), + case xmpp:decode(SubEl, ?NS_CLIENT, CodecOpts) of + #iq{from = To, to = From, type = Type, id = ID} = Reply + when Type == error; Type == result -> + ejabberd_router:route(Reply) + end + catch _:_ -> + ?ERROR_MSG("Got iq-result with invalid delegated " + "payload:~n~ts", [xmpp:pp(ResIQ)]), + Txt = ?T("External component failure"), + Err = xmpp:err_internal_server_error(Txt, Lang), + ejabberd_router:route_error(IQ, Err) + end; +process_iq_result(#iq{from = From, to = To}, #iq{type = error} = ResIQ) -> + Err = xmpp:set_from_to(ResIQ, To, From), + ejabberd_router:route(Err); +process_iq_result(#iq{lang = Lang} = IQ, timeout) -> + Txt = ?T("External component timeout"), + Err = xmpp:err_internal_server_error(Txt, Lang), + ejabberd_router:route_error(IQ, Err). + +-spec process_disco_info(binary(), route_type(), + binary(), binary(), disco_info()) -> ok. +process_disco_info(ServerHost, Type, Host, NS, Info) -> + From = jid:make(ServerHost), + To = jid:make(Host), + Delegations = get_delegations(ServerHost), + case maps:find({NS, Type}, Delegations) of + error -> + Msg = #message{from = From, to = To, + sub_els = [#delegation{delegated = [#delegated{ns = NS}]}]}, + Delegations1 = maps:put({NS, Type}, {Host, Info}, Delegations), + gen_iq_handler:add_iq_handler(Type, ServerHost, NS, ?MODULE, Type), + ejabberd_router:route(Msg), + set_delegations(ServerHost, Delegations1), + ?INFO_MSG("Namespace '~ts' is delegated to external component '~ts'", + [NS, Host]); + {ok, {AnotherHost, _}} -> + ?WARNING_MSG("Failed to delegate namespace '~ts' to " + "external component '~ts' because it's already " + "delegated to '~ts'", + [NS, Host, AnotherHost]) + end. + +-spec send_disco_queries(binary(), binary(), binary()) -> ok. +send_disco_queries(LServer, Host, NS) -> + From = jid:make(LServer), + To = jid:make(Host), + lists:foreach( + fun({Type, Node}) -> + ejabberd_router:route_iq( + #iq{type = get, from = From, to = To, + sub_els = [#disco_info{node = Node}]}, + {disco_info, Type, Host, NS}, + gen_mod:get_module_proc(LServer, ?MODULE)) + end, [{ejabberd_local, <<(?NS_DELEGATION)/binary, "::", NS/binary>>}, + {ejabberd_sm, <<(?NS_DELEGATION)/binary, ":bare:", NS/binary>>}]). + +-spec disco_features(mod_disco:features_acc(), jid(), jid(), binary(), binary(), + route_type()) -> mod_disco:features_acc(). +disco_features(Acc, _From, To, <<"">>, _Lang, Type) -> + Delegations = get_delegations(To#jid.lserver), + Features = my_features(Type) ++ + lists:flatmap( + fun({{_, T}, {_, Info}}) when T == Type -> + Info#disco_info.features; + (_) -> + [] + end, maps:to_list(Delegations)), + case Acc of + empty when Features /= [] -> {result, Features}; + {result, Fs} -> {result, Fs ++ Features}; + _ -> Acc + end; +disco_features(Acc, _, _, _, _, _) -> + Acc. + +-spec disco_identity([identity()], jid(), jid(), binary(), binary(), + route_type()) -> [identity()]. +disco_identity(Acc, _From, To, <<"">>, _Lang, Type) -> + Delegations = get_delegations(To#jid.lserver), + Identities = lists:flatmap( + fun({{_, T}, {_, Info}}) when T == Type -> + Info#disco_info.identities; + (_) -> + [] + end, maps:to_list(Delegations)), + Acc ++ Identities; +disco_identity(Acc, _From, _To, _Node, _Lang, _Type) -> + Acc. + +my_features(ejabberd_local) -> [?NS_DELEGATION]; +my_features(ejabberd_sm) -> []. |