• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

processone / ejabberd / 1258

12 Dec 2025 03:57PM UTC coverage: 33.638% (-0.006%) from 33.644%
1258

push

github

badlop
Container: Apply commit a22c88a

ejabberdctl.template: Show meaningful error when ERL_DIST_PORT is in use

15554 of 46240 relevant lines covered (33.64%)

1078.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

48.85
/src/mod_disco.erl
1
%%%----------------------------------------------------------------------
2
%%% File    : mod_disco.erl
3
%%% Author  : Alexey Shchepin <alexey@process-one.net>
4
%%% Purpose : Service Discovery (XEP-0030) support
5
%%% Created :  1 Jan 2003 by Alexey Shchepin <alexey@process-one.net>
6
%%%
7
%%%
8
%%% ejabberd, Copyright (C) 2002-2025   ProcessOne
9
%%%
10
%%% This program is free software; you can redistribute it and/or
11
%%% modify it under the terms of the GNU General Public License as
12
%%% published by the Free Software Foundation; either version 2 of the
13
%%% License, or (at your option) any later version.
14
%%%
15
%%% This program is distributed in the hope that it will be useful,
16
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
17
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18
%%% General Public License for more details.
19
%%%
20
%%% You should have received a copy of the GNU General Public License along
21
%%% with this program; if not, write to the Free Software Foundation, Inc.,
22
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23
%%%
24
%%%----------------------------------------------------------------------
25

26
-module(mod_disco).
27

28
-author('alexey@process-one.net').
29

30
-protocol({xep, 30, '2.5.0', '0.1.0', "complete", ""}).
31
-protocol({xep, 157, '1.1.1', '2.1.0', "complete", ""}).
32

33
-behaviour(gen_mod).
34

35
-export([start/2, stop/1, reload/3, process_local_iq_items/1,
36
         process_local_iq_info/1, get_local_identity/5,
37
         get_local_features/5, get_local_services/5,
38
         process_sm_iq_items/1, process_sm_iq_info/1,
39
         get_sm_identity/5, get_sm_features/5, get_sm_items/5,
40
         get_info/5, mod_opt_type/1, mod_options/1, depends/2,
41
         mod_doc/0]).
42

43
-include("logger.hrl").
44
-include("translate.hrl").
45
-include_lib("xmpp/include/xmpp.hrl").
46
-include_lib("stdlib/include/ms_transform.hrl").
47
-include("mod_roster.hrl").
48

49
-type features_acc() :: {error, stanza_error()} | {result, [binary()]} | empty.
50
-type items_acc() :: {error, stanza_error()} | {result, [disco_item()]} | empty.
51
-export_type([features_acc/0, items_acc/0]).
52

53
start(Host, Opts) ->
54
    catch ets:new(disco_extra_domains,
99✔
55
                  [named_table, ordered_set, public,
56
                   {heir, erlang:group_leader(), none}]),
57
    ExtraDomains = mod_disco_opt:extra_domains(Opts),
99✔
58
    lists:foreach(fun (Domain) ->
99✔
59
                          register_extra_domain(Host, Domain)
×
60
                  end,
61
                  ExtraDomains),
62
    {ok, [{iq_handler, ejabberd_local, ?NS_DISCO_ITEMS, process_local_iq_items},
99✔
63
          {iq_handler, ejabberd_local, ?NS_DISCO_INFO, process_local_iq_info},
64
          {iq_handler, ejabberd_sm, ?NS_DISCO_ITEMS, process_sm_iq_items},
65
          {iq_handler, ejabberd_sm, ?NS_DISCO_INFO, process_sm_iq_info},
66
          {hook, disco_local_items, get_local_services, 100},
67
          {hook, disco_local_features, get_local_features, 100},
68
          {hook, disco_local_identity, get_local_identity, 100},
69
          {hook, disco_sm_items, get_sm_items, 100},
70
          {hook, disco_sm_features, get_sm_features, 100},
71
          {hook, disco_sm_identity, get_sm_identity, 100},
72
          {hook, disco_info, get_info, 100}]}.
73

74
stop(Host) ->
75
    catch ets:match_delete(disco_extra_domains,
99✔
76
                           {{'_', Host}}),
77
    ok.
99✔
78

79
reload(Host, NewOpts, OldOpts) ->
80
    NewDomains = mod_disco_opt:extra_domains(NewOpts),
×
81
    OldDomains = mod_disco_opt:extra_domains(OldOpts),
×
82
    lists:foreach(
×
83
      fun(Domain) ->
84
              register_extra_domain(Host, Domain)
×
85
      end, NewDomains -- OldDomains),
