Packages
ejabberd
20.3.0
26.4.0
26.3.0
26.2.0
26.1.0
25.10.0
25.8.0
25.7.0
25.4.0
25.3.0
24.12.0
24.10.0
24.7.0
24.6.0
24.2.6
23.10.0
23.4.0
23.1.0
22.10.0
22.5.0
21.12.0
21.7.0
21.4.0
21.1.0
20.12.0
20.7.0
20.4.0
20.3.0
20.2.0
20.1.0
19.9.1
19.9.0
19.8.0
19.5.0
19.2.0
18.12.1
18.12.0
18.6.0
18.4.0
18.3.0
18.1.0
17.11.0
17.9.0
17.6.0
17.3.0
17.1.0
16.12.0-beta1
16.9.0
16.8.0
16.6.2
16.6.1
16.6.0
16.4.1
16.4.0
16.3.0
16.2.0
16.1.0-beta1
Robust, Ubiquitous and Massively Scalable Messaging Platform (XMPP, MQTT, SIP Server)
Current section
Files
Jump to
Current section
Files
src/mod_disco.erl
%%%----------------------------------------------------------------------
%%% File : mod_disco.erl
%%% Author : Alexey Shchepin <alexey@process-one.net>
%%% Purpose : Service Discovery (XEP-0030) support
%%% Created : 1 Jan 2003 by Alexey Shchepin <alexey@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2020 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_disco).
-author('alexey@process-one.net').
-protocol({xep, 30, '2.4'}).
-protocol({xep, 157, '1.0'}).
-behaviour(gen_mod).
-export([start/2, stop/1, reload/3, process_local_iq_items/1,
process_local_iq_info/1, get_local_identity/5,
get_local_features/5, get_local_services/5,
process_sm_iq_items/1, process_sm_iq_info/1,
get_sm_identity/5, get_sm_features/5, get_sm_items/5,
get_info/5, mod_opt_type/1, mod_options/1, depends/2,
mod_doc/0]).
-include("logger.hrl").
-include("translate.hrl").
-include("xmpp.hrl").
-include_lib("stdlib/include/ms_transform.hrl").
-include("mod_roster.hrl").
-type features_acc() :: {error, stanza_error()} | {result, [binary()]} | empty.
-type items_acc() :: {error, stanza_error()} | {result, [disco_item()]} | empty.
-export_type([features_acc/0, items_acc/0]).
start(Host, Opts) ->
gen_iq_handler:add_iq_handler(ejabberd_local, Host,
?NS_DISCO_ITEMS, ?MODULE,
process_local_iq_items),
gen_iq_handler:add_iq_handler(ejabberd_local, Host,
?NS_DISCO_INFO, ?MODULE,
process_local_iq_info),
gen_iq_handler:add_iq_handler(ejabberd_sm, Host,
?NS_DISCO_ITEMS, ?MODULE, process_sm_iq_items),
gen_iq_handler:add_iq_handler(ejabberd_sm, Host,
?NS_DISCO_INFO, ?MODULE, process_sm_iq_info),
catch ets:new(disco_extra_domains,
[named_table, ordered_set, public,
{heir, erlang:group_leader(), none}]),
ExtraDomains = mod_disco_opt:extra_domains(Opts),
lists:foreach(fun (Domain) ->
register_extra_domain(Host, Domain)
end,
ExtraDomains),
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_info, Host, ?MODULE, get_info,
100),
ok.
stop(Host) ->
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),
ejabberd_hooks:delete(disco_info, Host, ?MODULE,
get_info, 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_extra_domains,
{{'_', Host}}),
ok.
reload(Host, NewOpts, OldOpts) ->
NewDomains = mod_disco_opt:extra_domains(NewOpts),
OldDomains = mod_disco_opt:extra_domains(OldOpts),
lists:foreach(
fun(Domain) ->
register_extra_domain(Host, Domain)
end, NewDomains -- OldDomains),
lists:foreach(
fun(Domain) ->
unregister_extra_domain(Host, Domain)
end, OldDomains -- NewDomains).
-spec register_extra_domain(binary(), binary()) -> true.
register_extra_domain(Host, Domain) ->
ets:insert(disco_extra_domains, {{Domain, Host}}).
-spec unregister_extra_domain(binary(), binary()) -> true.
unregister_extra_domain(Host, Domain) ->
ets:delete_object(disco_extra_domains, {{Domain, Host}}).
-spec process_local_iq_items(iq()) -> iq().
process_local_iq_items(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_local_iq_items(#iq{type = get, lang = Lang,
from = From, to = To,
sub_els = [#disco_items{node = Node}]} = IQ) ->
Host = To#jid.lserver,
case ejabberd_hooks:run_fold(disco_local_items, Host,
empty, [From, To, Node, Lang]) of
{result, Items} ->
xmpp:make_iq_result(IQ, #disco_items{node = Node, items = Items});
{error, Error} ->
xmpp:make_error(IQ, Error)
end.
-spec process_local_iq_info(iq()) -> iq().
process_local_iq_info(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_local_iq_info(#iq{type = get, lang = Lang,
from = From, to = To,
sub_els = [#disco_info{node = Node}]} = IQ) ->
Host = To#jid.lserver,
Identity = ejabberd_hooks:run_fold(disco_local_identity,
Host, [], [From, To, Node, Lang]),
Info = ejabberd_hooks:run_fold(disco_info, Host, [],
[Host, ?MODULE, Node, Lang]),
case ejabberd_hooks:run_fold(disco_local_features, Host,
empty, [From, To, Node, Lang]) of
{result, Features} ->
xmpp:make_iq_result(IQ, #disco_info{node = Node,
identities = Identity,
xdata = Info,
features = Features});
{error, Error} ->
xmpp:make_error(IQ, Error)
end.
-spec get_local_identity([identity()], jid(), jid(),
binary(), binary()) -> [identity()].
get_local_identity(Acc, _From, To, <<"">>, _Lang) ->
Host = To#jid.lserver,
Name = mod_disco_opt:name(Host),
Acc ++ [#identity{category = <<"server">>,
type = <<"im">>,
name = Name}];
get_local_identity(Acc, _From, _To, _Node, _Lang) ->
Acc.
-spec get_local_features(features_acc(), jid(), jid(), binary(), binary()) ->
{error, stanza_error()} | {result, [binary()]}.
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,
{result, lists:usort(
lists:flatten(
[?NS_FEATURE_IQ, ?NS_FEATURE_PRESENCE,
?NS_DISCO_INFO, ?NS_DISCO_ITEMS, Feats,
ejabberd_local:get_features(To#jid.lserver)]))};
get_local_features(Acc, _From, _To, _Node, Lang) ->
case Acc of
{result, _Features} -> Acc;
empty ->
Txt = ?T("No features available"),
{error, xmpp:err_item_not_found(Txt, Lang)}
end.
-spec get_local_services(items_acc(), jid(), jid(), binary(), binary()) ->
{error, stanza_error()} | {result, [disco_item()]}.
get_local_services({error, _Error} = Acc, _From, _To,
_Node, _Lang) ->
Acc;
get_local_services(Acc, _From, To, <<"">>, _Lang) ->
Items = case Acc of
{result, Its} -> Its;
empty -> []
end,
Host = To#jid.lserver,
{result,
lists:usort(
lists:map(
fun(Domain) -> #disco_item{jid = jid:make(Domain)} end,
get_vh_services(Host) ++
ets:select(disco_extra_domains,
ets:fun2ms(
fun({{D, H}}) when H == Host -> D end))))
++ Items};
get_local_services({result, _} = Acc, _From, _To, _Node,
_Lang) ->
Acc;
get_local_services(empty, _From, _To, _Node, Lang) ->
{error, xmpp:err_item_not_found(?T("No services available"), Lang)}.
-spec get_vh_services(binary()) -> [binary()].
get_vh_services(Host) ->
Hosts = lists:sort(fun (H1, H2) ->
byte_size(H1) >= byte_size(H2)
end,
ejabberd_option:hosts()),
lists:filter(fun (H) ->
case lists:dropwhile(fun (VH) ->
not
str:suffix(
<<".", VH/binary>>,
H)
end,
Hosts)
of
[] -> false;
[VH | _] -> VH == Host
end
end,
ejabberd_router:get_all_routes()).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec process_sm_iq_items(iq()) -> iq().
process_sm_iq_items(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_sm_iq_items(#iq{type = get, lang = Lang,
from = From, to = To,
sub_els = [#disco_items{node = Node}]} = IQ) ->
case mod_roster:is_subscribed(From, To) of
true ->
Host = To#jid.lserver,
case ejabberd_hooks:run_fold(disco_sm_items, Host,
empty, [From, To, Node, Lang]) of
{result, Items} ->
xmpp:make_iq_result(
IQ, #disco_items{node = Node, items = Items});
{error, Error} ->
xmpp:make_error(IQ, Error)
end;
false ->
Txt = ?T("Not subscribed"),
xmpp:make_error(IQ, xmpp:err_subscription_required(Txt, Lang))
end.
-spec get_sm_items(items_acc(), jid(), jid(), binary(), binary()) ->
{error, stanza_error()} | {result, [disco_item()]}.
get_sm_items({error, _Error} = Acc, _From, _To, _Node,
_Lang) ->
Acc;
get_sm_items(Acc, From,
#jid{user = User, server = Server} = To, <<"">>, _Lang) ->
Items = case Acc of
{result, Its} -> Its;
empty -> []
end,
Items1 = case mod_roster:is_subscribed(From, To) of
true -> 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{luser = LFrom, lserver = LSFrom} = From,
#jid{luser = LTo, lserver = LSTo} = To,
case {LFrom, LSFrom} of
{LTo, LSTo} -> {error, xmpp:err_item_not_found()};
_ ->
Txt = ?T("Query to another users is forbidden"),
{error, xmpp:err_not_allowed(Txt, Lang)}
end.
-spec process_sm_iq_info(iq()) -> iq().
process_sm_iq_info(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_sm_iq_info(#iq{type = get, lang = Lang,
from = From, to = To,
sub_els = [#disco_info{node = Node}]} = IQ) ->
case mod_roster:is_subscribed(From, To) of
true ->
Host = To#jid.lserver,
Identity = ejabberd_hooks:run_fold(disco_sm_identity,
Host, [],
[From, To, Node, Lang]),
Info = ejabberd_hooks:run_fold(disco_info, Host, [],
[From, To, Node, Lang]),
case ejabberd_hooks:run_fold(disco_sm_features, Host,
empty, [From, To, Node, Lang]) of
{result, Features} ->
xmpp:make_iq_result(IQ, #disco_info{node = Node,
identities = Identity,
xdata = Info,
features = Features});
{error, Error} ->
xmpp:make_error(IQ, Error)
end;
false ->
Txt = ?T("Not subscribed"),
xmpp:make_error(IQ, xmpp:err_subscription_required(Txt, Lang))
end.
-spec get_sm_identity([identity()], jid(), jid(),
binary(), binary()) -> [identity()].
get_sm_identity(Acc, _From,
#jid{luser = LUser, lserver = LServer}, _Node, _Lang) ->
Acc ++
case ejabberd_auth:user_exists(LUser, LServer) of
true ->
[#identity{category = <<"account">>, type = <<"registered">>}];
_ -> []
end.
-spec get_sm_features(features_acc(), jid(), jid(), binary(), binary()) ->
{error, stanza_error()} | {result, [binary()]}.
get_sm_features(empty, From, To, Node, Lang) ->
#jid{luser = LFrom, lserver = LSFrom} = From,
#jid{luser = LTo, lserver = LSTo} = To,
case {LFrom, LSFrom} of
{LTo, LSTo} ->
case Node of
<<"">> -> {result, [?NS_DISCO_INFO, ?NS_DISCO_ITEMS]};
_ -> {error, xmpp:err_item_not_found()}
end;
_ ->
Txt = ?T("Query to another users is forbidden"),
{error, xmpp:err_not_allowed(Txt, Lang)}
end;
get_sm_features({result, Features}, _From, _To, <<"">>, _Lang) ->
{result, [?NS_DISCO_INFO, ?NS_DISCO_ITEMS|Features]};
get_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc.
-spec get_user_resources(binary(), binary()) -> [disco_item()].
get_user_resources(User, Server) ->
Rs = ejabberd_sm:get_user_resources(User, Server),
[#disco_item{jid = jid:make(User, Server, Resource), name = User}
|| Resource <- lists:sort(Rs)].
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Support for: XEP-0157 Contact Addresses for XMPP Services
-spec get_info([xdata()], binary(), module(), binary(), binary()) -> [xdata()];
([xdata()], jid(), jid(), binary(), binary()) -> [xdata()].
get_info(_A, Host, Mod, Node, _Lang) when is_atom(Mod), Node == <<"">> ->
Module = case Mod of
undefined -> ?MODULE;
_ -> Mod
end,
[#xdata{type = result,
fields = [#xdata_field{type = hidden,
var = <<"FORM_TYPE">>,
values = [?NS_SERVERINFO]}
| get_fields(Host, Module)]}];
get_info(Acc, _, _, _Node, _) -> Acc.
-spec get_fields(binary(), module()) -> [xdata_field()].
get_fields(Host, Module) ->
Fields = mod_disco_opt:server_info(Host),
Fields1 = lists:filter(fun ({Modules, _, _}) ->
case Modules of
all -> true;
Modules ->
lists:member(Module, Modules)
end
end,
Fields),
[#xdata_field{var = Var,
type = 'list-multi',
values = Values} || {_, Var, Values} <- Fields1].
-spec depends(binary(), gen_mod:opts()) -> [].
depends(_Host, _Opts) ->
[].
mod_opt_type(extra_domains) ->
econf:list(econf:binary());
mod_opt_type(name) ->
econf:binary();
mod_opt_type(server_info) ->
econf:list(
econf:and_then(
econf:options(
#{name => econf:binary(),
urls => econf:list(econf:binary()),
modules =>
econf:either(
all,
econf:list(econf:beam()))}),
fun(Opts) ->
Mods = proplists:get_value(modules, Opts, all),
Name = proplists:get_value(name, Opts, <<>>),
URLs = proplists:get_value(urls, Opts, []),
{Mods, Name, URLs}
end)).
-spec mod_options(binary()) -> [{server_info,
[{all | [module()], binary(), [binary()]}]} |
{atom(), any()}].
mod_options(_Host) ->
[{extra_domains, []},
{server_info, []},
{name, ?T("ejabberd")}].
mod_doc() ->
#{desc =>
?T("This module adds support for "
"https://xmpp.org/extensions/xep-0030.html"
"[XEP-0030: Service Discovery]. With this module enabled, "
"services on your server can be discovered by XMPP clients."),
opts =>
[{extra_domains,
#{value => "[Domain, ...]",
desc =>
?T("With this option, you can specify a list of extra "
"domains that are added to the Service Discovery item list. "
"The default value is an empty list.")}},
{name,
#{value => ?T("Name"),
desc =>
?T("A name of the server in the Service Discovery. "
"This will only be displayed by special XMPP clients. "
"The default value is 'ejabberd'.")}},
{server_info,
#{value => "[Info, ...]",
example =>
["server_info:",
" -",
" modules: all",
" name: abuse-addresses",
" urls: [mailto:abuse@shakespeare.lit]",
" -",
" modules: [mod_muc]",
" name: \"Web chatroom logs\"",
" urls: [http://www.example.org/muc-logs]",
" -",
" modules: [mod_disco]",
" name: feedback-addresses",
" urls:",
" - http://shakespeare.lit/feedback.php",
" - mailto:feedback@shakespeare.lit",
" - xmpp:feedback@shakespeare.lit",
" -",
" modules:",
" - mod_disco",
" - mod_vcard",
" name: admin-addresses",
" urls:",
" - mailto:xmpp@shakespeare.lit",
" - xmpp:admins@shakespeare.lit"],
desc =>
?T("Specify additional information about the server, "
"as described in https://xmpp.org/extensions/xep-0157.html"
"[XEP-0157: Contact Addresses for XMPP Services]. Every 'Info' "
"element in the list is constructed from the following options:")},
[{modules,
#{value => "all | [Module, ...]",
desc =>
?T("The value can be the keyword 'all', in which case the "
"information is reported in all the services, "
"or a list of ejabberd modules, in which case the "
"information is only specified for the services provided "
"by those modules.")}},
{name,
#{value => ?T("Name"),
desc => ?T("Any arbitrary name of the contact.")}},
{urls,
#{value => "[URI, ...]",
desc => ?T("A list of contact URIs, such as "
"HTTP URLs, XMPP URIs and so on.")}}]}]}.