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>
|
|
|
|
|
%%%
|
|
|
|
|
%%%
|
2016-01-13 12:29:14 +01:00
|
|
|
|
%%% ejabberd, Copyright (C) 2002-2016 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.
|
|
|
|
|
%%%
|
|
|
|
|
%%%----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
%% Example config:
|
|
|
|
|
%%
|
|
|
|
|
%% in ejabberd_http listener
|
|
|
|
|
%% request_handlers:
|
|
|
|
|
%% "/api": mod_http_api
|
2016-03-29 11:21:53 +02:00
|
|
|
|
%%
|
2016-03-31 13:53:31 +02:00
|
|
|
|
%% To use a specific API version N, add a vN element in the URL path:
|
|
|
|
|
%% in ejabberd_http listener
|
|
|
|
|
%% request_handlers:
|
|
|
|
|
%% "/api/v2": mod_http_api
|
|
|
|
|
%%
|
2015-09-25 14:53:25 +02:00
|
|
|
|
%% Access rights are defined with:
|
2016-03-29 11:21:53 +02:00
|
|
|
|
%% commands_admin_access: configure
|
2015-09-25 14:53:25 +02:00
|
|
|
|
%% commands:
|
|
|
|
|
%% - add_commands: user
|
|
|
|
|
%%
|
|
|
|
|
%%
|
|
|
|
|
%% add_commands allow exporting a class of commands, from
|
|
|
|
|
%% open: methods is not risky and can be called by without any access check
|
|
|
|
|
%% restricted (default): the same, but will appear only in ejabberdctl list.
|
|
|
|
|
%% admin – auth is required with XMLRPC and HTTP API and checked for admin priviledges, works as usual in ejabberdctl.
|
|
|
|
|
%% user - can be used through XMLRPC and HTTP API, even by user. Only admin can use the commands for other users.
|
|
|
|
|
%%
|
|
|
|
|
%% Then to perform an action, send a POST request to the following URL:
|
|
|
|
|
%% http://localhost:5280/api/<call_name>
|
2016-03-29 19:40:20 +02:00
|
|
|
|
%%
|
|
|
|
|
%% It's also possible to enable unrestricted access to some commands from group
|
|
|
|
|
%% of IP addresses by using option `admin_ip_access` by having fragment like
|
|
|
|
|
%% this in configuration file:
|
|
|
|
|
%% modules:
|
|
|
|
|
%% mod_http_api:
|
|
|
|
|
%% admin_ip_access: admin_ip_access_rule
|
|
|
|
|
%%...
|
|
|
|
|
%% access:
|
|
|
|
|
%% admin_ip_access_rule:
|
|
|
|
|
%% admin_ip_acl:
|
|
|
|
|
%% - command1
|
|
|
|
|
%% - command2
|
|
|
|
|
%% %% use `all` to give access to all commands
|
|
|
|
|
%%...
|
|
|
|
|
%% acl:
|
|
|
|
|
%% admin_ip_acl:
|
|
|
|
|
%% ip:
|
|
|
|
|
%% - "127.0.0.1/8"
|
2015-09-25 14:53:25 +02:00
|
|
|
|
|
|
|
|
|
-module(mod_http_api).
|
|
|
|
|
|
|
|
|
|
-author('cromain@process-one.net').
|
|
|
|
|
|
|
|
|
|
-behaviour(gen_mod).
|
|
|
|
|
|
|
|
|
|
-export([start/2, stop/1, process/2, mod_opt_type/1]).
|
|
|
|
|
|
|
|
|
|
-include("ejabberd.hrl").
|
|
|
|
|
-include("jlib.hrl").
|
|
|
|
|
-include("logger.hrl").
|
|
|
|
|
-include("ejabberd_http.hrl").
|
|
|
|
|
|
2016-03-31 13:53:31 +02:00
|
|
|
|
-define(DEFAULT_API_VERSION, 0).
|
|
|
|
|
|
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">>,
|
|
|
|
|
<<"Content-Type">>}).
|
|
|
|
|
|
|
|
|
|
-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.
|
|
|
|
|
|
|
|
|
|
%% ----------
|
|
|
|
|
%% basic auth
|
|
|
|
|
%% ----------
|
|
|
|
|
|
2016-03-29 19:40:20 +02:00
|
|
|
|
check_permissions(Request, Command) ->
|
2015-09-25 14:53:25 +02:00
|
|
|
|
case catch binary_to_existing_atom(Command, utf8) of
|
|
|
|
|
Call when is_atom(Call) ->
|
2016-03-30 14:23:09 +02:00
|
|
|
|
{ok, CommandPolicy} = ejabberd_commands:get_command_policy(Call),
|
|
|
|
|
check_permissions2(Request, Call, CommandPolicy);
|
2016-03-29 19:40:20 +02:00
|
|
|
|
_ ->
|
|
|
|
|
unauthorized_response()
|
|
|
|
|
end.
|
|
|
|
|
|
2016-03-30 14:23:09 +02:00
|
|
|
|
check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _)
|
2016-03-29 19:40:20 +02:00
|
|
|
|
when HTTPAuth /= undefined ->
|
|
|
|
|
Admin =
|
|
|
|
|
case lists:keysearch(<<"X-Admin">>, 1, Headers) of
|
|
|
|
|
{value, {_, <<"true">>}} -> true;
|
|
|
|
|
_ -> false
|
|
|
|
|
end,
|
|
|
|
|
Auth =
|
|
|
|
|
case HTTPAuth of
|
|
|
|
|
{SJID, Pass} ->
|
|
|
|
|
case jid:from_string(SJID) of
|
|
|
|
|
#jid{user = User, server = Server} ->
|
|
|
|
|
case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of
|
|
|
|
|
true -> {ok, {User, Server, Pass, Admin}};
|
|
|
|
|
false -> false
|
2015-09-25 14:53:25 +02:00
|
|
|
|
end;
|
|
|
|
|
_ ->
|
|
|
|
|
false
|
2016-03-29 19:40:20 +02:00
|
|
|
|
end;
|
|
|
|
|
{oauth, Token, _} ->
|
|
|
|
|
case oauth_check_token(Call, Token) of
|
|
|
|
|
{ok, User, Server} ->
|
|
|
|
|
{ok, {User, Server, {oauth, Token}, Admin}};
|
|
|
|
|
false ->
|
|
|
|
|
false
|
|
|
|
|
end;
|
|
|
|
|
_ ->
|
|
|
|
|
false
|
|
|
|
|
end,
|
|
|
|
|
case Auth of
|
|
|
|
|
{ok, A} -> {allowed, Call, A};
|
|
|
|
|
_ -> unauthorized_response()
|
|
|
|
|
end;
|
2016-03-30 14:23:09 +02:00
|
|
|
|
check_permissions2(_Request, Call, open) ->
|
|
|
|
|
{allowed, Call, noauth};
|
|
|
|
|
check_permissions2(#request{ip={IP, _Port}}, Call, _Policy) ->
|
2016-03-29 19:40:20 +02:00
|
|
|
|
Access = gen_mod:get_module_opt(global, ?MODULE, admin_ip_access,
|
|
|
|
|
mod_opt_type(admin_ip_access),
|
|
|
|
|
none),
|
|
|
|
|
Res = acl:match_rule(global, Access, IP),
|
|
|
|
|
case Res of
|
|
|
|
|
all ->
|
|
|
|
|
{allowed, Call, admin};
|
|
|
|
|
[all] ->
|
|
|
|
|
{allowed, Call, admin};
|
|
|
|
|
allow ->
|
|
|
|
|
{allowed, Call, admin};
|
|
|
|
|
Commands when is_list(Commands) ->
|
|
|
|
|
case lists:member(Call, Commands) of
|
|
|
|
|
true -> {allowed, Call, admin};
|
2015-09-25 14:53:25 +02:00
|
|
|
|
_ -> unauthorized_response()
|
|
|
|
|
end;
|
2016-04-01 12:24:49 +02:00
|
|
|
|
E ->
|
|
|
|
|
?DEBUG("Unauthorized: ~p", [E]),
|
2015-09-25 14:53:25 +02:00
|
|
|
|
unauthorized_response()
|
2016-03-30 14:49:19 +02:00
|
|
|
|
end;
|
|
|
|
|
check_permissions2(_Request, _Call, _Policy) ->
|
|
|
|
|
unauthorized_response().
|
2016-03-29 19:40:20 +02:00
|
|
|
|
|
|
|
|
|
oauth_check_token(Scope, Token) when is_atom(Scope) ->
|
|
|
|
|
oauth_check_token(atom_to_binary(Scope, utf8), Token);
|
|
|
|
|
oauth_check_token(Scope, Token) ->
|
|
|
|
|
ejabberd_oauth:check_token(Scope, Token).
|
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">>);
|
2015-09-25 14:53:25 +02:00
|
|
|
|
process([Call], #request{method = 'POST', data = 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 jiffy:decode(Data) of
|
|
|
|
|
List when is_list(List) -> List;
|
|
|
|
|
{List} when is_list(List) -> List;
|
|
|
|
|
Other -> [Other]
|
|
|
|
|
end,
|
|
|
|
|
log(Call, Args, IP),
|
|
|
|
|
case check_permissions(Req, Call) of
|
|
|
|
|
{allowed, Cmd, Auth} ->
|
2016-03-31 13:53:31 +02:00
|
|
|
|
{Code, Result} = handle(Cmd, Auth, Args, Version),
|
2015-09-25 14:53:25 +02:00
|
|
|
|
json_response(Code, jiffy:encode(Result));
|
2016-03-30 14:23:09 +02:00
|
|
|
|
%% Warning: check_permission direcly formats 401 reply if not authorized
|
|
|
|
|
ErrorResponse ->
|
2015-09-25 14:53:25 +02:00
|
|
|
|
ErrorResponse
|
|
|
|
|
end
|
2016-03-31 13:53:31 +02:00
|
|
|
|
catch _:{error,{_,invalid_json}} = _Err ->
|
|
|
|
|
?DEBUG("Bad Request: ~p", [_Err]),
|
|
|
|
|
badrequest_response(<<"Invalid JSON input">>);
|
|
|
|
|
_:_Error ->
|
2016-04-01 11:13:48 +02:00
|
|
|
|
?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
|
|
|
|
|
badrequest_response()
|
2015-09-25 14:53:25 +02:00
|
|
|
|
end;
|
|
|
|
|
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),
|
|
|
|
|
case check_permissions(Req, Call) of
|
|
|
|
|
{allowed, Cmd, Auth} ->
|
2016-03-31 13:53:31 +02:00
|
|
|
|
{Code, Result} = handle(Cmd, Auth, Args, Version),
|
2015-09-25 14:53:25 +02:00
|
|
|
|
json_response(Code, jiffy:encode(Result));
|
2016-03-30 14:23:09 +02:00
|
|
|
|
%% Warning: check_permission direcly formats 401 reply if not authorized
|
2015-09-25 14:53:25 +02:00
|
|
|
|
ErrorResponse ->
|
|
|
|
|
ErrorResponse
|
|
|
|
|
end
|
2016-03-31 13:53:31 +02:00
|
|
|
|
catch _:_Error ->
|
|
|
|
|
?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
|
2015-09-25 14:53:25 +02:00
|
|
|
|
badrequest_response()
|
|
|
|
|
end;
|
|
|
|
|
process([], #request{method = 'OPTIONS', data = <<>>}) ->
|
|
|
|
|
{200, ?OPTIONS_HEADER, []};
|
|
|
|
|
process(_Path, Request) ->
|
|
|
|
|
?DEBUG("Bad Request: no handler ~p", [Request]),
|
|
|
|
|
badrequest_response().
|
|
|
|
|
|
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]) ->
|
|
|
|
|
case catch jlib:binary_to_integer(String) of
|
|
|
|
|
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
|
|
|
|
|
%% ----------------
|
|
|
|
|
|
|
|
|
|
% generic ejabberd command handler
|
2016-03-31 13:53:31 +02:00
|
|
|
|
handle(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
|
|
|
|
|
case ejabberd_commands:get_command_format(Call, Auth, Version) of
|
2015-09-25 14:53:25 +02:00
|
|
|
|
{ArgsSpec, _} when is_list(ArgsSpec) ->
|
|
|
|
|
Args2 = [{jlib:binary_to_atom(Key), Value} || {Key, Value} <- Args],
|
|
|
|
|
Spec = lists:foldr(
|
|
|
|
|
fun ({Key, binary}, Acc) ->
|
|
|
|
|
[{Key, <<>>}|Acc];
|
|
|
|
|
({Key, string}, Acc) ->
|
|
|
|
|
[{Key, <<>>}|Acc];
|
|
|
|
|
({Key, integer}, Acc) ->
|
|
|
|
|
[{Key, 0}|Acc];
|
|
|
|
|
({Key, {list, _}}, Acc) ->
|
|
|
|
|
[{Key, []}|Acc];
|
|
|
|
|
({Key, atom}, Acc) ->
|
|
|
|
|
[{Key, undefined}|Acc]
|
|
|
|
|
end, [], ArgsSpec),
|
2016-03-31 13:53:31 +02:00
|
|
|
|
try
|
|
|
|
|
handle2(Call, Auth, match(Args2, Spec), Version)
|
|
|
|
|
catch throw:not_found ->
|
|
|
|
|
{404, <<"not_found">>};
|
|
|
|
|
throw:{not_found, Why} when is_atom(Why) ->
|
|
|
|
|
{404, jlib: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, jlib:atom_to_binary(Why)};
|
|
|
|
|
throw:{not_allowed, Msg} ->
|
|
|
|
|
{401, iolist_to_binary(Msg)};
|
2016-03-31 17:34:50 +02:00
|
|
|
|
throw:{error, account_unprivileged} ->
|
|
|
|
|
{401, iolist_to_binary(<<"Unauthorized: Account Unpriviledged">>)};
|
2016-03-31 13:53:31 +02:00
|
|
|
|
throw:{invalid_parameter, Msg} ->
|
|
|
|
|
{400, iolist_to_binary(Msg)};
|
|
|
|
|
throw:{error, Why} when is_atom(Why) ->
|
|
|
|
|
{400, jlib:atom_to_binary(Why)};
|
|
|
|
|
throw:{error, Msg} ->
|
|
|
|
|
{400, iolist_to_binary(Msg)};
|
|
|
|
|
throw:Error when is_atom(Error) ->
|
|
|
|
|
{400, jlib:atom_to_binary(Error)};
|
|
|
|
|
throw:Msg when is_list(Msg); is_binary(Msg) ->
|
|
|
|
|
{400, iolist_to_binary(Msg)};
|
|
|
|
|
_Error ->
|
|
|
|
|
?ERROR_MSG("REST API Error: ~p ~p", [_Error, erlang:get_stacktrace()]),
|
|
|
|
|
{500, <<"internal_error">>}
|
|
|
|
|
end;
|
2015-09-25 14:53:25 +02:00
|
|
|
|
{error, Msg} ->
|
2016-03-31 13:53:31 +02:00
|
|
|
|
?ERROR_MSG("REST API Error: ~p", [Msg]),
|
2015-09-25 14:53:25 +02:00
|
|
|
|
{400, Msg};
|
|
|
|
|
_Error ->
|
2016-03-31 13:53:31 +02:00
|
|
|
|
?ERROR_MSG("REST API Error: ~p", [_Error]),
|
2015-09-25 14:53:25 +02:00
|
|
|
|
{400, <<"Error">>}
|
|
|
|
|
end.
|
|
|
|
|
|
2016-03-31 13:53:31 +02:00
|
|
|
|
handle2(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
|
|
|
|
|
{ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth, Version),
|
2015-09-25 14:53:25 +02:00
|
|
|
|
ArgsFormatted = format_args(Args, ArgsF),
|
2016-03-31 15:37:21 +02:00
|
|
|
|
ejabberd_command(Auth, Call, ArgsFormatted, Version).
|
2015-09-25 14:53:25 +02:00
|
|
|
|
|
|
|
|
|
get_elem_delete(A, L) ->
|
|
|
|
|
case proplists:get_all_values(A, L) of
|
|
|
|
|
[Value] -> {Value, proplists:delete(A, L)};
|
|
|
|
|
[_, _ | _] ->
|
|
|
|
|
%% Crash reporting the error
|
|
|
|
|
exit({duplicated_attribute, A, L});
|
|
|
|
|
[] ->
|
|
|
|
|
%% Report the error and then force a crash
|
|
|
|
|
exit({attribute_not_found, A, L})
|
|
|
|
|
end.
|
|
|
|
|
|
|
|
|
|
format_args(Args, ArgsFormat) ->
|
|
|
|
|
{ArgsRemaining, R} = lists:foldl(fun ({ArgName,
|
|
|
|
|
ArgFormat},
|
|
|
|
|
{Args1, Res}) ->
|
|
|
|
|
{ArgValue, Args2} =
|
|
|
|
|
get_elem_delete(ArgName,
|
|
|
|
|
Args1),
|
|
|
|
|
Formatted = format_arg(ArgValue,
|
|
|
|
|
ArgFormat),
|
|
|
|
|
{Args2, Res ++ [Formatted]}
|
|
|
|
|
end,
|
|
|
|
|
{Args, []}, ArgsFormat),
|
|
|
|
|
case ArgsRemaining of
|
|
|
|
|
[] -> R;
|
|
|
|
|
L when is_list(L) -> exit({additional_unused_args, L})
|
|
|
|
|
end.
|
|
|
|
|
|
|
|
|
|
format_arg({array, Elements},
|
|
|
|
|
{list, {ElementDefName, ElementDefFormat}})
|
|
|
|
|
when is_list(Elements) ->
|
|
|
|
|
lists:map(fun ({struct, [{ElementName, ElementValue}]}) when
|
|
|
|
|
ElementDefName == ElementName ->
|
|
|
|
|
format_arg(ElementValue, ElementDefFormat)
|
|
|
|
|
end,
|
|
|
|
|
Elements);
|
|
|
|
|
format_arg({array, [{struct, Elements}]},
|
|
|
|
|
{list, {ElementDefName, ElementDefFormat}})
|
|
|
|
|
when is_list(Elements) ->
|
|
|
|
|
lists:map(fun ({ElementName, ElementValue}) ->
|
|
|
|
|
true = ElementDefName == ElementName,
|
|
|
|
|
format_arg(ElementValue, ElementDefFormat)
|
|
|
|
|
end,
|
|
|
|
|
Elements);
|
|
|
|
|
format_arg({array, [{struct, Elements}]},
|
|
|
|
|
{tuple, ElementsDef})
|
|
|
|
|
when is_list(Elements) ->
|
|
|
|
|
FormattedList = format_args(Elements, ElementsDef),
|
|
|
|
|
list_to_tuple(FormattedList);
|
|
|
|
|
format_arg({array, Elements}, {list, ElementsDef})
|
|
|
|
|
when is_list(Elements) and is_atom(ElementsDef) ->
|
|
|
|
|
[format_arg(Element, ElementsDef)
|
|
|
|
|
|| Element <- Elements];
|
|
|
|
|
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;
|
|
|
|
|
format_arg(Arg, string) when is_list(Arg) -> process_unicode_codepoints(Arg);
|
|
|
|
|
format_arg(Arg, string) when is_binary(Arg) -> Arg;
|
|
|
|
|
format_arg(undefined, binary) -> <<>>;
|
|
|
|
|
format_arg(undefined, string) -> <<>>;
|
|
|
|
|
format_arg(Arg, Format) ->
|
|
|
|
|
?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,
|
|
|
|
|
io_lib:format("Arg ~p is not in format ~p",
|
|
|
|
|
[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
|
|
|
|
|
%% ----------------
|
|
|
|
|
|
|
|
|
|
match(Args, Spec) ->
|
|
|
|
|
[{Key, proplists:get_value(Key, Args, Default)} || {Key, Default} <- Spec].
|
|
|
|
|
|
2016-03-31 15:37:21 +02:00
|
|
|
|
ejabberd_command(Auth, Cmd, Args, Version) ->
|
2016-03-29 19:40:20 +02:00
|
|
|
|
Access = case Auth of
|
|
|
|
|
admin -> [];
|
|
|
|
|
_ -> undefined
|
|
|
|
|
end,
|
2016-03-31 15:37:21 +02:00
|
|
|
|
case ejabberd_commands:execute_command(Access, Auth, Cmd, Args, Version) of
|
|
|
|
|
{error, Error} ->
|
|
|
|
|
throw(Error);
|
|
|
|
|
Res ->
|
|
|
|
|
format_command_result(Cmd, Auth, Res, Version)
|
2015-09-25 14:53:25 +02:00
|
|
|
|
end.
|
|
|
|
|
|
2016-03-31 13:53:31 +02:00
|
|
|
|
format_command_result(Cmd, Auth, Result, Version) ->
|
|
|
|
|
{_, 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};
|
|
|
|
|
{{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok ->
|
|
|
|
|
{200, iolist_to_binary(Text1)};
|
|
|
|
|
{{_, restuple}, {_, Text2}} ->
|
|
|
|
|
{500, iolist_to_binary(Text2)};
|
|
|
|
|
{{_, {list, _}}, _V} ->
|
|
|
|
|
{_, L} = format_result(Result, ResultFormat),
|
|
|
|
|
{200, L};
|
|
|
|
|
{{_, {tuple, _}}, _V} ->
|
|
|
|
|
{_, T} = format_result(Result, ResultFormat),
|
|
|
|
|
{200, T};
|
|
|
|
|
_ ->
|
|
|
|
|
{200, {[format_result(Result, ResultFormat)]}}
|
2015-09-25 14:53:25 +02:00
|
|
|
|
end.
|
|
|
|
|
|
|
|
|
|
format_result(Atom, {Name, atom}) ->
|
|
|
|
|
{jlib:atom_to_binary(Name), jlib:atom_to_binary(Atom)};
|
|
|
|
|
|
|
|
|
|
format_result(Int, {Name, integer}) ->
|
|
|
|
|
{jlib:atom_to_binary(Name), Int};
|
|
|
|
|
|
|
|
|
|
format_result(String, {Name, string}) ->
|
|
|
|
|
{jlib:atom_to_binary(Name), iolist_to_binary(String)};
|
|
|
|
|
|
|
|
|
|
format_result(Code, {Name, rescode}) ->
|
|
|
|
|
{jlib:atom_to_binary(Name), Code == true orelse Code == ok};
|
|
|
|
|
|
|
|
|
|
format_result({Code, Text}, {Name, restuple}) ->
|
|
|
|
|
{jlib:atom_to_binary(Name),
|
|
|
|
|
{[{<<"res">>, Code == true orelse Code == ok},
|
|
|
|
|
{<<"text">>, iolist_to_binary(Text)}]}};
|
|
|
|
|
|
|
|
|
|
format_result(Els, {Name, {list, {_, {tuple, [{_, atom}, _]}} = Fmt}}) ->
|
|
|
|
|
{jlib:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}};
|
|
|
|
|
|
|
|
|
|
format_result(Els, {Name, {list, Def}}) ->
|
|
|
|
|
{jlib:atom_to_binary(Name), [element(2, format_result(El, Def)) || El <- Els]};
|
|
|
|
|
|
|
|
|
|
format_result(Tuple, {_Name, {tuple, [{_, atom}, ValFmt]}}) ->
|
|
|
|
|
{Name2, Val} = Tuple,
|
|
|
|
|
{_, Val2} = format_result(Val, ValFmt),
|
|
|
|
|
{jlib:atom_to_binary(Name2), Val2};
|
|
|
|
|
|
|
|
|
|
format_result(Tuple, {Name, {tuple, Def}}) ->
|
|
|
|
|
Els = lists:zip(tuple_to_list(Tuple), Def),
|
|
|
|
|
{jlib:atom_to_binary(Name), {[format_result(El, ElDef) || {El, ElDef} <- Els]}};
|
|
|
|
|
|
|
|
|
|
format_result(404, {_Name, _}) ->
|
|
|
|
|
"not_found".
|
|
|
|
|
|
|
|
|
|
unauthorized_response() ->
|
2016-03-31 13:53:31 +02:00
|
|
|
|
unauthorized_response(<<"401 Unauthorized">>).
|
|
|
|
|
unauthorized_response(Body) ->
|
|
|
|
|
json_response(401, jiffy:encode(Body)).
|
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) ->
|
|
|
|
|
json_response(400, jiffy:encode(Body)).
|
|
|
|
|
|
2015-09-25 14:53:25 +02:00
|
|
|
|
json_response(Code, Body) when is_integer(Code) ->
|
|
|
|
|
{Code, ?HEADER(?CT_JSON), Body}.
|
|
|
|
|
|
|
|
|
|
log(Call, Args, {Addr, Port}) ->
|
|
|
|
|
AddrS = jlib:ip_to_list({Addr, Port}),
|
2016-03-30 14:23:09 +02:00
|
|
|
|
?INFO_MSG("API call ~s ~p from ~s:~p", [Call, Args, AddrS, Port]);
|
|
|
|
|
log(Call, Args, IP) ->
|
|
|
|
|
?INFO_MSG("API call ~s ~p (~p)", [Call, Args, IP]).
|
2015-09-25 14:53:25 +02:00
|
|
|
|
|
2016-03-29 19:40:20 +02:00
|
|
|
|
mod_opt_type(admin_ip_access) ->
|
2015-09-25 14:53:25 +02:00
|
|
|
|
fun(Access) when is_atom(Access) -> Access end;
|
2016-03-29 19:40:20 +02:00
|
|
|
|
mod_opt_type(_) -> [admin_ip_access].
|