Merge pull request #1223 from processone/expand_api

More API fixes and improvements
This commit is contained in:
Mickaël Rémond 2016-08-01 15:36:47 +02:00 committed by GitHub
commit 2ef58a33a9
9 changed files with 131 additions and 80 deletions

View File

@ -51,7 +51,7 @@ mech_step(State, ClientIn) ->
{ok, {ok,
[{username, User}, {authzid, AuthzId}, [{username, User}, {authzid, AuthzId},
{auth_module, ejabberd_oauth}]}; {auth_module, ejabberd_oauth}]};
false -> _ ->
{error, <<"not-authorized">>, User} {error, <<"not-authorized">>, User}
end; end;
_ -> {error, <<"bad-protocol">>} _ -> {error, <<"bad-protocol">>}

View File

@ -87,6 +87,7 @@ get_commands_spec() ->
args = [], result = {res, rescode}}, args = [], result = {res, rescode}},
#ejabberd_commands{name = reopen_log, tags = [logs, server], #ejabberd_commands{name = reopen_log, tags = [logs, server],
desc = "Reopen the log files", desc = "Reopen the log files",
policy = admin,
module = ?MODULE, function = reopen_log, module = ?MODULE, function = reopen_log,
args = [], result = {res, rescode}}, args = [], result = {res, rescode}},
#ejabberd_commands{name = rotate_log, tags = [logs, server], #ejabberd_commands{name = rotate_log, tags = [logs, server],
@ -380,13 +381,12 @@ register(User, Host, Password) ->
{atomic, ok} -> {atomic, ok} ->
{ok, io_lib:format("User ~s@~s successfully registered", [User, Host])}; {ok, io_lib:format("User ~s@~s successfully registered", [User, Host])};
{atomic, exists} -> {atomic, exists} ->
String = io_lib:format("User ~s@~s already registered at node ~p", Msg = io_lib:format("User ~s@~s already registered", [User, Host]),
[User, Host, node()]), {error, conflict, 10090, Msg};
{exists, String};
{error, Reason} -> {error, Reason} ->
String = io_lib:format("Can't register user ~s@~s at node ~p: ~p", String = io_lib:format("Can't register user ~s@~s at node ~p: ~p",
[User, Host, node(), Reason]), [User, Host, node(), Reason]),
{cannot_register, String} {error, cannot_register, 10001, String}
end. end.
unregister(User, Host) -> unregister(User, Host) ->

View File

@ -425,7 +425,7 @@ get_command_definition(Name, Version) ->
{V, C} {V, C}
end)))) of end)))) of
[{_, Command} | _ ] -> Command; [{_, Command} | _ ] -> Command;
_E -> throw(unknown_command) _E -> throw({error, unknown_command})
end. end.
-spec get_commands_definition(integer()) -> [ejabberd_commands()]. -spec get_commands_definition(integer()) -> [ejabberd_commands()].
@ -682,7 +682,7 @@ check_auth(Command, {User, Server, {oauth, Token}, _}) ->
case ejabberd_oauth:check_token(User, Server, ScopeList, Token) of case ejabberd_oauth:check_token(User, Server, ScopeList, Token) of
true -> true ->
{ok, User, Server}; {ok, User, Server};
false -> _ ->
throw({error, invalid_account_data}) throw({error, invalid_account_data})
end; end;
check_auth(_Command, {User, Server, Password, _}) when is_binary(Password) -> check_auth(_Command, {User, Server, Password, _}) when is_binary(Password) ->

View File

@ -212,7 +212,7 @@ process(["help" | Mode], Version) ->
end; end;
process(["--version", Arg | Args], _) -> process(["--version", Arg | Args], _) ->
Version = Version =
try try
list_to_integer(Arg) list_to_integer(Arg)
catch _:_ -> catch _:_ ->
@ -321,7 +321,7 @@ call_command([CmdString | Args], Auth, AccessCommands, Version) ->
{ArgsFormat, ResultFormat} -> {ArgsFormat, ResultFormat} ->
case (catch format_args(Args, ArgsFormat)) of case (catch format_args(Args, ArgsFormat)) of
ArgsFormatted when is_list(ArgsFormatted) -> ArgsFormatted when is_list(ArgsFormatted) ->
Result = ejabberd_commands:execute_command(AccessCommands, Result = ejabberd_commands:execute_command(AccessCommands,
Auth, Command, Auth, Command,
ArgsFormatted, ArgsFormatted,
Version), Version),
@ -374,6 +374,12 @@ format_arg2(Arg, Parse)->
format_result({error, ErrorAtom}, _) -> format_result({error, ErrorAtom}, _) ->
{io_lib:format("Error: ~p", [ErrorAtom]), make_status(error)}; {io_lib:format("Error: ~p", [ErrorAtom]), make_status(error)};
%% An error should always be allowed to return extended error to help with API.
%% Extended error is of the form:
%% {error, type :: atom(), code :: int(), Desc :: string()}
format_result({error, ErrorAtom, Code, _Msg}, _) ->
{io_lib:format("Error: ~p", [ErrorAtom]), make_status(Code)};
format_result(Atom, {_Name, atom}) -> format_result(Atom, {_Name, atom}) ->
io_lib:format("~p", [Atom]); io_lib:format("~p", [Atom]);
@ -433,6 +439,8 @@ format_result(404, {_Name, _}) ->
make_status(ok) -> ?STATUS_SUCCESS; make_status(ok) -> ?STATUS_SUCCESS;
make_status(true) -> ?STATUS_SUCCESS; make_status(true) -> ?STATUS_SUCCESS;
make_status(Code) when is_integer(Code), Code > 255 -> ?STATUS_ERROR;
make_status(Code) when is_integer(Code), Code > 0 -> Code;
make_status(_Error) -> ?STATUS_ERROR. make_status(_Error) -> ?STATUS_ERROR.
get_list_commands(Version) -> get_list_commands(Version) ->

View File

@ -302,12 +302,17 @@ check_token(User, Server, ScopeList, Token) ->
expire = Expire} -> expire = Expire} ->
{MegaSecs, Secs, _} = os:timestamp(), {MegaSecs, Secs, _} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs, TS = 1000000 * MegaSecs + Secs,
TokenScopeSet = oauth2_priv_set:new(TokenScope), if
lists:any(fun(Scope) -> Expire > TS ->
oauth2_priv_set:is_member(Scope, TokenScopeSet) end, TokenScopeSet = oauth2_priv_set:new(TokenScope),
ScopeList) andalso Expire > TS; lists:any(fun(Scope) ->
oauth2_priv_set:is_member(Scope, TokenScopeSet) end,
ScopeList);
true ->
{false, expired}
end;
_ -> _ ->
false {false, not_found}
end. end.
check_token(ScopeList, Token) -> check_token(ScopeList, Token) ->
@ -318,15 +323,20 @@ check_token(ScopeList, Token) ->
expire = Expire} -> expire = Expire} ->
{MegaSecs, Secs, _} = os:timestamp(), {MegaSecs, Secs, _} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs, TS = 1000000 * MegaSecs + Secs,
TokenScopeSet = oauth2_priv_set:new(TokenScope), if
case lists:any(fun(Scope) -> Expire > TS ->
oauth2_priv_set:is_member(Scope, TokenScopeSet) end, TokenScopeSet = oauth2_priv_set:new(TokenScope),
ScopeList) andalso Expire > TS of case lists:any(fun(Scope) ->
true -> {ok, user, US}; oauth2_priv_set:is_member(Scope, TokenScopeSet) end,
false -> false ScopeList) of
true -> {ok, user, US};
false -> {false, no_matching_scope}
end;
true ->
{false, expired}
end; end;
_ -> _ ->
false {false, not_found}
end. end.

View File

@ -473,28 +473,34 @@ send_element(Pid, El) ->
%%% ejabberd commands %%% ejabberd commands
get_commands_spec() -> get_commands_spec() ->
[#ejabberd_commands{name = incoming_s2s_number, [#ejabberd_commands{
tags = [stats, s2s], name = incoming_s2s_number,
desc = tags = [stats, s2s],
"Number of incoming s2s connections on " desc = "Number of incoming s2s connections on the node",
"the node", policy = admin,
policy = admin, module = ?MODULE, function = incoming_s2s_number,
module = ?MODULE, function = incoming_s2s_number, args = [], result = {s2s_incoming, integer}},
args = [], result = {s2s_incoming, integer}}, #ejabberd_commands{
#ejabberd_commands{name = outgoing_s2s_number, name = outgoing_s2s_number,
tags = [stats, s2s], tags = [stats, s2s],
desc = desc = "Number of outgoing s2s connections on the node",
"Number of outgoing s2s connections on " policy = admin,
"the node", module = ?MODULE, function = outgoing_s2s_number,
policy = admin, args = [], result = {s2s_outgoing, integer}}].
module = ?MODULE, function = outgoing_s2s_number,
args = [], result = {s2s_outgoing, integer}}].
%% TODO Move those stats commands to ejabberd stats command ?
incoming_s2s_number() -> incoming_s2s_number() ->
length(supervisor:which_children(ejabberd_s2s_in_sup)). supervisor_count(ejabberd_s2s_in_sup).
outgoing_s2s_number() -> outgoing_s2s_number() ->
length(supervisor:which_children(ejabberd_s2s_out_sup)). supervisor_count(ejabberd_s2s_out_sup).
supervisor_count(Supervisor) ->
case catch supervisor:which_children(Supervisor) of
{'EXIT', _} -> 0;
Result ->
length(Result)
end.
%%%---------------------------------------------------------------------- %%%----------------------------------------------------------------------
%%% Update Mnesia tables %%% Update Mnesia tables

View File

@ -535,7 +535,7 @@ get_commands_spec() ->
policy = user, policy = user,
module = mod_offline, function = count_offline_messages, module = mod_offline, function = count_offline_messages,
args = [], args = [],
result = {res, integer}}, result = {value, integer}},
#ejabberd_commands{name = send_message, tags = [stanza], #ejabberd_commands{name = send_message, tags = [stanza],
desc = "Send a message to a local or remote bare of full JID", desc = "Send a message to a local or remote bare of full JID",
module = ?MODULE, function = send_message, module = ?MODULE, function = send_message,

View File

@ -162,14 +162,15 @@ check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _, ScopeL
case oauth_check_token(ScopeList, Token) of case oauth_check_token(ScopeList, Token) of
{ok, user, {User, Server}} -> {ok, user, {User, Server}} ->
{ok, {User, Server, {oauth, Token}, Admin}}; {ok, {User, Server, {oauth, Token}, Admin}};
false -> {false, Reason} ->
false {false, Reason}
end; end;
_ -> _ ->
false false
end, end,
case Auth of case Auth of
{ok, A} -> {allowed, Call, A}; {ok, A} -> {allowed, Call, A};
{false, no_matching_scope} -> outofscope_response();
_ -> unauthorized_response() _ -> unauthorized_response()
end; end;
check_permissions2(_Request, Call, open, _Scope) -> check_permissions2(_Request, Call, open, _Scope) ->
@ -189,7 +190,7 @@ check_permissions2(#request{ip={IP, _Port}}, Call, _Policy, _Scope) ->
Commands when is_list(Commands) -> Commands when is_list(Commands) ->
case lists:member(Call, Commands) of case lists:member(Call, Commands) of
true -> {allowed, Call, admin}; true -> {allowed, Call, admin};
_ -> unauthorized_response() _ -> outofscope_response()
end; end;
_E -> _E ->
{allowed, Call, noauth} {allowed, Call, noauth}
@ -212,28 +213,24 @@ process(_, #request{method = 'POST', data = <<>>}) ->
process([Call], #request{method = 'POST', data = Data, ip = {IP, _} = IPPort} = Req) -> process([Call], #request{method = 'POST', data = Data, ip = {IP, _} = IPPort} = Req) ->
Version = get_api_version(Req), Version = get_api_version(Req),
try try
Args = case jiffy:decode(Data) of Args = extract_args(Data),
List when is_list(List) -> List;
{List} when is_list(List) -> List;
Other -> [Other]
end,
log(Call, Args, IPPort), log(Call, Args, IPPort),
case check_permissions(Req, Call) of case check_permissions(Req, Call) of
{allowed, Cmd, Auth} -> {allowed, Cmd, Auth} ->
case handle(Cmd, Auth, Args, Version, IP) of Result = handle(Cmd, Auth, Args, Version, IP),
{Code, Result} -> json_format(Result);
json_response(Code, jiffy:encode(Result));
{HTMLCode, JSONErrorCode, Message} ->
json_error(HTMLCode, JSONErrorCode, Message)
end;
%% Warning: check_permission direcly formats 401 reply if not authorized %% Warning: check_permission direcly formats 401 reply if not authorized
ErrorResponse -> ErrorResponse ->
ErrorResponse ErrorResponse
end end
catch _:{error,{_,invalid_json}} = _Err -> catch
?DEBUG("Bad Request: ~p", [_Err]), %% TODO We need to refactor to remove redundant error return formatting
badrequest_response(<<"Invalid JSON input">>); throw:{error, unknown_command} ->
_:_Error -> {404, 40, <<"Command not found.">>};
_:{error,{_,invalid_json}} = _Err ->
?DEBUG("Bad Request: ~p", [_Err]),
badrequest_response(<<"Invalid JSON input">>);
_:_Error ->
?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]), ?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
badrequest_response() badrequest_response()
end; end;
@ -247,13 +244,18 @@ process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) ->
log(Call, Args, IP), log(Call, Args, IP),
case check_permissions(Req, Call) of case check_permissions(Req, Call) of
{allowed, Cmd, Auth} -> {allowed, Cmd, Auth} ->
{Code, Result} = handle(Cmd, Auth, Args, Version, IP), Result = handle(Cmd, Auth, Args, Version, IP),
json_response(Code, jiffy:encode(Result)); json_format(Result);
%% Warning: check_permission direcly formats 401 reply if not authorized %% Warning: check_permission direcly formats 401 reply if not authorized
ErrorResponse -> ErrorResponse ->
ErrorResponse ErrorResponse
end end
catch _:_Error -> catch
%% TODO We need to refactor to remove redundant error return formatting
throw:{error, unknown_command} ->
json_format({404, 44, <<"Command not found.">>});
_:_Error ->
?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]), ?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
badrequest_response() badrequest_response()
end; end;
@ -261,7 +263,16 @@ process([], #request{method = 'OPTIONS', data = <<>>}) ->
{200, ?OPTIONS_HEADER, []}; {200, ?OPTIONS_HEADER, []};
process(_Path, Request) -> process(_Path, Request) ->
?DEBUG("Bad Request: no handler ~p", [Request]), ?DEBUG("Bad Request: no handler ~p", [Request]),
badrequest_response(). json_error(400, 40, <<"Missing command name.">>).
%% Be tolerant to make API more easily usable from command-line pipe.
extract_args(<<"\n">>) -> [];
extract_args(Data) ->
case jiffy:decode(Data) of
List when is_list(List) -> List;
{List} when is_list(List) -> List;
Other -> [Other]
end.
% get API version N from last "vN" element in URL path % get API version N from last "vN" element in URL path
get_api_version(#request{path = Path}) -> get_api_version(#request{path = Path}) ->
@ -302,7 +313,7 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
[{Key, undefined}|Acc] [{Key, undefined}|Acc]
end, [], ArgsSpec), end, [], ArgsSpec),
try try
handle2(Call, Auth, match(Args2, Spec), Version, IP) handle2(Call, Auth, match(Args2, Spec), Version, IP)
catch throw:not_found -> catch throw:not_found ->
{404, <<"not_found">>}; {404, <<"not_found">>};
throw:{not_found, Why} when is_atom(Why) -> throw:{not_found, Why} when is_atom(Why) ->
@ -444,22 +455,24 @@ ejabberd_command(Auth, Cmd, Args, Version, IP) ->
format_command_result(Cmd, Auth, Result, Version) -> format_command_result(Cmd, Auth, Result, Version) ->
{_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version), {_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version),
case {ResultFormat, Result} of case {ResultFormat, Result} of
{{_, rescode}, V} when V == true; V == ok -> {{_, rescode}, V} when V == true; V == ok ->
{200, 0}; {200, 0};
{{_, rescode}, _} -> {{_, rescode}, _} ->
{200, 1}; {200, 1};
{{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok -> {_, {error, ErrorAtom, Code, Msg}} ->
{200, iolist_to_binary(Text1)}; format_error_result(ErrorAtom, Code, Msg);
{{_, restuple}, {_, Text2}} -> {{_, restuple}, {V, Text}} when V == true; V == ok ->
{500, iolist_to_binary(Text2)}; {200, iolist_to_binary(Text)};
{{_, {list, _}}, _V} -> {{_, restuple}, {ErrorAtom, Msg}} ->
{_, L} = format_result(Result, ResultFormat), format_error_result(ErrorAtom, 0, Msg);
{200, L}; {{_, {list, _}}, _V} ->
{{_, {tuple, _}}, _V} -> {_, L} = format_result(Result, ResultFormat),
{_, T} = format_result(Result, ResultFormat), {200, L};
{200, T}; {{_, {tuple, _}}, _V} ->
_ -> {_, T} = format_result(Result, ResultFormat),
{200, {[format_result(Result, ResultFormat)]}} {200, T};
_ ->
{200, {[format_result(Result, ResultFormat)]}}
end. end.
format_result(Atom, {Name, atom}) -> format_result(Atom, {Name, atom}) ->
@ -497,14 +510,28 @@ format_result(Tuple, {Name, {tuple, Def}}) ->
format_result(404, {_Name, _}) -> format_result(404, {_Name, _}) ->
"not_found". "not_found".
format_error_result(conflict, Code, Msg) ->
{409, Code, iolist_to_binary(Msg)};
format_error_result(_ErrorAtom, Code, Msg) ->
{500, Code, iolist_to_binary(Msg)}.
unauthorized_response() -> unauthorized_response() ->
json_error(401, 10, <<"Oauth Token is invalid or expired.">>). json_error(401, 10, <<"Oauth Token is invalid or expired.">>).
outofscope_response() ->
json_error(401, 11, <<"Token does not grant usage to command required scope.">>).
badrequest_response() -> badrequest_response() ->
badrequest_response(<<"400 Bad Request">>). badrequest_response(<<"400 Bad Request">>).
badrequest_response(Body) -> badrequest_response(Body) ->
json_response(400, jiffy:encode(Body)). json_response(400, jiffy:encode(Body)).
json_format({Code, Result}) ->
json_response(Code, jiffy:encode(Result));
json_format({HTMLCode, JSONErrorCode, Message}) ->
json_error(HTMLCode, JSONErrorCode, Message).
json_response(Code, Body) when is_integer(Code) -> json_response(Code, Body) when is_integer(Code) ->
{Code, ?HEADER(?CT_JSON), Body}. {Code, ?HEADER(?CT_JSON), Body}.

View File

@ -174,7 +174,7 @@ defmodule EjabberdCommandsMockTest do
# default version is latest one # default version is latest one
assert :result3 == :ejabberd_commands.execute_command(command_name, []) assert :result3 == :ejabberd_commands.execute_command(command_name, [])
# no such command in APIv0 # no such command in APIv0
assert :unknown_command == assert {:error, :unknown_command} ==
catch_throw :ejabberd_commands.execute_command(command_name, [], 0) catch_throw :ejabberd_commands.execute_command(command_name, [], 0)
assert :result1 == :ejabberd_commands.execute_command(command_name, [], 1) assert :result1 == :ejabberd_commands.execute_command(command_name, [], 1)
assert :result1 == :ejabberd_commands.execute_command(command_name, [], 2) assert :result1 == :ejabberd_commands.execute_command(command_name, [], 2)