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

processone / ejabberd / 1291

15 Jan 2026 04:04PM UTC coverage: 33.54% (+0.008%) from 33.532%
1291

push

github

prefiks
Add replaced_connection_timeout option

This option enabled new session to wait for termination
of session that it replaces. This should mitigate
problems where old session presences unavailable sometimes
were delivered after new session sent it's presence available.

9 of 44 new or added lines in 3 files covered. (20.45%)

4264 existing lines in 86 files now uncovered.

15568 of 46416 relevant lines covered (33.54%)

1074.61 hits per line

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

34.23
/src/mod_http_api.erl
1
%%%----------------------------------------------------------------------
2
%%% File    : mod_http_api.erl
3
%%% Author  : Christophe romain <christophe.romain@process-one.net>
4
%%% Purpose : Implements REST API for ejabberd using JSON data
5
%%% Created : 15 Sep 2014 by Christophe Romain <christophe.romain@process-one.net>
6
%%%
7
%%%
8
%%% ejabberd, Copyright (C) 2002-2026   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_http_api).
27

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

30
-behaviour(gen_mod).
31

32
-export([start/2, stop/1, reload/3, process/2, depends/2,
33
         format_arg/2, handle/4,
34
         mod_opt_type/1, mod_options/1, mod_doc/0]).
35

36
-include_lib("xmpp/include/xmpp.hrl").
37
-include("logger.hrl").
38
-include("ejabberd_http.hrl").
39

40
-include("translate.hrl").
41

42
-define(DEFAULT_API_VERSION, 1000000).
43

44
-define(CT_PLAIN,
45
        {<<"Content-Type">>, <<"text/plain">>}).
46

47
-define(CT_XML,
48
        {<<"Content-Type">>, <<"text/xml; charset=utf-8">>}).
49

50
-define(CT_JSON,
51
        {<<"Content-Type">>, <<"application/json">>}).
52

53
-define(AC_ALLOW_ORIGIN,
54
        {<<"Access-Control-Allow-Origin">>, <<"*">>}).
55

56
-define(AC_ALLOW_METHODS,
57
        {<<"Access-Control-Allow-Methods">>,
58
         <<"GET, POST, OPTIONS">>}).
59

60
-define(AC_ALLOW_HEADERS,
61
        {<<"Access-Control-Allow-Headers">>,
62
         <<"Content-Type, Authorization, X-Admin">>}).
63

64
-define(AC_MAX_AGE,
65
        {<<"Access-Control-Max-Age">>, <<"86400">>}).
66

67
-define(OPTIONS_HEADER,
68
        [?CT_PLAIN, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_METHODS,
69
         ?AC_ALLOW_HEADERS, ?AC_MAX_AGE]).
70

71
-define(HEADER(CType),
72
        [CType, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS]).
73

74
%% -------------------
75
%% Module control
76
%% -------------------
77

78
start(_Host, _Opts) ->
79
    ok.
×
80

81
stop(_Host) ->
82
    ok.
×
83

84
reload(_Host, _NewOpts, _OldOpts) ->
85
    ok.
×
86

87
depends(_Host, _Opts) ->
88
    [].
×
89

90
%% ----------
91
%% basic auth
92
%% ----------
93