86
    lists:foreach(
×
87
      fun(Domain) ->
88
              unregister_extra_domain(Host, Domain)
×
89
      end, OldDomains -- NewDomains).
90

91
-spec register_extra_domain(binary(), binary()) -> true.
92
register_extra_domain(Host, Domain) ->
93
    ets:insert(disco_extra_domains, {{Domain, Host}}).
×
94

95
-spec unregister_extra_domain(binary(), binary()) -> true.
96
unregister_extra_domain(Host, Domain) ->
97
    ets:delete_object(disco_extra_domains, {{Domain, Host}}).
×
98

99
-spec process_local_iq_items(iq()) -> iq().
100
process_local_iq_items(#iq{type = set, lang = Lang} = IQ) ->
101
    Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
×
102
    xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
×
103
process_local_iq_items(#iq{type = get, lang = Lang,
104
                           from = From, to = To,
105
                           sub_els = [#disco_items{node = Node}]} = IQ) ->
106
    Host = To#jid.lserver,
2✔
107
    case ejabberd_hooks:run_fold(disco_local_items, Host,
2✔
108
                                 empty, [From, To, Node, Lang]) of
109
        {result, Items} ->
110
            xmpp:make_iq_result(IQ, #disco_items{node = Node, items = Items});
2✔
111
        {error, Error} ->
112
            xmpp:make_error(IQ, Error)
×
113
    end.
114

115
-spec process_local_iq_info(iq()) -> iq().
116
process_local_iq_info(#iq{type = set, lang = Lang} = IQ) ->
117
    Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
×
118
    xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
×
119
process_local_iq_info(#iq{type = get, lang = Lang,
120
                          from = From, to = To,
121
                          sub_els = [#disco_info{node = Node}]} = IQ) ->
122
    Host = To#jid.lserver,
59✔
123
    Identity = ejabberd_hooks:run_fold(disco_local_identity,
59✔
124
                                       Host, [], [From, To, Node, Lang]),
125
    Info = ejabberd_hooks:run_fold(disco_info, Host, [],
59✔
126
                                   [Host, ?MODULE, Node, Lang]),
127
    case ejabberd_hooks:run_fold(disco_local_features, Host,
59✔
128
                                 empty, [From, To, Node, Lang]) of
129
        {result, Features} ->
130
            xmpp:make_iq_result(IQ, #disco_info{node = Node,
59✔
131
                                                identities = Identity,
132
                                                xdata = Info,
133
                                                features = Features});
134
        {error, Error} ->
135
            xmpp:make_error(IQ, Error)
×
136
    end.
137

138
-spec get_local_identity([identity()], jid(), jid(),
139
                         binary(), binary()) ->        [identity()].
140
get_local_identity(Acc, _From, To, <<"">>, _Lang) ->
141
    Host = To#jid.lserver,
1,882✔
142
    Name = mod_disco_opt:name(Host),
1,882✔
143
    Acc ++ [#identity{category = <<"server">>,
1,882✔
144
                      type = <<"im">>,
145
                      name = Name}];
146
get_local_identity(Acc, _From, _To, _Node, _Lang) ->
147
    Acc.
1✔
148

149
-spec get_local_features(features_acc(), jid(), jid(), binary(), binary()) ->
150
                                {error, stanza_error()} | {result, [binary()]}.
151
get_local_features({error, _Error} = Acc, _From, _To,
152
                   _Node, _Lang) ->
153
    Acc;
×
154
get_local_features(Acc, _From, To, <<"">>, _Lang) ->
155
    Feats = case Acc of
1,882✔
156
                {result, Features} -> Features;
1,882✔
157
                empty -> []
×
158
            end,
159
    {result, lists:usort(
1,882✔
160
               lists:flatten(
161
                 [?NS_FEATURE_IQ, ?NS_FEATURE_PRESENCE,
162
                  ?NS_DISCO_INFO, ?NS_DISCO_ITEMS, Feats,
163
                  ejabberd_local:get_features(To#jid.lserver)]))};
164
get_local_features(Acc, _From, _To, _Node, Lang) ->
165
    case Acc of
1✔
166
      {result, _Features} -> Acc;
1✔
167
      empty ->
168
            Txt = ?T("No features available"),
×
169
            {error, xmpp:err_item_not_found(Txt, Lang)}
×
170
    end.
171

172
-spec get_local_services(items_acc(), jid(), jid(), binary(), binary()) ->
173
                                {error, stanza_error()} | {result, [disco_item()]}.
174
get_local_services({error, _Error} = Acc, _From, _To,
175
                   _Node, _Lang) ->
176
    Acc;
×
177
get_local_services(Acc, _From, To, <<"">>, _Lang) ->
178
    Items = case Acc of
1✔
179
              {result, Its} -> Its;
1✔
180
              empty -> []
×
181
            end,
182
    Host = To#jid.lserver,
1✔
183
    {result,
1✔
184
     lists:usort(
185
       lists:map(
186
         fun(Domain) -> #disco_item{jid = jid:make(Domain)} end,
3✔
187
         get_vh_services(Host) ++
188
             ets:select(disco_extra_domains,
189
                        ets:fun2ms(
190
                          fun({{D, H}}) when H == Host -> D end))))
191
     ++ Items};
192
get_local_services({result, _} = Acc, _From, _To, _Node,
193
                   _Lang) ->
194
    Acc;
1✔
195
get_local_services(empty, _From, _To, _Node, Lang) ->
196
    {error, xmpp:err_item_not_found(?T("No services available"), Lang)}.
×
197

198
-spec get_vh_services(binary()) -> [binary()].
199
get_vh_services(Host) ->
200
    Hosts = lists:sort(fun (H1, H2) ->
1✔
201
                               byte_size(H1) >= byte_size(H2)
31✔
202
                       end,
203
                       ejabberd_option:hosts()),
204
    lists:filter(fun (H) ->
1✔
205
                         case lists:dropwhile(fun (VH) ->
28✔
206
                                                      not
163✔
207
                                                        str:suffix(
208
                                                          <<".", VH/binary>>,
209
                                                          H)
210
                                              end,
211
                                              Hosts)
212
                             of
213
                           [] -> false;
×
214
                           [VH | _] -> VH == Host
28✔
215
                         end
216
                 end,
217
                 ejabberd_router:get_all_routes()).
218

219
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
220

221
-spec process_sm_iq_items(iq()) -> iq().
222
process_sm_iq_items(#iq{type = set, lang = Lang} = IQ) ->
223
    Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
×
224
    xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
×
225
process_sm_iq_items(#iq{type = get, lang = Lang,
226
                        from = From, to = To,
227
                        sub_els = [#disco_items{node = Node}]} = IQ) ->
228
    case mod_roster:is_subscribed(From, To) of
8✔
229
        true ->
230
            Host = To#jid.lserver,
8✔
231
            case ejabberd_hooks:run_fold(disco_sm_items, Host,
8✔
232
                                         empty, [From, To, Node, Lang]) of
233
                {result, Items} ->
234
                    xmpp:make_iq_result(
8✔
235
                      IQ, #disco_items{node = Node, items = Items});
236
                {error, Error} ->
237
                    xmpp:make_error(IQ, Error)
×
238
            end;
239
        false ->
240
            Txt = ?T("Not subscribed"),
×
241
            xmpp:make_error(IQ, xmpp:err_subscription_required(Txt, Lang))
×
242
    end.
243

244
-spec get_sm_items(items_acc(), jid(), jid(), binary(), binary()) ->
245
                          {error, stanza_error()} | {result, [disco_item()]}.
246
get_sm_items({error, _Error} = Acc, _From, _To, _Node,
247
             _Lang) ->
248
    Acc;
×
249
get_sm_items(Acc, From,
250
             #jid{user = User, server = Server} = To, <<"">>, _Lang) ->
251
    Items = case Acc of
×
252
              {result, Its} -> Its;
×
253
              empty -> []
×
254
            end,
255
    Items1 = case mod_roster:is_subscribed(From, To) of
×
256
               true -> get_user_resources(User, Server);
×
257
               _ -> []
×
258
             end,
259
    {result, Items ++ Items1};
×
260
get_sm_items({result, _} = Acc, _From, _To, _Node,
261
             _Lang) ->
262
    Acc;
8✔
263
get_sm_items(empty, From, To, _Node, Lang) ->
264
    #jid{luser = LFrom, lserver = LSFrom} = From,
×
265
    #jid{luser = LTo, lserver = LSTo} = To,
×
266
    case {LFrom, LSFrom} of
×
267
      {LTo, LSTo} -> {error, xmpp:err_item_not_found()};
×
268
      _ ->
269
            Txt = ?T("Query to another users is forbidden"),
×
270
            {error, xmpp:err_not_allowed(Txt, Lang)}
×
271
    end.
272

273
-spec process_sm_iq_info(iq()) -> iq().
274
process_sm_iq_info(#iq{type = set, lang = Lang} = IQ) ->
275
    Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
×
276
    xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
×
277
process_sm_iq_info(#iq{type = get, lang = Lang,
278
                       from = From, to = To,
279
                       sub_els = [#disco_info{node = Node}]} = IQ) ->
280
    case mod_roster:is_subscribed(From, To) of
56✔
281
        true ->
282
            Host = To#jid.lserver,
56✔
283
            Identity = ejabberd_hooks:run_fold(disco_sm_identity,
56✔
284
                                               Host, [],
285
                                               [From, To, Node, Lang]),
286
            Info = ejabberd_hooks:run_fold(disco_info, Host, [],
56✔
287
                                           [From, To, Node, Lang]),
288
            case ejabberd_hooks:run_fold(disco_sm_features, Host,
56✔
289
                                         empty, [From, To, Node, Lang]) of
290
                {result, Features} ->
291
                    xmpp:make_iq_result(IQ, #disco_info{node = Node,
56✔
292
                                                        identities = Identity,
293
                                                        xdata = Info,
294
                                                        features = Features});
295
                {error, Error} ->
296
                    xmpp:make_error(IQ, Error)
×
297
            end;
298
        false ->
299
            Txt = ?T("Not subscribed"),
×
300
            xmpp:make_error(IQ, xmpp:err_subscription_required(Txt, Lang))
×
301
    end.
302

303
-spec get_sm_identity([identity()], jid(), jid(),
304
                      binary(), binary()) -> [identity()].
305
get_sm_identity(Acc, _From,
306
                #jid{luser = LUser, lserver = LServer}, _Node, _Lang) ->
307
    Acc ++
56✔
308
      case ejabberd_auth:user_exists(LUser, LServer) of
309
        true ->
310
            [#identity{category = <<"account">>, type = <<"registered">>}];
56✔
311
        _ -> []
×
312
      end.
313

314
-spec get_sm_features(features_acc(), jid(), jid(), binary(), binary()) ->
315
                             {error, stanza_error()} | {result, [binary()]}.
316
get_sm_features(empty, From, To, Node, Lang) ->
317
    #jid{luser = LFrom, lserver = LSFrom} = From,
×
318
    #jid{luser = LTo, lserver = LSTo} = To,
×
319
    case {LFrom, LSFrom} of
×
320
        {LTo, LSTo} ->
321
            case Node of
×
322
                <<"">> -> {result, [?NS_DISCO_INFO, ?NS_DISCO_ITEMS]};
×
323
                _ -> {error, xmpp:err_item_not_found()}
×
324
            end;
325
        _ ->
326
            Txt = ?T("Query to another users is forbidden"),
×
327
            {error, xmpp:err_not_allowed(Txt, Lang)}
×
328
    end;
329
get_sm_features({result, Features}, _From, _To, <<"">>, _Lang) ->
330
    {result, [?NS_DISCO_INFO, ?NS_DISCO_ITEMS|Features]};
32✔
331
get_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc.
24✔
332

333
-spec get_user_resources(binary(), binary()) -> [disco_item()].
334
get_user_resources(User, Server) ->
335
    Rs = ejabberd_sm:get_user_resources(User, Server),
×
336
    [#disco_item{jid = jid:make(User, Server, Resource), name = User}
×
337
     || Resource <- lists:sort(Rs)].
×
338

339
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
340

341
%%% Support for: XEP-0157 Contact Addresses for XMPP Services
342

343
-spec get_info([xdata()], binary(), module(), binary(), binary()) -> [xdata()];
344
              ([xdata()], jid(), jid(), binary(), binary()) -> [xdata()].
345
get_info(_A, Host, Mod, Node, _Lang) when is_atom(Mod), Node == <<"">> ->
346
    Module = case Mod of
1,917✔
347
               undefined -> ?MODULE;
1,824✔
348
               _ -> Mod
93✔
349
             end,
350
    [#xdata{type = result,
1,917✔
351
            fields = [#xdata_field{type = hidden,
352
                                   var = <<"FORM_TYPE">>,
353
                                   values = [?NS_SERVERINFO]}
354
                      | get_fields(Host, Module)]}];
355
get_info(Acc, _, _, _Node, _) -> Acc.
57✔
356

357
-spec get_fields(binary(), module()) -> [xdata_field()].
358
get_fields(Host, Module) ->
359
    Fields = mod_disco_opt:server_info(Host),
1,917✔
360
    Fields1 = lists:filter(fun ({Modules, _, _}) ->
1,917✔
361
                                   case Modules of
×
362
                                       all -> true;
×
363
                                       Modules ->
364
                                           lists:member(Module, Modules)
×
365
                                   end
366
                           end,
367
                           Fields),
368
    [#xdata_field{var = Var,
1,917✔
369
                  type = 'list-multi',
370
                  values = Values} || {_, Var, Values} <- Fields1].
1,917✔
371

372
-spec depends(binary(), gen_mod:opts()) -> [].
373
depends(_Host, _Opts) ->
374
    [].
117✔
375

376
mod_opt_type(extra_domains) ->
377
    econf:list(econf:binary());
117✔
378
mod_opt_type(name) ->
379
    econf:binary();
117✔
380
mod_opt_type(server_info) ->
381
    econf:list(
117✔
382
      econf:and_then(
383
        econf:options(
384
          #{name => econf:binary(),
385
            urls => econf:list(econf:binary()),
386
            modules =>
387
                econf:either(
388
                  all,
389
                  econf:list(econf:beam()))}),
390
        fun(Opts) ->
391
                Mods = proplists:get_value(modules, Opts, all),
×
392
                Name = proplists:get_value(name, Opts, <<>>),
×
393
                URLs = proplists:get_value(urls, Opts, []),
×
394
                {Mods, Name, URLs}
×
395
        end)).
396

397
-spec mod_options(binary()) -> [{server_info,
398
                                 [{all | [module()], binary(), [binary()]}]} |
399
                                {atom(), any()}].
400
mod_options(_Host) ->
401
    [{extra_domains, []},
117✔
402
     {server_info, []},
403
     {name, ?T("ejabberd")}].
404

405
mod_doc() ->
406
    #{desc =>
×
407
          ?T("This module adds support for "
408
             "https://xmpp.org/extensions/xep-0030.html"
409
             "[XEP-0030: Service Discovery]. With this module enabled, "
410
             "services on your server can be discovered by XMPP clients."),
411
      opts =>
412
          [{extra_domains,
413
            #{value => "[Domain, ...]",
414
              desc =>
415
                  ?T("With this option, you can specify a list of extra "
416
                     "domains that are added to the Service Discovery item list. "
417
                     "The default value is an empty list.")}},
418
           {name,
419
            #{value => ?T("Name"),
420
              desc =>
421
                  ?T("A name of the server in the Service Discovery. "
422
                     "This will only be displayed by special XMPP clients. "
423
                     "The default value is 'ejabberd'.")}},
424
           {server_info,
425
            #{value => "[Info, ...]",
426
              example =>
427
                  ["server_info:",
428
                   "  -",
429
                   "    modules: all",
430
                   "    name: abuse-addresses",
431
                   "    urls: [\"mailto:abuse@shakespeare.lit\"]",
432
                   "  -",
433
                   "    modules: [mod_muc]",
434
                   "    name: \"Web chatroom logs\"",
435
                   "    urls: [\"http://www.example.org/muc-logs\"]",
436
                   "  -",
437
                   "    modules: [mod_disco]",
438
                   "    name: feedback-addresses",
439
                   "    urls:",
440
                   "      - http://shakespeare.lit/feedback.php",
441
                   "      - mailto:feedback@shakespeare.lit",
442
                   "      - xmpp:feedback@shakespeare.lit",
443
                   "  -",
444
                   "    modules:",
445
                   "      - mod_disco",
446
                   "      - mod_vcard",
447
                   "    name: admin-addresses",
448
                   "    urls:",
449
                   "      - mailto:xmpp@shakespeare.lit",
450
                   "      - xmpp:admins@shakespeare.lit"],
451
              desc =>
452
                  ?T("Specify additional information about the server, "
453
                     "as described in https://xmpp.org/extensions/xep-0157.html"
454
                     "[XEP-0157: Contact Addresses for XMPP Services]. Every 'Info' "
455
                     "element in the list is constructed from the following options:")},
456
            [{modules,
457
              #{value => "all | [Module, ...]",
458
                desc =>
459
                    ?T("The value can be the keyword 'all', in which case the "
460
                       "information is reported in all the services, "
461
                       "or a list of ejabberd modules, in which case the "
462
                       "information is only specified for the services provided "
463
                       "by those modules.")}},
464
             {name,
465
              #{value => ?T("Name"),
466
                desc => ?T("The field 'var' name that will be defined. "
467
                           "See XEP-0157 for some standardized names.")}},
468
             {urls,
469
              #{value => "[URI, ...]",
470
                desc => ?T("A list of contact URIs, such as "
471
                           "HTTP URLs, XMPP URIs and so on.")}}]}]}.
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc