%%%---------------------------------------------------------------------- %%% File : mod_disco.erl %%% Author : Alexey Shchepin %%% Purpose : Service Discovery (JEP-0030) support %%% Created : 1 Jan 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2008 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., 59 Temple Place, Suite 330, Boston, MA %%% 02111-1307 USA %%% %%%---------------------------------------------------------------------- -module(mod_disco). -author('alexey@process-one.net'). -behaviour(gen_mod). -export([start/2, stop/1, process_local_iq_items/3, process_local_iq_info/3, get_local_identity/5, get_local_features/5, get_local_services/5, process_sm_iq_items/3, process_sm_iq_info/3, get_sm_identity/5, get_sm_features/5, get_sm_items/5, get_publish_items/5, register_feature/2, unregister_feature/2, register_extra_domain/2, unregister_extra_domain/2]). -include_lib("exmpp/include/exmpp.hrl"). -include("ejabberd.hrl"). -record(disco_publish, {owner_node, jid, name, node}). start(Host, Opts) -> mnesia:create_table(disco_publish, [{disc_only_copies, [node()]}, {attributes, record_info(fields, disco_publish)}, {type, bag}]), mnesia:add_table_index(disco_publish, owner_node), ejabberd_local:refresh_iq_handlers(), IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS, ?MODULE, process_local_iq_items, IQDisc), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO, ?MODULE, process_local_iq_info, IQDisc), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_DISCO_ITEMS, ?MODULE, process_sm_iq_items, IQDisc), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_DISCO_INFO, ?MODULE, process_sm_iq_info, IQDisc), catch ets:new(disco_features, [named_table, ordered_set, public]), register_feature(Host, "iq"), register_feature(Host, "presence"), register_feature(Host, "presence-invisible"), register_feature(Host, "http://jabber.org/protocol/disco#publish"), catch ets:new(disco_extra_domains, [named_table, ordered_set, public]), ExtraDomains = gen_mod:get_opt(extra_domains, Opts, []), lists:foreach(fun(Domain) -> register_extra_domain(Host, Domain) end, ExtraDomains), catch ets:new(disco_sm_features, [named_table, ordered_set, public]), catch ets:new(disco_sm_nodes, [named_table, ordered_set, public]), ejabberd_hooks:add(disco_local_items, Host, ?MODULE, get_local_services, 100), ejabberd_hooks:add(disco_local_features, Host, ?MODULE, get_local_features, 100), ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, get_local_identity, 100), ejabberd_hooks:add(disco_sm_items, Host, ?MODULE, get_sm_items, 100), ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, get_sm_features, 100), ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE, get_sm_identity, 100), ejabberd_hooks:add(disco_sm_items, Host, ?MODULE, get_publish_items, 75), ok. stop(Host) -> ejabberd_hooks:delete(disco_sm_items, Host, ?MODULE, get_publish_items, 75), ejabberd_hooks:delete(disco_sm_identity, Host, ?MODULE, get_sm_identity, 100), ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 100), ejabberd_hooks:delete(disco_sm_items, Host, ?MODULE, get_sm_items, 100), ejabberd_hooks:delete(disco_local_identity, Host, ?MODULE, get_local_identity, 100), ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, get_local_features, 100), ejabberd_hooks:delete(disco_local_items, Host, ?MODULE, get_local_services, 100), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_DISCO_ITEMS), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_DISCO_INFO), catch ets:match_delete(disco_features, {{'_', Host}}), catch ets:match_delete(disco_extra_domains, {{'_', Host}}), ok. register_feature(Host, Feature) -> catch ets:new(disco_features, [named_table, ordered_set, public]), ets:insert(disco_features, {{Feature, Host}}). unregister_feature(Host, Feature) -> catch ets:new(disco_features, [named_table, ordered_set, public]), ets:delete(disco_features, {Feature, Host}). register_extra_domain(Host, Domain) -> catch ets:new(disco_extra_domains, [named_table, ordered_set, public]), ets:insert(disco_extra_domains, {{Domain, Host}}). unregister_extra_domain(Host, Domain) -> catch ets:new(disco_extra_domains, [named_table, ordered_set, public]), ets:delete(disco_extra_domains, {Domain, Host}). process_local_iq_items(From, To, #iq{type = get, payload = SubEl, lang = Lang} = IQ_Rec) -> Host = To#jid.ldomain, Node = exmpp_xml:get_attribute(SubEl, 'node', ""), % XXX OLD FORMAT: From, To. FromOld = jlib:to_old_jid(From), ToOld = jlib:to_old_jid(To), case ejabberd_hooks:run_fold(disco_local_items, Host, empty, [FromOld, ToOld, Node, Lang]) of {result, Items} -> % XXX OLD FORMAT: Items might be an #xmlelement. ANode = case Node of "" -> []; _ -> [#xmlattr{name = 'node', value = Node}] end, Result = #xmlel{ns = ?NS_DISCO_ITEMS, name = 'query', attrs = ANode, children = Items}, exmpp_iq:result(IQ_Rec, Result); {error, Error} -> % XXX OLD FORMAT: Error. exmpp_iq:error(IQ_Rec, Error) end; process_local_iq_items(_From, _To, #iq{type = set} = IQ_Rec) -> exmpp_iq:error(IQ_Rec, 'not-allowed'). process_local_iq_info(From, To, #iq{type = get, payload = SubEl, lang = Lang} = IQ_Rec) -> Host = To#jid.ldomain, Node = exmpp_xml:get_attribute(SubEl, 'node', ""), % XXX OLD FORMAT: From, To. FromOld = jlib:to_old_jid(From), ToOld = jlib:to_old_jid(To), % XXX OLD FORMAT: Identity might be an #xmlelement. Identity = ejabberd_hooks:run_fold(disco_local_identity, Host, [], [FromOld, ToOld, Node, Lang]), % XXX OLD FORMAT: From, To. case ejabberd_hooks:run_fold(disco_local_features, Host, empty, [FromOld, ToOld, Node, Lang]) of {result, Features} -> ANode = case Node of "" -> []; _ -> [#xmlattr{name = 'node', value = Node}] end, Result = #xmlel{ns = ?NS_DISCO_INFO, name = 'query', attrs = ANode, children = Identity ++ lists:map(fun feature_to_xml/1, Features)}, exmpp_iq:result(IQ_Rec, Result); {error, Error} -> exmpp_iq:error(IQ_Rec, Error) end; process_local_iq_info(_From, _To, #iq{type = set} = IQ_Rec) -> exmpp_iq:error(IQ_Rec, 'not-allowed'). get_local_identity(Acc, _From, _To, [], _Lang) -> Acc ++ [#xmlel{ns = ?NS_DISCO_INFO, name = 'identity', attrs = [ #xmlattr{name = 'category', value = "server"}, #xmlattr{name = 'type', value = "im"}, #xmlattr{name = 'name', value = "ejabberd"} ]}]; get_local_identity(Acc, _From, _To, _Node, _Lang) -> Acc. get_local_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> Acc; get_local_features(Acc, _From, To, [], _Lang) -> Feats = case Acc of {result, Features} -> Features; empty -> [] end, Host = To#jid.ldomain, {result, ets:select(disco_features, [{{{'_', Host}}, [], ['$_']}]) ++ Feats}; get_local_features(Acc, _From, _To, _Node, _Lang) -> case Acc of {result, _Features} -> Acc; empty -> {error, 'item-not-found'} end. feature_to_xml({{Feature, _Host}}) -> feature_to_xml(Feature); feature_to_xml(Feature) when is_list(Feature) -> #xmlel{ns = ?NS_DISCO_INFO, name = 'feature', attrs = [ #xmlattr{name = 'var', value = Feature} ]}; feature_to_xml(Feature) when is_atom(Feature) -> #xmlel{ns = ?NS_DISCO_INFO, name = 'feature', attrs = [ #xmlattr{name = 'var', value = atom_to_list(Feature)} ]}. domain_to_xml({Domain}) -> #xmlel{ns = ?NS_DISCO_ITEMS, name = 'item', attrs = [ #xmlattr{name = 'jid', value = Domain} ]}; domain_to_xml(Domain) -> #xmlel{ns = ?NS_DISCO_ITEMS, name = 'item', attrs = [ #xmlattr{name = 'jid', value = Domain} ]}. get_local_services({error, _Error} = Acc, _From, _To, _Node, _Lang) -> Acc; get_local_services(Acc, _From, To, [], _Lang) -> % XXX OLD FORMAT: Items might be an #xmlelement. Items = case Acc of {result, Its} -> Its; empty -> [] end, Host = To#jid.ldomain, {result, lists:usort( lists:map(fun domain_to_xml/1, get_vh_services(Host) ++ ets:select(disco_extra_domains, [{{{'$1', Host}}, [], ['$1']}])) ) ++ Items}; get_local_services({result, _} = Acc, _From, _To, _Node, _Lang) -> Acc; get_local_services(empty, _From, _To, _Node, _Lang) -> {error, 'item-not-found'}. get_vh_services(Host) -> Hosts = lists:sort(fun(H1, H2) -> length(H1) >= length(H2) end, ?MYHOSTS), lists:filter(fun(H) -> case lists:dropwhile( fun(VH) -> not lists:suffix("." ++ VH, H) end, Hosts) of [] -> false; [VH | _] -> VH == Host end end, ejabberd_router:dirty_get_all_routes()). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% process_sm_iq_items(From, To, #iq{type = get, payload = SubEl, lang = Lang} = IQ_Rec) -> Host = To#jid.ldomain, Node = exmpp_xml:get_attribute(SubEl, 'node', ""), % XXX OLD FORMAT: From, To. FromOld = jlib:to_old_jid(From), ToOld = jlib:to_old_jid(To), case ejabberd_hooks:run_fold(disco_sm_items, Host, empty, [FromOld, ToOld, Node, Lang]) of {result, Items} -> ANode = case Node of "" -> []; _ -> [#xmlattr{name = 'node', value = Node}] end, Result = #xmlel{ns = ?NS_DISCO_ITEMS, name = 'query', attrs = ANode, children = Items}, exmpp_iq:result(IQ_Rec, Result); {error, Error} -> exmpp_iq:error(IQ_Rec, Error) end; process_sm_iq_items(From, To, #iq{type = set, payload = SubEl} = IQ_Rec) -> #jid{lnode = LTo, ldomain = ToServer} = To, #jid{lnode = LFrom, ldomain = LServer} = From, Self = (LTo == LFrom) andalso (ToServer == LServer), Node = exmpp_xml:get_attribute(SubEl, 'node', ""), if Self, Node /= [] -> %% Here, we treat disco publish attempts to your own JID. Items = SubEl#xmlel.children, case process_disco_publish({LFrom, LServer}, Node, Items) of ok -> exmpp_iq:result(IQ_Rec); {error, Err} -> exmpp_iq:error(IQ_Rec, Err) end; true -> exmpp_iq:error(IQ_Rec, 'not-allowed') end. get_sm_items({error, _Error} = Acc, _From, _To, _Node, _Lang) -> Acc; get_sm_items(Acc, #jid{lnode = LFrom, ldomain = LSFrom}, #jid{node = User, domain = Server, lnode = LTo, ldomain = LSTo} = _To, [], _Lang) -> Items = case Acc of {result, Its} -> Its; empty -> [] end, Items1 = case {LFrom, LSFrom} of {LTo, LSTo} -> get_user_resources(User, Server); _ -> [] end, {result, Items ++ Items1}; get_sm_items({result, _} = Acc, _From, _To, _Node, _Lang) -> Acc; get_sm_items(empty, From, To, _Node, _Lang) -> #jid{lnode = LFrom, ldomain = LSFrom} = From, #jid{lnode = LTo, ldomain = LSTo} = To, case {LFrom, LSFrom} of {LTo, LSTo} -> {error, 'item-not-found'}; _ -> {error, 'not-allowed'} end. process_sm_iq_info(From, To, #iq{type = get, payload = SubEl, lang = Lang} = IQ_Rec) -> Host = To#jid.ldomain, Node = exmpp_xml:get_attribute(SubEl, 'node', ""), % XXX OLD FORMAT: From, To. FromOld = jlib:to_old_jid(From), ToOld = jlib:to_old_jid(To), % XXX OLD FORMAT: Identity might be an #xmlelement. Identity = ejabberd_hooks:run_fold(disco_sm_identity, Host, [], [FromOld, ToOld, Node, Lang]), case ejabberd_hooks:run_fold(disco_sm_features, Host, empty, [FromOld, ToOld, Node, Lang]) of {result, Features} -> ANode = case Node of "" -> []; _ -> [#xmlattr{name = 'node', value = Node}] end, Result = #xmlel{ns = ?NS_DISCO_INFO, name = 'query', attrs = ANode, children = Identity ++ lists:map(fun feature_to_xml/1, Features)}, exmpp_iq:result(IQ_Rec, Result); {error, Error} -> exmpp_iq:error(IQ_Rec, Error) end; process_sm_iq_info(_From, _To, #iq{type = set} = IQ_Rec) -> exmpp_iq:error(IQ_Rec, 'not-allowed'). get_sm_identity(Acc, _From, _To, _Node, _Lang) -> Acc. get_sm_features(empty, From, To, _Node, _Lang) -> #jid{lnode = LFrom, ldomain = LSFrom} = From, #jid{lnode = LTo, ldomain = LSTo} = To, case {LFrom, LSFrom} of {LTo, LSTo} -> {error, 'item-not-found'}; _ -> {error, 'not-allowed'} end; get_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. get_user_resources(User, Server) -> Rs = ejabberd_sm:get_user_resources(User, Server), lists:map(fun(R) -> #xmlel{ns = ?NS_DISCO_ITEMS, name = 'item', attrs = [ #xmlattr{name = 'jid', value = exmpp_jid:jid_to_list(User, Server, R)}, #xmlattr{name = 'name', value = User} ]} end, lists:sort(Rs)). get_publish_items(empty, #jid{lnode = LFrom, ldomain = LSFrom}, #jid{lnode = LTo, ldomain = LSTo} = _To, Node, _Lang) -> if (LFrom == LTo) and (LSFrom == LSTo) -> retrieve_disco_publish({LTo, LSTo}, Node); true -> empty end; get_publish_items(Acc, _From, _To, _Node, _Lang) -> Acc. process_disco_publish(User, Node, Items) -> F = fun() -> lists:foreach( fun(#xmlel{} = Item) -> Action = exmpp_xml:get_attribute(Item, 'action', ""), Jid = exmpp_xml:get_attribute(Item, 'jid', ""), PNode = exmpp_xml:get_attribute(Item, 'node', ""), Name = exmpp_xml:get_attribute(Item, 'name', ""), ?INFO_MSG("Disco publish: ~p ~p ~p ~p ~p ~p~n", [User, Action, Node, Jid, PNode, Name]), %% The disco_publish table isn't strictly a "bag" table, as %% entries with same jid and node combination are considered %% the same, even if they have different names. Therefore, %% we find a list of items to supersede. SupersededItems = mnesia:match_object( #disco_publish{owner_node = {User, Node}, jid = Jid, node = PNode, _ = '_'}), case Action of "update" -> lists:map( fun(O) -> mnesia:delete_object(O) end, SupersededItems), mnesia:write( #disco_publish{owner_node = {User, Node}, jid = Jid, name = Name, node = PNode}); "remove" -> case SupersededItems of [] -> mnesia:abort({error, 'item-not-found'}); _ -> lists:map( fun(O) -> mnesia:delete_object(O) end, SupersededItems) end; _ -> %% invalid "action" attribute - return an error mnesia:abort({error, 'bad-request'}) end; (#xmlcdata{}) -> ok end, Items) end, case mnesia:transaction(F) of {aborted, {error, _} = Error} -> Error; {atomic, _} -> ok; _ -> {error, 'internal-server-error'} end. retrieve_disco_publish(User, Node) -> case catch mnesia:dirty_read({disco_publish, {User, Node}}) of {'EXIT', _Reason} -> {error, 'internal-server-error'}; [] -> empty; Items -> {result, lists:map( fun(#disco_publish{jid = Jid, name = Name, node = PNode}) -> #xmlel{ns = ?NS_DISCO_ITEMS, name = 'item', attrs = lists:append([[#xmlattr{name = 'jid', value = Jid}], case Name of "" -> []; _ -> [#xmlattr{name = 'name', value = Name}] end, case PNode of "" -> []; _ -> [#xmlattr{name = 'node', value = PNode}] end])} end, Items)} end.