• 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

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-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_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}) ->
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
               _ ->
118
                   #{}
29✔
119
           end,
120
    case Info of
29✔
121
        Map when is_map(Map) ->
122
            Tag = proplists:get_value(tag, Opts, <<>>),
29✔
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) ->
139
    Version = get_api_version(Req),
29✔
140
    try
29✔
141
        Args = extract_args(Data),
29✔
142
        log(Call, Args, IPPort),
29✔
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) ->
181
    case catch binary_to_existing_atom(Command, utf8) of
29✔
182
        Call when is_atom(Call) ->
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) ->
188
                    Result = handle(Call, Auth, Args, Version),
29✔
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) ->
198
    Maps = misc:json_decode(Data),
29✔
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}) ->
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) ->
213
    get_api_version(Tail, Host);
58✔
214
get_api_version([], Host) ->
215
    try mod_http_api_opt:default_version(Host)
29✔
216
    catch error:{module_not_loaded, ?MODULE, Host} ->
217
        ?WARNING_MSG("Using module ~p for host ~s, but it isn't configured "
29✔
218
                     "in the configuration file", [?MODULE, Host]),
29✔
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) ->
230
    Args2 = [{misc:binary_to_atom(Key), Value} || {Key, Value} <- Args],
41✔
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) ->
270
    {ArgsF, ArgsR, _ResultF} = ejabberd_commands:get_command_format(Call, Auth, Version),
41✔
271
    ArgsFormatted = format_args(Call, rename_old_args(Args, ArgsR), ArgsF),
41✔
272
    case ejabberd_commands:execute_command2(Call, ArgsFormatted, Auth, Version) of
41✔
273
        {error, Error} ->
274
            throw(Error);
×
275
        Res ->
276
            format_command_result(Call, Auth, Res, Version)
41✔
277
    end.
278

279
rename_old_args(Args, []) ->
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) ->
291
    case proplists:get_all_values(A, L) of
77✔
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) ->
311
    {ArgsRemaining, R} = lists:foldl(fun ({ArgName,
41✔
312
                                           ArgFormat},
313
                                          {Args1, Res}) ->
314
                                             {ArgValue, Args2} =
77✔
315
                                                 get_elem_delete(Call, ArgName,
316
                                                                 Args1, ArgFormat),
317
                                             Formatted = format_arg(ArgValue,
77✔
318
                                                                    ArgFormat),
319
                                             {Args2, Res ++ [Formatted]}
77✔
320
                                     end,
321
                                     {Args, []}, ArgsFormat),
322
    case ArgsRemaining of
41✔
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) ->
345
    maps:fold(
1✔
346
        fun(K, V, Acc) ->
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) ->
362
    [format_arg(Element, ElementDefFormat)
6✔
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) ->
375
    list_to_tuple([format_arg(element(2, maps:find(atom_to_binary(Name, latin1), Elements)), Format)
12✔
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

400
format_arg(Arg, integer) when is_integer(Arg) -> Arg;
2✔
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);
×
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];
×
409
format_arg(Arg, string) when is_list(Arg) -> Arg;
22✔
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) ->
430
    {_, _, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version),
41✔
431
    case {ResultFormat, Result} of
41✔
432
        {{_, rescode}, V} when V == true; V == ok ->
433
            {200, 0};
12✔
434
        {{_, rescode}, _} ->
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 ->
439
            {200, iolist_to_binary(Text)};
4✔
440
        {{_, restuple}, {ErrorAtom, Msg}} ->
441
            format_error_result(ErrorAtom, 0, Msg);
×
442
        {{_, {list, _}}, _V} ->
443
            {_, L} = format_result(Result, ResultFormat),
7✔
444
            {200, L};
7✔
445
        {{_, {tuple, _}}, _V} ->
446
            {_, T} = format_result(Result, ResultFormat),
3✔
447
            {200, T};
3✔
448
        _ ->
449
            OtherResult1 = format_result(Result, ResultFormat),
13✔
450
            OtherResult2 = case Version of
13✔
451
                               0 ->
452
                                   {[OtherResult1]};
×
453
                               _ ->
454
                                   {_, Other3} = OtherResult1,
13✔
455
                                   Other3
13✔
456
                           end,
457
            {200, OtherResult2}
13✔
458
    end.
459

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

463
format_result(Int, {Name, integer}) ->
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}) ->
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}}) ->
499
    Els = lists:sort(Els1),
7✔
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}}) ->
514
    Els = lists:zip(tuple_to_list(Tuple), Def),
15✔
515
    Els2 = [format_result(El, ElDef) || {El, ElDef} <- Els],
15✔
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}) ->
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) ->
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}) ->
561
    AddrS = misc:ip_to_list({Addr, Port}),
29✔
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]) ->
567
    lists:map(fun({<<"password">>, Password}) -> {<<"password">>, ejabberd_config:may_hide_data(Password)};
29✔
568
         ({<<"newpass">>,NewPassword}) -> {<<"newpass">>, ejabberd_config:may_hide_data(NewPassword)};
×
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

© 2025 Coveralls, Inc