94
extract_auth(#request{auth = HTTPAuth, ip = {IP, _}, opts = Opts}) ->
UNCOV
95
    Info = case HTTPAuth of
29✔
96
               {SJID, Pass} ->
97
                   try jid:decode(SJID) of
×
98
                       #jid{luser = User, lserver = Server} ->
99
                           case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of
×
100
                               true ->
101
                                   #{usr => {User, Server, <<"">>}, caller_server => Server};
×
102
                               false ->
103
                                   {error, invalid_auth}
×
104
                           end
105
                   catch _:{bad_jid, _} ->
106
                       {error, invalid_auth}
×
107
                   end;
108
               {oauth, Token, _} ->
109
                   case ejabberd_oauth:check_token(Token) of
×
110
                       {ok, {U, S}, Scope} ->
111
                           #{usr => {U, S, <<"">>}, oauth_scope => Scope, caller_server => S};
×
112
                       {false, Reason} ->
113
                           {error, Reason}
×
114
                   end;
115
               invalid ->
116
                   {error, invalid_auth};
×
117
               _ ->
UNCOV
118
                   #{}
29✔
119
           end,
UNCOV
120
    case Info of
29✔
121
        Map when is_map(Map) ->
UNCOV
122
            Tag = proplists:get_value(tag, Opts, <<>>),
29✔
UNCOV
123
            Map#{caller_module => ?MODULE, ip => IP, tag => Tag};
29✔
124
        _ ->
125
            ?DEBUG("Invalid auth data: ~p", [Info]),
×
126
            Info
×
127
    end.
128

129
%% ------------------
130
%% command processing
131
%% ------------------
132

133
%process(Call, Request) ->
134
%    ?DEBUG("~p~n~p", [Call, Request]), ok;
135
process(_, #request{method = 'POST', data = <<>>}) ->
136
    ?DEBUG("Bad Request: no data", []),
×
137
    badrequest_response(<<"Missing POST data">>);
×
138
process([Call | _], #request{method = 'POST', data = Data, ip = IPPort} = Req) ->
UNCOV
139
    Version = get_api_version(Req),
29✔
UNCOV
140
    try
29✔
UNCOV
141
        Args = extract_args(Data),
29✔
UNCOV
142
        log(Call, Args, IPPort),
29✔
UNCOV
143
        perform_call(Call, Args, Req, Version)
29✔
144
    catch
145
        %% TODO We need to refactor to remove redundant error return formatting
146
        throw:{error, unknown_command} ->
147
            json_format({404, 44, <<"Command not found.">>});
×
148
        _:{error,{_,invalid_json}} = Err ->
149
            ?DEBUG("Bad Request: ~p", [Err]),
×
150
            badrequest_response(<<"Invalid JSON input">>);
×
151
        _Class:Error:StackTrace ->
152
            ?DEBUG("Bad Request: ~p ~p", [Error, StackTrace]),
×
153
            badrequest_response()
×
154
    end;
155
process([Call | _], #request{method = 'GET', q = Data, ip = {IP, _}} = Req) ->
156
    Version = get_api_version(Req),
×
157
    try
×
158
        Args = case Data of
×
159
                   [{nokey, <<>>}] -> [];
×
160
                   _ -> Data
×
161
               end,
162
        log(Call, Args, IP),
×
163
        perform_call(Call, Args, Req, Version)
×
164
    catch
165
        %% TODO We need to refactor to remove redundant error return formatting
166
        throw:{error, unknown_command} ->
167
            json_format({404, 44, <<"Command not found.">>});
×
168
        _:Error:StackTrace ->
169
            ?DEBUG("Bad Request: ~p ~p", [Error, StackTrace]),
×
170
            badrequest_response()
×
171
    end;
172
process([_Call], #request{method = 'OPTIONS', data = <<>>}) ->
173
    {200, ?OPTIONS_HEADER, []};
×
174
process(_, #request{method = 'OPTIONS'}) ->
175
    {400, ?OPTIONS_HEADER, []};
×
176
process(_Path, Request) ->
177
    ?DEBUG("Bad Request: no handler ~p", [Request]),
×
178
    json_error(400, 40, <<"Missing command name.">>).
×
179

180
perform_call(Command, Args, Req, Version) ->
UNCOV
181
    case catch binary_to_existing_atom(Command, utf8) of
29✔
182
        Call when is_atom(Call) ->
UNCOV
183
            case extract_auth(Req) of
29✔
184
                {error, expired} -> invalid_token_response();
×
185
                {error, not_found} -> invalid_token_response();
×
186
                {error, invalid_auth} -> unauthorized_response();
×
187
                Auth when is_map(Auth) ->
UNCOV
188
                    Result = handle(Call, Auth, Args, Version),
29✔
UNCOV
189
                    json_format(Result)
29✔
190
            end;
191
        _ ->
192
            json_error(404, 40, <<"Endpoint not found.">>)
×
193
    end.
194

195
%% Be tolerant to make API more easily usable from command-line pipe.
196
extract_args(<<"\n">>) -> [];
×
197
extract_args(Data) ->
UNCOV
198
    Maps = misc:json_decode(Data),
29✔
UNCOV
199
    maps:to_list(Maps).
29✔
200

201
% get API version N from last "vN" element in URL path
202
get_api_version(#request{path = Path, host = Host}) ->
UNCOV
203
    get_api_version(lists:reverse(Path), Host).
29✔
204

205
get_api_version([<<"v", String/binary>> | Tail], Host) ->
206
    case catch binary_to_integer(String) of
×
207
        N when is_integer(N) ->
208
            N;
×
209
        _ ->
210
            get_api_version(Tail, Host)
×
211
    end;
212
get_api_version([_Head | Tail], Host) ->
UNCOV
213
    get_api_version(Tail, Host);
58✔
214
get_api_version([], Host) ->
UNCOV
215
    try mod_http_api_opt:default_version(Host)
29✔
216
    catch error:{module_not_loaded, ?MODULE, Host} ->
UNCOV
217
        ?WARNING_MSG("Using module ~p for host ~s, but it isn't configured "
29✔
UNCOV
218
                     "in the configuration file", [?MODULE, Host]),
29✔
UNCOV
219
        ?DEFAULT_API_VERSION
29✔
220
    end.
221

222
%% ----------------
223
%% command handlers
224
%% ----------------
225

226
%% TODO Check accept types of request before decided format of reply.
227

228
% generic ejabberd command handler
229
handle(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
UNCOV
230
    Args2 = [{misc:binary_to_atom(Key), Value} || {Key, Value} <- Args],
41✔
UNCOV
231
    try handle2(Call, Auth, Args2, Version)
41✔
232
    catch throw:not_found ->
233
            {404, <<"not_found">>};
×
234
          throw:{not_found, Why} when is_atom(Why) ->
235
            {404, misc:atom_to_binary(Why)};
×
236
          throw:{not_found, Msg} ->
237
            {404, iolist_to_binary(Msg)};
×
238
          throw:not_allowed ->
239
            {401, <<"not_allowed">>};
×
240
          throw:{not_allowed, Why} when is_atom(Why) ->
241
            {401, misc:atom_to_binary(Why)};
×
242
          throw:{not_allowed, Msg} ->
243
            {401, iolist_to_binary(Msg)};
×
244
          throw:{error, account_unprivileged} ->
245
            {403, 31, <<"Command need to be run with admin privilege.">>};
×
246
          throw:{error, access_rules_unauthorized} ->
247
            {403, 32, <<"AccessRules: Account does not have the right to perform the operation.">>};
×
248
          throw:{invalid_parameter, Msg} ->
249
            {400, iolist_to_binary(Msg)};
×
250
          throw:{error, Why} when is_atom(Why) ->
251
            {400, misc:atom_to_binary(Why)};
×
252
          throw:{error, Msg} ->
253
            {400, iolist_to_binary(Msg)};
×
254
          throw:Error when is_atom(Error) ->
255
            {400, misc:atom_to_binary(Error)};
×
256
          throw:Msg when is_list(Msg); is_binary(Msg) ->
257
            {400, iolist_to_binary(Msg)};
×
258
        Class:Error:StackTrace ->
259
            ?ERROR_MSG("REST API Error: "
×
260
                       "~ts(~p) -> ~p:~p ~p",
261
                       [Call,
262
                        hide_sensitive_args(Args),
263
                        Class,
264
                        Error,
265
                        StackTrace]),
×
266
            {500, <<"internal_error">>}
×
267
    end.
268

269
handle2(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
UNCOV
270
    {ArgsF, ArgsR, _ResultF} = ejabberd_commands:get_command_format(Call, Auth, Version),
41✔
UNCOV
271
    ArgsFormatted = format_args(Call, rename_old_args(Args, ArgsR), ArgsF),
41✔
UNCOV
272
    case ejabberd_commands:execute_command2(Call, ArgsFormatted, Auth, Version) of
41✔
273
        {error, Error} ->
274
            throw(Error);
×
275
        Res ->
UNCOV
276
            format_command_result(Call, Auth, Res, Version)
41✔
277
    end.
278

279
rename_old_args(Args, []) ->
UNCOV
280
    Args;
41✔
281
rename_old_args(Args, [{OldName, NewName} | ArgsR]) ->
282
    Args2 = case lists:keytake(OldName, 1, Args) of
×
283
        {value, {OldName, Value}, ArgsTail} ->
284
            [{NewName, Value} | ArgsTail];
×
285
        false ->
286
            Args
×
287
    end,
288
    rename_old_args(Args2, ArgsR).
×
289

290
get_elem_delete(Call, A, L, F) ->
UNCOV
291
    case proplists:get_all_values(A, L) of
77✔
UNCOV
292
      [Value] -> {Value, proplists:delete(A, L)};
77✔
293
      [_, _ | _] ->
294
          ?INFO_MSG("Command ~ts call rejected, it has duplicate attribute ~w",
×
295
                    [Call, A]),
×
296
          throw({invalid_parameter,
×
297
                 io_lib:format("Request have duplicate argument: ~w", [A])});
298
      [] ->
299
          case F of
×
300
              {list, _} ->
301
                  {[], L};
×
302
              _ ->
303
                  ?INFO_MSG("Command ~ts call rejected, missing attribute ~w",
×
304
                            [Call, A]),
×
305
                  throw({invalid_parameter,
×
306
                         io_lib:format("Request have missing argument: ~w", [A])})
307
          end
308
    end.
309

310
format_args(Call, Args, ArgsFormat) ->
UNCOV
311
    {ArgsRemaining, R} = lists:foldl(fun ({ArgName,
41✔
312
                                           ArgFormat},
313
                                          {Args1, Res}) ->
UNCOV
314
                                             {ArgValue, Args2} =
77✔
315
                                                 get_elem_delete(Call, ArgName,
316
                                                                 Args1, ArgFormat),
UNCOV
317
                                             Formatted = format_arg(ArgValue,
77✔
318
                                                                    ArgFormat),
UNCOV
319
                                             {Args2, Res ++ [Formatted]}
77✔
320
                                     end,
321
                                     {Args, []}, ArgsFormat),
UNCOV
322
    case ArgsRemaining of
41✔
UNCOV
323
      [] -> R;
41✔
324
      L when is_list(L) ->
325
          ExtraArgs = [N || {N, _} <- L],
×
326
          ?INFO_MSG("Command ~ts call rejected, it has unknown arguments ~w",
×
327
              [Call, ExtraArgs]),
×
328
          throw({invalid_parameter,
×
329
                 io_lib:format("Request have unknown arguments: ~w", [ExtraArgs])})
330
    end.
331

332
format_arg({Elements},
333
           {list, {_ElementDefName, {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]} = Tuple}})
334
    when is_list(Elements) andalso
335
         (Tuple1S == binary orelse Tuple1S == string) ->
336
    lists:map(fun({F1, F2}) ->
×
337
                      {format_arg(F1, Tuple1S), format_arg(F2, Tuple2S)};
×
338
                 ({Val}) when is_list(Val) ->
339
                      format_arg({Val}, Tuple)
×
340
              end, Elements);
341
format_arg(Map,
342
           {list, {_ElementDefName, {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]}}})
343
    when is_map(Map) andalso
344
         (Tuple1S == binary orelse Tuple1S == string) ->
UNCOV
345
    maps:fold(
1✔
346
        fun(K, V, Acc) ->
UNCOV
347
            [{format_arg(K, Tuple1S), format_arg(V, Tuple2S)} | Acc]
3✔
348
        end, [], Map);
349
format_arg(Elements,
350
           {list, {_ElementDefName, {list, _} = ElementDefFormat}})
351
    when is_list(Elements) ->
352
    [{format_arg(Element, ElementDefFormat)}
×
353
     || Element <- Elements];
×
354

355
%% Covered by command_test_list and command_test_list_tuple
356
format_arg(Element, {list, Def})
357
    when not is_list(Element) ->
358
    format_arg([Element], {list, Def});
×
359
format_arg(Elements,
360
           {list, {_ElementDefName, ElementDefFormat}})
361
    when is_list(Elements) ->
UNCOV
362
    [format_arg(Element, ElementDefFormat)
6✔
UNCOV
363
     || Element <- Elements];
6✔
364

365
format_arg({[{Name, Value}]},
366
           {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]})
367
  when Tuple1S == binary;
368
       Tuple1S == string ->
369
    {format_arg(Name, Tuple1S), format_arg(Value, Tuple2S)};
×
370

371
%% Covered by command_test_tuple and command_test_list_tuple
372
format_arg(Elements,
373
           {tuple, ElementsDef})
374
  when is_map(Elements) ->
UNCOV
375
    list_to_tuple([format_arg(element(2, maps:find(atom_to_binary(Name, latin1), Elements)), Format)
12✔
UNCOV
376
                   || {Name, Format} <- ElementsDef]);
12✔
377

378
format_arg({Elements},
379
           {tuple, ElementsDef})
380
    when is_list(Elements) ->
381
    F = lists:map(fun({TElName, TElDef}) ->
×
382
                          case lists:keyfind(atom_to_binary(TElName, latin1), 1, Elements) of
×
383
                              {_, Value} ->
384
                                  format_arg(Value, TElDef);
×
385
                              _ when TElDef == binary; TElDef == string ->
386
                                  <<"">>;
×
387
                              _ ->
388
                                  ?ERROR_MSG("Missing field ~p in tuple ~p", [TElName, Elements]),
×
389
                                  throw({invalid_parameter,
×
390
                                         io_lib:format("Missing field ~w in tuple ~w", [TElName, Elements])})
391
                          end
392
                  end, ElementsDef),
393
    list_to_tuple(F);
×
394

395
format_arg(Elements, {list, ElementsDef})
396
    when is_list(Elements) and is_atom(ElementsDef) ->
397
    [format_arg(Element, ElementsDef)
×
398
     || Element <- Elements];
×
399

UNCOV
400
format_arg(Arg, integer) when is_integer(Arg) -> Arg;
2✔
UNCOV
401
format_arg(Arg, integer) when is_binary(Arg) -> binary_to_integer(Arg);
1✔
402
format_arg(Arg, binary) when is_list(Arg) -> process_unicode_codepoints(Arg);
×
UNCOV
403
format_arg(Arg, binary) when is_binary(Arg) -> Arg;
47✔
404
format_arg([], binary_or_list) -> [];
×
405
format_arg([First | _] = Arg, binary_or_list) when is_binary(First) -> Arg;
×
406
format_arg([First | _] = Arg, binary_or_list) when is_integer(First) ->
407
    [process_unicode_codepoints(Arg)];
×
408
format_arg(Arg, binary_or_list) when is_binary(Arg) -> [Arg];
×
UNCOV
409
format_arg(Arg, string) when is_list(Arg) -> Arg;
22✔
UNCOV
410
format_arg(Arg, string) when is_binary(Arg) -> binary_to_list(Arg);
37✔
411
format_arg(undefined, binary) -> <<>>;
×
412
format_arg(undefined, binary_or_list) -> [];
×
413
format_arg(undefined, string) -> "";
×
414
format_arg(Arg, Format) ->
415
    ?ERROR_MSG("Don't know how to format Arg ~p for format ~p", [Arg, Format]),
×
416
    throw({invalid_parameter,
×
417
           io_lib:format("Arg ~w is not in format ~w",
418
                         [Arg, Format])}).
419

420
process_unicode_codepoints(Str) ->
421
    iolist_to_binary(lists:map(fun(X) when X > 255 -> unicode:characters_to_binary([X]);
×
422
                                  (Y) -> Y
×
423
                               end, Str)).
424

425
%% ----------------
426
%% internal helpers
427
%% ----------------
428

429
format_command_result(Cmd, Auth, Result, Version) ->
UNCOV
430
    {_, _, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version),
41✔
UNCOV
431
    case {ResultFormat, Result} of
41✔
432
        {{_, rescode}, V} when V == true; V == ok ->
UNCOV
433
            {200, 0};
12✔
434
        {{_, rescode}, _} ->
UNCOV
435
            {200, 1};
2✔
436
        {_, {error, ErrorAtom, Code, Msg}} ->
437
            format_error_result(ErrorAtom, Code, Msg);
×
438
        {{_, restuple}, {V, Text}} when V == true; V == ok ->
UNCOV
439
            {200, iolist_to_binary(Text)};
4✔
440
        {{_, restuple}, {ErrorAtom, Msg}} ->
441
            format_error_result(ErrorAtom, 0, Msg);
×
442
        {{_, {list, _}}, _V} ->
UNCOV
443
            {_, L} = format_result(Result, ResultFormat),
7✔
UNCOV
444
            {200, L};
7✔
445
        {{_, {tuple, _}}, _V} ->
UNCOV
446
            {_, T} = format_result(Result, ResultFormat),
3✔
UNCOV
447
            {200, T};
3✔
448
        _ ->
UNCOV
449
            OtherResult1 = format_result(Result, ResultFormat),
13✔
UNCOV
450
            OtherResult2 = case Version of
13✔
451
                               0 ->
452
                                   {[OtherResult1]};
×
453
                               _ ->
UNCOV
454
                                   {_, Other3} = OtherResult1,
13✔
UNCOV
455
                                   Other3
13✔
456
                           end,
UNCOV
457
            {200, OtherResult2}
13✔
458
    end.
459

460
format_result(Atom, {Name, atom}) ->
UNCOV
461
    {misc:atom_to_binary(Name), misc:atom_to_binary(Atom)};
3✔
462

463
format_result(Int, {Name, integer}) ->
UNCOV
464
    {misc:atom_to_binary(Name), Int};
5✔
465

466
format_result([String | _] = StringList, {Name, string}) when is_list(String) ->
467
    Binarized = iolist_to_binary(string:join(StringList, "\n")),
×
468
    {misc:atom_to_binary(Name), Binarized};
×
469

470
format_result(String, {Name, string}) ->
UNCOV
471
    {misc:atom_to_binary(Name), iolist_to_binary(String)};
47✔
472

473
format_result(Binary, {Name, binary}) ->
474
    {misc:atom_to_binary(Name), Binary};
×
475

476
format_result(Code, {Name, rescode}) ->
477
    {misc:atom_to_binary(Name), Code == true orelse Code == ok};
×
478

479
format_result({Code, Text}, {Name, restuple}) ->
480
    {misc:atom_to_binary(Name),
×
481
     {[{<<"res">>, Code == true orelse Code == ok},
×
482
       {<<"text">>, iolist_to_binary(Text)}]}};
483

484
format_result(Code, {Name, restuple}) ->
485
    {misc:atom_to_binary(Name),
×
486
     {[{<<"res">>, Code == true orelse Code == ok},
×
487
       {<<"text">>, <<"">>}]}};
488

489
format_result(Els1, {Name, {list, {_, {tuple, [{_, atom}, _]}} = Fmt}}) ->
490
    Els = lists:keysort(1, Els1),
×
491
    {misc:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}};
×
492

493
format_result(Els1, {Name, {list, {_, {tuple, [{name, string}, {value, _}]}} = Fmt}}) ->
494
    Els = lists:keysort(1, Els1),
×
495
    {misc:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}};
×
496

497
%% Covered by command_test_list and command_test_list_tuple
498
format_result(Els1, {Name, {list, Def}}) ->
UNCOV
499
    Els = lists:sort(Els1),
7✔
UNCOV
500
    {misc:atom_to_binary(Name), [element(2, format_result(El, Def)) || El <- Els]};
7✔
501

502
format_result(Tuple, {_Name, {tuple, [{_, atom}, ValFmt]}}) ->
503
    {Name2, Val} = Tuple,
×
504
    {_, Val2} = format_result(Val, ValFmt),
×
505
    {misc:atom_to_binary(Name2), Val2};
×
506

507
format_result(Tuple, {_Name, {tuple, [{name, string}, {value, _} = ValFmt]}}) ->
508
    {Name2, Val} = Tuple,
×
509
    {_, Val2} = format_result(Val, ValFmt),
×
510
    {iolist_to_binary(Name2), Val2};
×
511

512
%% Covered by command_test_tuple and command_test_list_tuple
513
format_result(Tuple, {Name, {tuple, Def}}) ->
UNCOV
514
    Els = lists:zip(tuple_to_list(Tuple), Def),
15✔
UNCOV
515
    Els2 = [format_result(El, ElDef) || {El, ElDef} <- Els],
15✔
UNCOV
516
    {misc:atom_to_binary(Name), maps:from_list(Els2)};
15✔
517

518
format_result(404, {_Name, _}) ->
519
    "not_found".
×
520

521

522
format_error_result(conflict, Code, Msg) ->
523
    {409, Code, iolist_to_binary(Msg)};
×
524
format_error_result(not_exists, Code, Msg) ->
525
    {404, Code, iolist_to_binary(Msg)};
×
526
format_error_result(_ErrorAtom, Code, Msg) ->
527
    {500, Code, iolist_to_binary(Msg)}.
×
528

529
unauthorized_response() ->
530
    json_error(401, 10, <<"You are not authorized to call this command.">>).
×
531

532
invalid_token_response() ->
533
    json_error(401, 10, <<"Oauth Token is invalid or expired.">>).
×
534

535
%% outofscope_response() ->
536
%%     json_error(401, 11, <<"Token does not grant usage to command required scope.">>).
537

538
badrequest_response() ->
539
    badrequest_response(<<"400 Bad Request">>).
×
540
badrequest_response(Body) ->
541
    json_response(400, misc:json_encode(Body)).
×
542

543
json_format({Code, Result}) ->
UNCOV
544
    json_response(Code, misc:json_encode(Result));
29✔
545
json_format({HTMLCode, JSONErrorCode, Message}) ->
546
    json_error(HTMLCode, JSONErrorCode, Message).
×
547

548
json_response(Code, Body) when is_integer(Code) ->
UNCOV
549
    {Code, ?HEADER(?CT_JSON), Body}.
29✔
550

551
%% HTTPCode, JSONCode = integers
552
%% message is binary
553
json_error(HTTPCode, JSONCode, Message) ->
554
    {HTTPCode, ?HEADER(?CT_JSON),
×
555
     misc:json_encode(#{<<"status">> => <<"error">>,
556
                    <<"code">> =>  JSONCode,
557
                    <<"message">> => Message})
558
    }.
559

560
log(Call, Args, {Addr, Port}) ->
UNCOV
561
    AddrS = misc:ip_to_list({Addr, Port}),
29✔
UNCOV
562
    ?INFO_MSG("API call ~ts ~p from ~ts:~p", [Call, hide_sensitive_args(Args), AddrS, Port]);
29✔
563
log(Call, Args, IP) ->
564
    ?INFO_MSG("API call ~ts ~p (~p)", [Call, hide_sensitive_args(Args), IP]).
×
565

566
hide_sensitive_args(Args=[_H|_T]) ->
UNCOV
567
    lists:map(fun({<<"password">>, Password}) -> {<<"password">>, ejabberd_config:may_hide_data(Password)};
29✔
568
         ({<<"newpass">>,NewPassword}) -> {<<"newpass">>, ejabberd_config:may_hide_data(NewPassword)};
×
UNCOV
569
         (E) -> E end,
67✔
570
         Args);
571
hide_sensitive_args(NonListArgs) ->
572
    NonListArgs.
×
573

574
mod_opt_type(default_version) ->
575
    econf:either(
×
576
        econf:int(0, 3),
577
        econf:and_then(
578
            econf:binary(),
579
            fun(Binary) ->
580
               case binary_to_list(Binary) of
×
581
                   F when F >= "24.06" ->
582
                       2;
×
583
                   F when (F > "23.10") and (F < "24.06") ->
584
                       1;
×
585
                   F when F =< "23.10" ->
586
                       0
×
587
               end
588
            end)).
589

590
-spec mod_options(binary()) -> [{default_version, integer()}].
591

592
mod_options(_) ->
593
    [{default_version, ?DEFAULT_API_VERSION}].
×
594

595
mod_doc() ->
596
    #{desc =>
×
597
          [?T("This module provides a ReST interface to call "
598
              "_`../../developer/ejabberd-api/index.md|ejabberd API`_ "
599
              "commands using JSON data."), "",
600
           ?T("To use this module, in addition to adding it to the 'modules' "
601
              "section, you must also enable it in 'listen' -> 'ejabberd_http' -> "
602
              "_`listen-options.md#request_handlers|request_handlers`_."), "",
603
           ?T("To use a specific API version N, when defining the URL path "
604
              "in the request_handlers, add a vN. "
605
              "For example: '/api/v2: mod_http_api'."), "",
606
           ?T("To run a command, send a POST request to the corresponding "
607
              "URL: 'http://localhost:5280/api/COMMAND-NAME'")],
608
     opts =>
609
          [{default_version,
610
            #{value => "integer() | string()",
611
              note => "added in 24.12",
612
              desc =>
613
                  ?T("What API version to use when none is specified in the URL path. "
614
                     "If setting an ejabberd version, it will use the latest API "
615
                     "version that was available in that ejabberd version. "
616
                     "For example, setting '\"24.06\"' in this option implies '2'. "
617
                     "The default value is the latest version.")}}],
618
     example =>
619
         ["listen:",
620
          "  -",
621
          "    port: 5280",
622
          "    module: ejabberd_http",
623
          "    request_handlers:",
624
          "      /api: mod_http_api",
625
          "",
626
          "modules:",
627
          "  mod_http_api:",
628
          "    default_version: 2"]}.
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