2015-09-25 14:53:25 +02:00
|
|
|
%%%----------------------------------------------------------------------
|
|
|
|
%%% File : mod_http_api.erl
|
|
|
|
%%% Author : Christophe romain <christophe.romain@process-one.net>
|
|
|
|
%%% Purpose : Implements REST API for ejabberd using JSON data
|
|
|
|
%%% Created : 15 Sep 2014 by Christophe Romain <christophe.romain@process-one.net>
|
|
|
|
%%%
|
|
|
|
%%%
|
2024-01-22 16:40:01 +01:00
|
|
|
%%% ejabberd, Copyright (C) 2002-2024 ProcessOne
|
2015-09-25 14:53:25 +02:00
|
|
|
%%%
|
|
|
|
%%% 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_http_api).
|
|
|
|
|
|
|
|
-author('cromain@process-one.net').
|
|
|
|
|
|
|
|
-behaviour(gen_mod).
|
|
|
|
|
2019-06-14 11:33:26 +02:00
|
|
|
-export([start/2, stop/1, reload/3, process/2, depends/2,
|
2021-12-20 07:29:42 +01:00
|
|
|
format_arg/2,
|
2020-01-08 10:24:51 +01:00
|
|
|
mod_options/1, mod_doc/0]).
|
2015-09-25 14:53:25 +02:00
|
|
|
|
2020-09-03 13:45:57 +02:00
|
|
|
-include_lib("xmpp/include/xmpp.hrl").
|
2015-09-25 14:53:25 +02:00
|
|
|
-include("logger.hrl").
|
|
|
|
-include("ejabberd_http.hrl").
|
2018-12-13 11:45:45 +01:00
|
|
|
-include("ejabberd_stacktrace.hrl").
|
2020-01-08 10:24:51 +01:00
|
|
|
-include("translate.hrl").
|
2015-09-25 14:53:25 +02:00
|
|
|
|
2023-11-29 17:39:34 +01:00
|
|
|
-define(DEFAULT_API_VERSION, 1000000).
|
2016-03-31 13:53:31 +02:00
|
|
|
|
2015-09-25 14:53:25 +02:00
|
|
|
-define(CT_PLAIN,
|
|
|
|
{<<"Content-Type">>, <<"text/plain">>}).
|
|
|
|
|
|
|
|
-define(CT_XML,
|
|
|
|
{<<"Content-Type">>, <<"text/xml; charset=utf-8">>}).
|
|
|
|
|
|
|
|
-define(CT_JSON,
|
|
|
|
{<<"Content-Type">>, <<"application/json">>}).
|
|
|
|
|
|
|
|
-define(AC_ALLOW_ORIGIN,
|
|
|
|
{<<"Access-Control-Allow-Origin">>, <<"*">>}).
|
|
|
|
|
|
|
|
-define(AC_ALLOW_METHODS,
|
|
|
|
{<<"Access-Control-Allow-Methods">>,
|
|
|
|
<<"GET, POST, OPTIONS">>}).
|
|
|
|
|
|
|
|
-define(AC_ALLOW_HEADERS,
|
|
|
|
{<<"Access-Control-Allow-Headers">>,
|
2016-09-12 15:39:00 +02:00
|
|
|
<<"Content-Type, Authorization, X-Admin">>}).
|
2015-09-25 14:53:25 +02:00
|
|
|
|
|
|
|
-define(AC_MAX_AGE,
|
|
|
|
{<<"Access-Control-Max-Age">>, <<"86400">>}).
|
|
|
|
|
|
|
|
-define(OPTIONS_HEADER,
|
|
|
|
[?CT_PLAIN, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_METHODS,
|
|
|
|
?AC_ALLOW_HEADERS, ?AC_MAX_AGE]).
|
|
|
|
|
|
|
|
-define(HEADER(CType),
|
|
|
|
[CType, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS]).
|
|
|
|
|
|
|
|
%% -------------------
|
|
|
|
%% Module control
|
|
|
|
%% -------------------
|
|
|
|
|
|
|
|
start(_Host, _Opts) ->
|
|
|
|
ok.
|
|
|
|
|
|
|
|
stop(_Host) ->
|
|
|
|
ok.
|
|
|
|
|
2019-06-14 11:33:26 +02:00
|
|
|
reload(_Host, _NewOpts, _OldOpts) ->
|
|
|
|
ok.
|
2017-02-22 17:46:47 +01:00
|
|
|
|
2016-07-06 13:58:48 +02:00
|
|
|
depends(_Host, _Opts) ->
|
|
|
|
[].
|
|
|
|
|
2015-09-25 14:53:25 +02:00
|
|
|
%% ----------
|
|
|
|
%% basic auth
|
|
|
|
%% ----------
|
|
|
|
|
2019-01-30 12:56:52 +01:00
|
|
|
extract_auth(#request{auth = HTTPAuth, ip = {IP, _}, opts = Opts}) ->
|
2016-10-05 13:21:11 +02:00
|
|
|
Info = case HTTPAuth of
|
2019-01-30 16:34:29 +01:00
|
|
|
{SJID, Pass} ->
|
|
|
|
try jid:decode(SJID) of
|
2016-10-05 13:21:11 +02:00
|
|
|
#jid{luser = User, lserver = Server} ->
|
2019-01-30 16:34:29 +01:00
|
|
|
case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of
|
2016-10-05 13:21:11 +02:00
|
|
|
true ->
|
|
|
|
#{usr => {User, Server, <<"">>}, caller_server => Server};
|
|
|
|
false ->
|
|
|
|
{error, invalid_auth}
|
2019-01-30 16:34:29 +01:00
|
|
|
end
|
|
|
|
catch _:{bad_jid, _} ->
|
|
|
|
{error, invalid_auth}
|
|
|
|
end;
|
|
|
|
{oauth, Token, _} ->
|
2016-10-05 13:21:11 +02:00
|
|
|
case ejabberd_oauth:check_token(Token) of
|
|
|
|
{ok, {U, S}, Scope} ->
|
|
|
|
#{usr => {U, S, <<"">>}, oauth_scope => Scope, caller_server => S};
|
|
|
|
{false, Reason} ->
|
|
|
|
{error, Reason}
|
2019-01-30 16:34:29 +01:00
|
|
|
end;
|
|
|
|
invalid ->
|
|
|
|
{error, invalid_auth};
|
|
|
|
_ ->
|
2016-10-05 13:21:11 +02:00
|
|
|
#{}
|
2019-01-30 16:34:29 +01:00
|
|
|
end,
|
2016-10-05 13:21:11 +02:00
|
|
|
case Info of
|
|
|
|
Map when is_map(Map) ->
|
2019-01-30 12:56:52 +01:00
|
|
|
Tag = proplists:get_value(tag, Opts, <<>>),
|
|
|
|
Map#{caller_module => ?MODULE, ip => IP, tag => Tag};
|
2016-10-05 13:21:11 +02:00
|
|
|
_ ->
|
|
|
|
?DEBUG("Invalid auth data: ~p", [Info]),
|
|
|
|
Info
|
2019-06-14 11:33:26 +02:00
|
|
|
end.
|
2015-09-25 14:53:25 +02:00
|
|
|
|
|
|
|
%% ------------------
|
|
|
|
%% command processing
|
|
|
|
%% ------------------
|
|
|
|
|
2016-03-31 13:53:31 +02:00
|
|
|
%process(Call, Request) ->
|
|
|
|
% ?DEBUG("~p~n~p", [Call, Request]), ok;
|
2015-09-25 14:53:25 +02:00
|
|
|
process(_, #request{method = 'POST', data = <<>>}) ->
|
|
|
|
?DEBUG("Bad Request: no data", []),
|
2016-03-31 13:53:31 +02:00
|
|
|
badrequest_response(<<"Missing POST data">>);
|
2023-11-30 12:38:46 +01:00
|
|
|
process([Call | _], #request{method = 'POST', data = Data, ip = IPPort} = Req) ->
|
2016-03-31 13:53:31 +02:00
|
|
|
Version = get_api_version(Req),
|
2015-09-25 14:53:25 +02:00
|
|
|
try
|
2016-07-31 22:48:24 +02:00
|
|
|
Args = extract_args(Data),
|
2016-05-25 13:01:07 +02:00
|
|
|
log(Call, Args, IPPort),
|
2016-10-05 13:21:11 +02:00
|
|
|
perform_call(Call, Args, Req, Version)
|
2016-07-31 22:48:24 +02:00
|
|
|
catch
|
|
|
|
%% TODO We need to refactor to remove redundant error return formatting
|
|
|
|
throw:{error, unknown_command} ->
|
2016-09-28 11:03:46 +02:00
|
|
|
json_format({404, 44, <<"Command not found.">>});
|
2016-07-31 22:48:24 +02:00
|
|
|
_:{error,{_,invalid_json}} = _Err ->
|
2016-03-31 13:53:31 +02:00
|
|
|
?DEBUG("Bad Request: ~p", [_Err]),
|
|
|
|
badrequest_response(<<"Invalid JSON input">>);
|
2018-12-13 11:45:45 +01:00
|
|
|
?EX_RULE(_Class, _Error, Stack) ->
|
2019-06-25 23:05:41 +02:00
|
|
|
StackTrace = ?EX_STACK(Stack),
|
|
|
|
?DEBUG("Bad Request: ~p ~p", [_Error, StackTrace]),
|
2016-04-01 11:13:48 +02:00
|
|
|
badrequest_response()
|
2015-09-25 14:53:25 +02:00
|
|
|
end;
|
2023-11-30 12:38:46 +01:00
|
|
|
process([Call | _], #request{method = 'GET', q = Data, ip = {IP, _}} = Req) ->
|
2016-03-31 13:53:31 +02:00
|
|
|
Version = get_api_version(Req),
|
2015-09-25 14:53:25 +02:00
|
|
|
try
|
|
|
|
Args = case Data of
|
2016-04-01 11:13:48 +02:00
|
|
|
[{nokey, <<>>}] -> [];
|
|
|
|
_ -> Data
|
|
|
|
end,
|
2015-09-25 14:53:25 +02:00
|
|
|
log(Call, Args, IP),
|
2016-10-05 13:21:11 +02:00
|
|
|
perform_call(Call, Args, Req, Version)
|
2016-07-31 22:48:24 +02:00
|
|
|
catch
|
|
|
|
%% TODO We need to refactor to remove redundant error return formatting
|
|
|
|
throw:{error, unknown_command} ->
|
2016-08-01 15:29:47 +02:00
|
|
|
json_format({404, 44, <<"Command not found.">>});
|
2018-12-13 11:45:45 +01:00
|
|
|
?EX_RULE(_, _Error, Stack) ->
|
2019-06-25 23:05:41 +02:00
|
|
|
StackTrace = ?EX_STACK(Stack),
|
|
|
|
?DEBUG("Bad Request: ~p ~p", [_Error, StackTrace]),
|
2018-09-01 18:37:26 +02:00
|
|
|
badrequest_response()
|
2015-09-25 14:53:25 +02:00
|
|
|
end;
|
2016-09-12 15:38:27 +02:00
|
|
|
process([_Call], #request{method = 'OPTIONS', data = <<>>}) ->
|
2015-09-25 14:53:25 +02:00
|
|
|
{200, ?OPTIONS_HEADER, []};
|
2016-09-12 15:38:27 +02:00
|
|
|
process(_, #request{method = 'OPTIONS'}) ->
|
|
|
|
{400, ?OPTIONS_HEADER, []};
|
2015-09-25 14:53:25 +02:00
|
|
|
process(_Path, Request) ->
|
|
|
|
?DEBUG("Bad Request: no handler ~p", [Request]),
|
2016-08-01 15:29:47 +02:00
|
|
|
json_error(400, 40, <<"Missing command name.">>).
|
2015-09-25 14:53:25 +02:00
|
|
|
|
2016-10-05 13:21:11 +02:00
|
|
|
perform_call(Command, Args, Req, Version) ->
|
|
|
|
case catch binary_to_existing_atom(Command, utf8) of
|
|
|
|
Call when is_atom(Call) ->
|
|
|
|
case extract_auth(Req) of
|
|
|
|
{error, expired} -> invalid_token_response();
|
|
|
|
{error, not_found} -> invalid_token_response();
|
|
|
|
{error, invalid_auth} -> unauthorized_response();
|
|
|
|
Auth when is_map(Auth) ->
|
|
|
|
Result = handle(Call, Auth, Args, Version),
|
|
|
|
json_format(Result)
|
|
|
|
end;
|
|
|
|
_ ->
|
|
|
|
json_error(404, 40, <<"Endpoint not found.">>)
|
|
|
|
end.
|
|
|
|
|
2016-07-31 22:48:24 +02:00
|
|
|
%% Be tolerant to make API more easily usable from command-line pipe.
|
|
|
|
extract_args(<<"\n">>) -> [];
|
|
|
|
extract_args(Data) ->
|
2024-05-06 18:02:36 +02:00
|
|
|
Maps = misc:json_decode(Data),
|
2024-05-06 17:07:26 +02:00
|
|
|
maps:to_list(Maps).
|
2015-09-25 14:53:25 +02:00
|
|
|
|
2016-03-31 13:53:31 +02:00
|
|
|
% get API version N from last "vN" element in URL path
|
|
|
|
get_api_version(#request{path = Path}) ->
|
|
|
|
get_api_version(lists:reverse(Path));
|
|
|
|
get_api_version([<<"v", String/binary>> | Tail]) ->
|
2016-09-24 22:34:28 +02:00
|
|
|
case catch binary_to_integer(String) of
|
2016-03-31 13:53:31 +02:00
|
|
|
N when is_integer(N) ->
|
|
|
|
N;
|
|
|
|
_ ->
|
|
|
|
get_api_version(Tail)
|
|
|
|
end;
|
|
|
|
get_api_version([_Head | Tail]) ->
|
|
|
|
get_api_version(Tail);
|
|
|
|
get_api_version([]) ->
|
|
|
|
?DEFAULT_API_VERSION.
|
|
|
|
|
2015-09-25 14:53:25 +02:00
|
|
|
%% ----------------
|
|
|
|
%% command handlers
|
|
|
|
%% ----------------
|
|
|
|
|
2016-07-20 20:50:59 +02:00
|
|
|
%% TODO Check accept types of request before decided format of reply.
|
|
|
|
|
2015-09-25 14:53:25 +02:00
|
|
|
% generic ejabberd command handler
|
2016-10-05 13:21:11 +02:00
|
|
|
handle(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
|
2019-06-14 11:33:26 +02:00
|
|
|
Args2 = [{misc:binary_to_atom(Key), Value} || {Key, Value} <- Args],
|
|
|
|
try handle2(Call, Auth, Args2, Version)
|
|
|
|
catch throw:not_found ->
|
|
|
|
{404, <<"not_found">>};
|
|
|
|
throw:{not_found, Why} when is_atom(Why) ->
|
|
|
|
{404, misc:atom_to_binary(Why)};
|
|
|
|
throw:{not_found, Msg} ->
|
|
|
|
{404, iolist_to_binary(Msg)};
|
|
|
|
throw:not_allowed ->
|
|
|
|
{401, <<"not_allowed">>};
|
|
|
|
throw:{not_allowed, Why} when is_atom(Why) ->
|
|
|
|
{401, misc:atom_to_binary(Why)};
|
|
|
|
throw:{not_allowed, Msg} ->
|
|
|
|
{401, iolist_to_binary(Msg)};
|
|
|
|
throw:{error, account_unprivileged} ->
|
|
|
|
{403, 31, <<"Command need to be run with admin privilege.">>};
|
|
|
|
throw:{error, access_rules_unauthorized} ->
|
|
|
|
{403, 32, <<"AccessRules: Account does not have the right to perform the operation.">>};
|
|
|
|
throw:{invalid_parameter, Msg} ->
|
|
|
|
{400, iolist_to_binary(Msg)};
|
|
|
|
throw:{error, Why} when is_atom(Why) ->
|
|
|
|
{400, misc:atom_to_binary(Why)};
|
|
|
|
throw:{error, Msg} ->
|
|
|
|
{400, iolist_to_binary(Msg)};
|
|
|
|
throw:Error when is_atom(Error) ->
|
|
|
|
{400, misc:atom_to_binary(Error)};
|
|
|
|
throw:Msg when is_list(Msg); is_binary(Msg) ->
|
|
|
|
{400, iolist_to_binary(Msg)};
|
|
|
|
?EX_RULE(Class, Error, Stack) ->
|
2019-06-25 23:05:41 +02:00
|
|
|
StackTrace = ?EX_STACK(Stack),
|
2019-06-14 11:33:26 +02:00
|
|
|
?ERROR_MSG("REST API Error: "
|
2019-09-23 14:17:20 +02:00
|
|
|
"~ts(~p) -> ~p:~p ~p",
|
2019-06-14 11:33:26 +02:00
|
|
|
[Call, hide_sensitive_args(Args),
|
2019-06-25 23:05:41 +02:00
|
|
|
Class, Error, StackTrace]),
|
2019-06-14 11:33:26 +02:00
|
|
|
{500, <<"internal_error">>}
|
2015-09-25 14:53:25 +02:00
|
|
|
end.
|
|
|
|
|
2016-10-05 13:21:11 +02:00
|
|
|
handle2(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
|
2019-06-18 11:09:35 +02:00
|
|
|
{ArgsF, ArgsR, _ResultF} = ejabberd_commands:get_command_format(Call, Auth, Version),
|
|
|
|
ArgsFormatted = format_args(Call, rename_old_args(Args, ArgsR), ArgsF),
|
2016-10-05 13:21:11 +02:00
|
|
|
case ejabberd_commands:execute_command2(Call, ArgsFormatted, Auth, Version) of
|
|
|
|
{error, Error} ->
|
|
|
|
throw(Error);
|
|
|
|
Res ->
|
|
|
|
format_command_result(Call, Auth, Res, Version)
|
|
|
|
end.
|
2015-09-25 14:53:25 +02:00
|
|
|
|
2019-06-18 11:09:35 +02:00
|
|
|
rename_old_args(Args, []) ->
|
|
|
|
Args;
|
|
|
|
rename_old_args(Args, [{OldName, NewName} | ArgsR]) ->
|
|
|
|
Args2 = case lists:keytake(OldName, 1, Args) of
|
|
|
|
{value, {OldName, Value}, ArgsTail} ->
|
|
|
|
[{NewName, Value} | ArgsTail];
|
|
|
|
false ->
|
|
|
|
Args
|
|
|
|
end,
|
|
|
|
rename_old_args(Args2, ArgsR).
|
|
|
|
|
2019-04-03 12:04:36 +02:00
|
|
|
get_elem_delete(Call, A, L, F) ->
|
2015-09-25 14:53:25 +02:00
|
|
|
case proplists:get_all_values(A, L) of
|
|
|
|
[Value] -> {Value, proplists:delete(A, L)};
|
|
|
|
[_, _ | _] ->
|
2019-09-23 14:17:20 +02:00
|
|
|
?INFO_MSG("Command ~ts call rejected, it has duplicate attribute ~w",
|
2019-04-03 12:04:36 +02:00
|
|
|
[Call, A]),
|
|
|
|
throw({invalid_parameter,
|
|
|
|
io_lib:format("Request have duplicate argument: ~w", [A])});
|
2015-09-25 14:53:25 +02:00
|
|
|
[] ->
|
2018-12-03 13:52:04 +01:00
|
|
|
case F of
|
|
|
|
{list, _} ->
|
|
|
|
{[], L};
|
|
|
|
_ ->
|
2019-09-23 14:17:20 +02:00
|
|
|
?INFO_MSG("Command ~ts call rejected, missing attribute ~w",
|
2019-04-03 12:04:36 +02:00
|
|
|
[Call, A]),
|
|
|
|
throw({invalid_parameter,
|
|
|
|
io_lib:format("Request have missing argument: ~w", [A])})
|
2018-12-03 13:52:04 +01:00
|
|
|
end
|
2015-09-25 14:53:25 +02:00
|
|
|
end.
|
|
|
|
|
2019-04-03 11:50:15 +02:00
|
|
|
format_args(Call, Args, ArgsFormat) ->
|
2015-09-25 14:53:25 +02:00
|
|
|
{ArgsRemaining, R} = lists:foldl(fun ({ArgName,
|
|
|
|
ArgFormat},
|
|
|
|
{Args1, Res}) ->
|
|
|
|
{ArgValue, Args2} =
|
2019-04-03 12:04:36 +02:00
|
|
|
get_elem_delete(Call, ArgName,
|
2018-12-03 13:52:04 +01:00
|
|
|
Args1, ArgFormat),
|
2015-09-25 14:53:25 +02:00
|
|
|
Formatted = format_arg(ArgValue,
|
|
|
|
ArgFormat),
|
|
|
|
{Args2, Res ++ [Formatted]}
|
|
|
|
end,
|
|
|
|
{Args, []}, ArgsFormat),
|
|
|
|
case ArgsRemaining of
|
|
|
|
[] -> R;
|
2019-01-30 16:35:06 +01:00
|
|
|
L when is_list(L) ->
|
2019-04-03 11:50:15 +02:00
|
|
|
ExtraArgs = [N || {N, _} <- L],
|
2019-09-23 14:17:20 +02:00
|
|
|
?INFO_MSG("Command ~ts call rejected, it has unknown arguments ~w",
|
2019-04-03 11:50:15 +02:00
|
|
|
[Call, ExtraArgs]),
|
2019-01-30 16:35:06 +01:00
|
|
|
throw({invalid_parameter,
|
2019-04-03 11:50:15 +02:00
|
|
|
io_lib:format("Request have unknown arguments: ~w", [ExtraArgs])})
|
2015-09-25 14:53:25 +02:00
|
|
|
end.
|
|
|
|
|
2016-08-04 16:04:43 +02:00
|
|
|
format_arg({Elements},
|
|
|
|
{list, {_ElementDefName, {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]} = Tuple}})
|
|
|
|
when is_list(Elements) andalso
|
|
|
|
(Tuple1S == binary orelse Tuple1S == string) ->
|
|
|
|
lists:map(fun({F1, F2}) ->
|
|
|
|
{format_arg(F1, Tuple1S), format_arg(F2, Tuple2S)};
|
|
|
|
({Val}) when is_list(Val) ->
|
|
|
|
format_arg({Val}, Tuple)
|
|
|
|
end, Elements);
|
2024-07-15 21:29:17 +02:00
|
|
|
format_arg(Map,
|
|
|
|
{list, {_ElementDefName, {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]}}})
|
|
|
|
when is_map(Map) andalso
|
|
|
|
(Tuple1S == binary orelse Tuple1S == string) ->
|
|
|
|
maps:fold(
|
|
|
|
fun(K, V, Acc) ->
|
|
|
|
[{format_arg(K, Tuple1S), format_arg(V, Tuple2S)} | Acc]
|
|
|
|
end, [], Map);
|
2016-08-04 16:04:43 +02:00
|
|
|
format_arg(Elements,
|
|
|
|
{list, {_ElementDefName, {list, _} = ElementDefFormat}})
|
2015-09-25 14:53:25 +02:00
|
|
|
when is_list(Elements) ->
|
2016-08-04 16:04:43 +02:00
|
|
|
[{format_arg(Element, ElementDefFormat)}
|
|
|
|
|| Element <- Elements];
|
2023-11-23 16:33:36 +01:00
|
|
|
|
|
|
|
%% Covered by command_test_list and command_test_list_tuple
|
2016-07-15 16:42:13 +02:00
|
|
|
format_arg(Elements,
|
|
|
|
{list, {_ElementDefName, ElementDefFormat}})
|
2015-09-25 14:53:25 +02:00
|
|
|
when is_list(Elements) ->
|
2016-07-15 16:42:13 +02:00
|
|
|
[format_arg(Element, ElementDefFormat)
|
|
|
|
|| Element <- Elements];
|
2023-11-23 16:33:36 +01:00
|
|
|
|
2016-07-15 16:42:13 +02:00
|
|
|
format_arg({[{Name, Value}]},
|
|
|
|
{tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]})
|
|
|
|
when Tuple1S == binary;
|
|
|
|
Tuple1S == string ->
|
|
|
|
{format_arg(Name, Tuple1S), format_arg(Value, Tuple2S)};
|
2023-11-23 16:33:36 +01:00
|
|
|
|
|
|
|
%% Covered by command_test_tuple and command_test_list_tuple
|
|
|
|
format_arg(Elements,
|
|
|
|
{tuple, ElementsDef})
|
|
|
|
when is_map(Elements) ->
|
|
|
|
list_to_tuple([element(2, maps:find(atom_to_binary(Name, latin1), Elements))
|
|
|
|
|| {Name, _Format} <- ElementsDef]);
|
|
|
|
|
2016-07-15 16:42:13 +02:00
|
|
|
format_arg({Elements},
|
2015-09-25 14:53:25 +02:00
|
|
|
{tuple, ElementsDef})
|
|
|
|
when is_list(Elements) ->
|
2016-07-15 16:42:13 +02:00
|
|
|
F = lists:map(fun({TElName, TElDef}) ->
|
|
|
|
case lists:keyfind(atom_to_binary(TElName, latin1), 1, Elements) of
|
|
|
|
{_, Value} ->
|
|
|
|
format_arg(Value, TElDef);
|
|
|
|
_ when TElDef == binary; TElDef == string ->
|
|
|
|
<<"">>;
|
|
|
|
_ ->
|
2019-06-24 19:32:34 +02:00
|
|
|
?ERROR_MSG("Missing field ~p in tuple ~p", [TElName, Elements]),
|
2016-07-15 16:42:13 +02:00
|
|
|
throw({invalid_parameter,
|
|
|
|
io_lib:format("Missing field ~w in tuple ~w", [TElName, Elements])})
|
|
|
|
end
|
|
|
|
end, ElementsDef),
|
|
|
|
list_to_tuple(F);
|
2023-11-23 16:33:36 +01:00
|
|
|
|
2016-07-15 16:42:13 +02:00
|
|
|
format_arg(Elements, {list, ElementsDef})
|
2015-09-25 14:53:25 +02:00
|
|
|
when is_list(Elements) and is_atom(ElementsDef) ->
|
|
|
|
[format_arg(Element, ElementsDef)
|
|
|
|
|| Element <- Elements];
|
2023-11-23 16:33:36 +01:00
|
|
|
|
2015-09-25 14:53:25 +02:00
|
|
|
format_arg(Arg, integer) when is_integer(Arg) -> Arg;
|
|
|
|
format_arg(Arg, binary) when is_list(Arg) -> process_unicode_codepoints(Arg);
|
|
|
|
format_arg(Arg, binary) when is_binary(Arg) -> Arg;
|
2016-11-10 11:15:34 +01:00
|
|
|
format_arg(Arg, string) when is_list(Arg) -> Arg;
|
|
|
|
format_arg(Arg, string) when is_binary(Arg) -> binary_to_list(Arg);
|
2015-09-25 14:53:25 +02:00
|
|
|
format_arg(undefined, binary) -> <<>>;
|
2016-11-10 11:15:34 +01:00
|
|
|
format_arg(undefined, string) -> "";
|
2015-09-25 14:53:25 +02:00
|
|
|
format_arg(Arg, Format) ->
|
2019-06-24 19:32:34 +02:00
|
|
|
?ERROR_MSG("Don't know how to format Arg ~p for format ~p", [Arg, Format]),
|
2016-03-31 13:53:31 +02:00
|
|
|
throw({invalid_parameter,
|
2016-07-15 16:42:13 +02:00
|
|
|
io_lib:format("Arg ~w is not in format ~w",
|
2016-03-31 13:53:31 +02:00
|
|
|
[Arg, Format])}).
|
2015-09-25 14:53:25 +02:00
|
|
|
|
|
|
|
process_unicode_codepoints(Str) ->
|
|
|
|
iolist_to_binary(lists:map(fun(X) when X > 255 -> unicode:characters_to_binary([X]);
|
|
|
|
(Y) -> Y
|
|
|
|
end, Str)).
|
|
|
|
|
|
|
|
%% ----------------
|
|
|
|
%% internal helpers
|
|
|
|
%% ----------------
|
|
|
|
|
2016-03-31 13:53:31 +02:00
|
|
|
format_command_result(Cmd, Auth, Result, Version) ->
|
2019-06-18 11:09:35 +02:00
|
|
|
{_, _, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version),
|
2015-09-25 14:53:25 +02:00
|
|
|
case {ResultFormat, Result} of
|
2016-03-31 13:53:31 +02:00
|
|
|
{{_, rescode}, V} when V == true; V == ok ->
|
|
|
|
{200, 0};
|
|
|
|
{{_, rescode}, _} ->
|
|
|
|
{200, 1};
|
2016-07-30 13:08:30 +02:00
|
|
|
{_, {error, ErrorAtom, Code, Msg}} ->
|
|
|
|
format_error_result(ErrorAtom, Code, Msg);
|
2016-07-30 11:50:04 +02:00
|
|
|
{{_, restuple}, {V, Text}} when V == true; V == ok ->
|
|
|
|
{200, iolist_to_binary(Text)};
|
2016-07-30 13:08:30 +02:00
|
|
|
{{_, restuple}, {ErrorAtom, Msg}} ->
|
|
|
|
format_error_result(ErrorAtom, 0, Msg);
|
2016-03-31 13:53:31 +02:00
|
|
|
{{_, {list, _}}, _V} ->
|
|
|
|
{_, L} = format_result(Result, ResultFormat),
|
|
|
|
{200, L};
|
|
|
|
{{_, {tuple, _}}, _V} ->
|
|
|
|
{_, T} = format_result(Result, ResultFormat),
|
|
|
|
{200, T};
|
|
|
|
_ ->
|
2023-11-28 13:13:42 +01:00
|
|
|
OtherResult1 = format_result(Result, ResultFormat),
|
|
|
|
OtherResult2 = case Version of
|
|
|
|
0 ->
|
|
|
|
{[OtherResult1]};
|
|
|
|
_ ->
|
|
|
|
{_, Other3} = OtherResult1,
|
|
|
|
Other3
|
|
|
|
end,
|
|
|
|
{200, OtherResult2}
|
2015-09-25 14:53:25 +02:00
|
|
|
end.
|
|
|
|
|
|
|
|
format_result(Atom, {Name, atom}) ->
|
2017-04-11 12:13:58 +02:00
|
|
|
{misc:atom_to_binary(Name), misc:atom_to_binary(Atom)};
|
2015-09-25 14:53:25 +02:00
|
|
|
|
|
|
|
format_result(Int, {Name, integer}) ->
|
2017-04-11 12:13:58 +02:00
|
|
|
{misc:atom_to_binary(Name), Int};
|
2015-09-25 14:53:25 +02:00
|
|
|
|
2017-02-23 19:23:03 +01:00
|
|
|
format_result([String | _] = StringList, {Name, string}) when is_list(String) ->
|
|
|
|
Binarized = iolist_to_binary(string:join(StringList, "\n")),
|
2017-04-11 12:13:58 +02:00
|
|
|
{misc:atom_to_binary(Name), Binarized};
|
2017-02-23 19:23:03 +01:00
|
|
|
|
2015-09-25 14:53:25 +02:00
|
|
|
format_result(String, {Name, string}) ->
|
2017-04-11 12:13:58 +02:00
|
|
|
{misc:atom_to_binary(Name), iolist_to_binary(String)};
|
2015-09-25 14:53:25 +02:00
|
|
|
|
|
|
|
format_result(Code, {Name, rescode}) ->
|
2017-04-11 12:13:58 +02:00
|
|
|
{misc:atom_to_binary(Name), Code == true orelse Code == ok};
|
2015-09-25 14:53:25 +02:00
|
|
|
|
|
|
|
format_result({Code, Text}, {Name, restuple}) ->
|
2017-04-11 12:13:58 +02:00
|
|
|
{misc:atom_to_binary(Name),
|
2015-09-25 14:53:25 +02:00
|
|
|
{[{<<"res">>, Code == true orelse Code == ok},
|
|
|
|
{<<"text">>, iolist_to_binary(Text)}]}};
|
|
|
|
|
2016-08-04 16:04:43 +02:00
|
|
|
format_result(Code, {Name, restuple}) ->
|
2017-04-11 12:13:58 +02:00
|
|
|
{misc:atom_to_binary(Name),
|
2016-08-04 16:04:43 +02:00
|
|
|
{[{<<"res">>, Code == true orelse Code == ok},
|
|
|
|
{<<"text">>, <<"">>}]}};
|
|
|
|
|
2015-09-25 14:53:25 +02:00
|
|
|
format_result(Els, {Name, {list, {_, {tuple, [{_, atom}, _]}} = Fmt}}) ->
|
2017-04-11 12:13:58 +02:00
|
|
|
{misc:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}};
|
2015-09-25 14:53:25 +02:00
|
|
|
|
2018-12-03 13:52:53 +01:00
|
|
|
format_result(Els, {Name, {list, {_, {tuple, [{name, string}, {value, _}]}} = Fmt}}) ->
|
|
|
|
{misc:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}};
|
|
|
|
|
2023-11-23 16:33:36 +01:00
|
|
|
%% Covered by command_test_list and command_test_list_tuple
|
2015-09-25 14:53:25 +02:00
|
|
|
format_result(Els, {Name, {list, Def}}) ->
|
2017-04-11 12:13:58 +02:00
|
|
|
{misc:atom_to_binary(Name), [element(2, format_result(El, Def)) || El <- Els]};
|
2015-09-25 14:53:25 +02:00
|
|
|
|
|
|
|
format_result(Tuple, {_Name, {tuple, [{_, atom}, ValFmt]}}) ->
|
|
|
|
{Name2, Val} = Tuple,
|
|
|
|
{_, Val2} = format_result(Val, ValFmt),
|
2017-04-11 12:13:58 +02:00
|
|
|
{misc:atom_to_binary(Name2), Val2};
|
2015-09-25 14:53:25 +02:00
|
|
|
|
2018-12-03 13:52:53 +01:00
|
|
|
format_result(Tuple, {_Name, {tuple, [{name, string}, {value, _} = ValFmt]}}) ->
|
|
|
|
{Name2, Val} = Tuple,
|
|
|
|
{_, Val2} = format_result(Val, ValFmt),
|
|
|
|
{iolist_to_binary(Name2), Val2};
|
|
|
|
|
2023-11-23 16:33:36 +01:00
|
|
|
%% Covered by command_test_tuple and command_test_list_tuple
|
2015-09-25 14:53:25 +02:00
|
|
|
format_result(Tuple, {Name, {tuple, Def}}) ->
|
|
|
|
Els = lists:zip(tuple_to_list(Tuple), Def),
|
2023-11-23 16:33:36 +01:00
|
|
|
Els2 = [format_result(El, ElDef) || {El, ElDef} <- Els],
|
|
|
|
{misc:atom_to_binary(Name), maps:from_list(Els2)};
|
2015-09-25 14:53:25 +02:00
|
|
|
|
|
|
|
format_result(404, {_Name, _}) ->
|
|
|
|
"not_found".
|
|
|
|
|
2016-07-30 18:51:54 +02:00
|
|
|
|
2016-07-30 13:08:30 +02:00
|
|
|
format_error_result(conflict, Code, Msg) ->
|
|
|
|
{409, Code, iolist_to_binary(Msg)};
|
2018-10-19 10:30:05 +02:00
|
|
|
format_error_result(not_exists, Code, Msg) ->
|
|
|
|
{404, Code, iolist_to_binary(Msg)};
|
2016-07-30 13:08:30 +02:00
|
|
|
format_error_result(_ErrorAtom, Code, Msg) ->
|
|
|
|
{500, Code, iolist_to_binary(Msg)}.
|
|
|
|
|
2015-09-25 14:53:25 +02:00
|
|
|
unauthorized_response() ->
|
2016-10-05 13:21:11 +02:00
|
|
|
json_error(401, 10, <<"You are not authorized to call this command.">>).
|
|
|
|
|
|
|
|
invalid_token_response() ->
|
2016-07-20 20:50:59 +02:00
|
|
|
json_error(401, 10, <<"Oauth Token is invalid or expired.">>).
|
2015-09-25 14:53:25 +02:00
|
|
|
|
2016-11-25 07:48:26 +01:00
|
|
|
%% outofscope_response() ->
|
|
|
|
%% json_error(401, 11, <<"Token does not grant usage to command required scope.">>).
|
2015-09-25 14:53:25 +02:00
|
|
|
|
|
|
|
badrequest_response() ->
|
2016-03-31 13:53:31 +02:00
|
|
|
badrequest_response(<<"400 Bad Request">>).
|
|
|
|
badrequest_response(Body) ->
|
2024-05-06 18:02:36 +02:00
|
|
|
json_response(400, misc:json_encode(Body)).
|
2016-03-31 13:53:31 +02:00
|
|
|
|
2016-07-30 13:08:30 +02:00
|
|
|
json_format({Code, Result}) ->
|
2024-05-06 18:02:36 +02:00
|
|
|
json_response(Code, misc:json_encode(Result));
|
2016-07-30 13:08:30 +02:00
|
|
|
json_format({HTMLCode, JSONErrorCode, Message}) ->
|
|
|
|
json_error(HTMLCode, JSONErrorCode, Message).
|
|
|
|
|
2015-09-25 14:53:25 +02:00
|
|
|
json_response(Code, Body) when is_integer(Code) ->
|
|
|
|
{Code, ?HEADER(?CT_JSON), Body}.
|
|
|
|
|
2016-07-20 20:50:59 +02:00
|
|
|
%% HTTPCode, JSONCode = integers
|
|
|
|
%% message is binary
|
|
|
|
json_error(HTTPCode, JSONCode, Message) ->
|
|
|
|
{HTTPCode, ?HEADER(?CT_JSON),
|
2024-05-06 18:02:36 +02:00
|
|
|
misc:json_encode(#{<<"status">> => <<"error">>,
|
|
|
|
<<"code">> => JSONCode,
|
2024-05-06 17:07:26 +02:00
|
|
|
<<"message">> => Message})
|
2016-07-20 20:50:59 +02:00
|
|
|
}.
|
|
|
|
|
2015-09-25 14:53:25 +02:00
|
|
|
log(Call, Args, {Addr, Port}) ->
|
2017-04-11 12:13:58 +02:00
|
|
|
AddrS = misc:ip_to_list({Addr, Port}),
|
2019-09-23 14:17:20 +02:00
|
|
|
?INFO_MSG("API call ~ts ~p from ~ts:~p", [Call, hide_sensitive_args(Args), AddrS, Port]);
|
2016-03-30 14:23:09 +02:00
|
|
|
log(Call, Args, IP) ->
|
2019-09-23 14:17:20 +02:00
|
|
|
?INFO_MSG("API call ~ts ~p (~p)", [Call, hide_sensitive_args(Args), IP]).
|
2017-07-27 15:30:56 +02:00
|
|
|
|
|
|
|
hide_sensitive_args(Args=[_H|_T]) ->
|
2024-08-26 11:28:54 +02:00
|
|
|
lists:map(fun({<<"password">>, Password}) -> {<<"password">>, ejabberd_config:may_hide_data(Password)};
|
2017-07-27 15:30:56 +02:00
|
|
|
({<<"newpass">>,NewPassword}) -> {<<"newpass">>, ejabberd_config:may_hide_data(NewPassword)};
|
|
|
|
(E) -> E end,
|
|
|
|
Args);
|
|
|
|
hide_sensitive_args(NonListArgs) ->
|
|
|
|
NonListArgs.
|
2015-09-25 14:53:25 +02:00
|
|
|
|
2019-06-14 11:33:26 +02:00
|
|
|
mod_options(_) ->
|
|
|
|
[].
|
2020-01-08 10:24:51 +01:00
|
|
|
|
|
|
|
mod_doc() ->
|
|
|
|
#{desc =>
|
2022-02-14 21:38:36 +01:00
|
|
|
[?T("This module provides a ReST interface to call "
|
2024-03-14 12:38:03 +01:00
|
|
|
"_`../../developer/ejabberd-api/index.md|ejabberd API`_ "
|
2022-02-14 21:38:36 +01:00
|
|
|
"commands using JSON data."), "",
|
2020-04-08 18:49:41 +02:00
|
|
|
?T("To use this module, in addition to adding it to the 'modules' "
|
2021-08-23 13:40:19 +02:00
|
|
|
"section, you must also enable it in 'listen' -> 'ejabberd_http' -> "
|
2024-03-14 12:38:03 +01:00
|
|
|
"_`listen-options.md#request_handlers|request_handlers`_."), "",
|
2020-04-08 18:49:41 +02:00
|
|
|
?T("To use a specific API version N, when defining the URL path "
|
|
|
|
"in the request_handlers, add a 'vN'. "
|
|
|
|
"For example: '/api/v2: mod_http_api'"), "",
|
|
|
|
?T("To run a command, send a POST request to the corresponding "
|
2021-08-18 13:39:17 +02:00
|
|
|
"URL: 'http://localhost:5280/api/<command_name>'")],
|
|
|
|
example =>
|
|
|
|
["listen:",
|
|
|
|
" -",
|
|
|
|
" port: 5280",
|
|
|
|
" module: ejabberd_http",
|
|
|
|
" request_handlers:",
|
|
|
|
" /api: mod_http_api",
|
|
|
|
"",
|
|
|
|
"modules:",
|
|
|
|
" mod_http_api: {}"]}.
|