25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-11-24 16:23:40 +01:00

Commands refactor, first pass.

- add API versionning
- changed error handling, based on exception
- commands moved/merged from mod_admin_p1 to mod_admin_extra
- command bufixes
- add some elixir unit test cases

Squashed commit of the following:

commit dd59855b3486f78a9349756e4f102e79b3accff8
Merge: 14e8ffc 506e08e
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Fri Oct 30 11:43:18 2015 +0100

    Merge branch '3.2.x' into api

commit 14e8ffce78cbea6c8605371d1fc50a0c1d1e012c
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Tue Oct 27 16:35:17 2015 +0100

    Added OAuth tests to ejabberd_commands

commit f81c550c14628edfe4861c228576cb767924366a
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Tue Oct 27 16:34:55 2015 +0100

    Added some mod_http_api tests

commit 6a64578d5b2ba532a2feb6503ed98561e56d5d53
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Mon Oct 26 15:29:36 2015 +0100

    Fix get_last command test

    Previous version won't work with dst.

commit 27e0cde9e9c1f001effe68f8424a365ad947c068
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Fri Oct 23 17:59:34 2015 +0200

    Add tests on admin command policy

commit 19dad8d54f54c9fabd454280483cccfb06c8e78a
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Fri Oct 23 16:49:36 2015 +0200

    Added command related tests (http api & user policy)

commit e0e596ab4a3f3a70aba5f374f028939ab794de33
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Fri Oct 23 16:49:16 2015 +0200

    Fix command call.

commit 128cd7d1ede3c47a34f8ec3a750c980ccad2c61d
Merge: 60c4c4c 447313c
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Thu Oct 22 14:48:39 2015 +0200

    Merge branch '3.2.x' into api

commit 60c4c4c0751302524c14219c6bc8c56a6069a689
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Thu Oct 22 14:45:57 2015 +0200

    Fix ejabberd_commands spec.

commit 8e145c28c5da762c2b93ee32327eff1db94ebfed
Merge: 397273a f13dc94
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Wed Oct 21 18:26:07 2015 +0200

    Merge branch '3.2.x' into api

commit 397273a23ed415feac87aed33da6452229793387
Merge: c30e89b f289e27
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Wed Oct 21 15:27:45 2015 +0200

    Merge branch '3.2.x' into api

commit c30e89bb8a0013bff37e61e4c6953350c9c1f313
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Wed Oct 21 12:47:02 2015 +0200

    Merge mod_http_api

commit 7b0db22b4acd48ff6fabce41c1b2525e6580a3c5
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Fri Oct 16 11:55:48 2015 +0200

    Fix exunit tests to run with common_test suites

commit d8b1a89800ac7379a57a7eb4a09c3c93c3e1e5eb
Merge: 2879ae8 63455b3
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Thu Oct 15 11:39:45 2015 +0200

    Merge branch '3.2.x' into api

commit 2879ae87ff3eee369ef3d780136b96ecff5285d1
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Wed Oct 14 14:53:44 2015 +0200

    Fix update_roster command.

commit a1d453dd7a3afda9861a8d747494a45057ad574b
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Tue Oct 13 16:14:28 2015 +0200

    API commands refactor

    Moving and/or merging commands from mod_admin_p1 to mod_admin_extra

commit b709ed26b0fc0ca4f3bdd5a59fa58ec7e3db97fa
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Wed Oct 7 15:10:01 2015 +0200

    Add tests on commands

commit 6711687bee9c672cb3d5aed0744e13420ecf6dbd
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Tue Sep 29 15:58:16 2015 +0200

    Add ejabberd_commands tests

commit df8682f419cf3877e77e36a19bca0fc55dc991f8
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Mon Sep 28 14:54:39 2015 +0200

    Added API versioning for ejabberdctl and rest commands

commit cd017b0e3aac431bc3ee807ceb7f8641e1523ef5
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Fri Sep 18 11:21:45 2015 +0200

    Better error handling of HTTP API commands.

commit ca5cb6acd8e4643f9d6c484d2277b0d7e88471e5
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Tue Sep 15 15:03:05 2015 +0200

    add commands to mod_admin_extra:
    - get_offline_count
    - get_presence
    - change_password

commit 7f583fa099e30ac2b0915669fd8f102ac565b833
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Tue Sep 15 15:02:16 2015 +0200

    Improve REST API error handling

commit 14753b1c02cdce434a786b7f80f6c09f0d210075
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Mon Sep 14 10:51:17 2015 +0200

    Change REST API return codes for integer type.
This commit is contained in:
Alexey Shchepin 2016-03-31 14:53:31 +03:00
parent c00cfca8e7
commit 3dc55c6d47
18 changed files with 2962 additions and 395 deletions

View File

@ -336,6 +336,9 @@ test:
quicktest: quicktest:
$(REBAR) skip_deps=true ct suites=elixir $(REBAR) skip_deps=true ct suites=elixir
eunit:
$(REBAR) skip_deps=true exunit
.PHONY: src edoc dialyzer Makefile TAGS clean clean-rel distclean rel \ .PHONY: src edoc dialyzer Makefile TAGS clean clean-rel distclean rel \
install uninstall uninstall-binary uninstall-all translations deps test spec \ install uninstall uninstall-binary uninstall-all translations deps test spec \
quicktest erlang_plt deps_plt ejabberd_plt quicktest erlang_plt deps_plt ejabberd_plt

View File

@ -31,8 +31,10 @@
tags = [] :: [atom()] | '_' | '$2', tags = [] :: [atom()] | '_' | '$2',
desc = "" :: string() | '_' | '$3', desc = "" :: string() | '_' | '$3',
longdesc = "" :: string() | '_', longdesc = "" :: string() | '_',
module :: atom(), version = 0 :: integer(),
function :: atom(), jabs = 1 :: integer(),
module :: atom() | '_',
function :: atom() | '_',
args = [] :: [aterm()] | '_' | '$1' | '$2', args = [] :: [aterm()] | '_' | '$1' | '$2',
policy = restricted :: open | restricted | admin | user, policy = restricted :: open | restricted | admin | user,
result = {res, rescode} :: rterm() | '_' | '$2', result = {res, rescode} :: rterm() | '_' | '$2',

View File

@ -43,6 +43,8 @@
{tag, "1.0.0"}}}}, {tag, "1.0.0"}}}},
{if_var_true, tools, {meck, "0.8.2", {git, "https://github.com/eproxus/meck", {if_var_true, tools, {meck, "0.8.2", {git, "https://github.com/eproxus/meck",
{tag, "0.8.2"}}}}, {tag, "0.8.2"}}}},
{if_var_true, tools, {moka, ".*", {git, "git://github.com/processone/moka.git",
{tag, "1.0.5"}}}},
{if_var_true, redis, {eredis, ".*", {git, "https://github.com/wooga/eredis", {if_var_true, redis, {eredis, ".*", {git, "https://github.com/wooga/eredis",
{tag, "v1.0.8"}}}}]}. {tag, "v1.0.8"}}}}]}.

View File

@ -90,7 +90,8 @@
%%% PowFloat = math:pow(Base, Exponent), %%% PowFloat = math:pow(Base, Exponent),
%%% round(PowFloat).</pre> %%% round(PowFloat).</pre>
%%% %%%
%%% Since this function will be called by ejabberd_commands, it must be exported. %%% Since this function will be called by ejabberd_commands, it must
%%% be exported.
%%% Add to your module: %%% Add to your module:
%%% <pre>-export([calc_power/2]).</pre> %%% <pre>-export([calc_power/2]).</pre>
%%% %%%
@ -201,24 +202,33 @@
%%% TODO: consider this feature: %%% TODO: consider this feature:
%%% All commands are catched. If an error happens, return the restuple: %%% All commands are catched. If an error happens, return the restuple:
%%% {error, flattened error string} %%% {error, flattened error string}
%%% This means that ecomm call APIs (ejabberd_ctl, ejabberd_xmlrpc) need to allows this. %%% This means that ecomm call APIs (ejabberd_ctl, ejabberd_xmlrpc)
%%% And ejabberd_xmlrpc must be prepared to handle such an unexpected response. %%% need to allows this. And ejabberd_xmlrpc must be prepared to
%%% handle such an unexpected response.
-module(ejabberd_commands). -module(ejabberd_commands).
-author('badlop@process-one.net'). -author('badlop@process-one.net').
-define(DEFAULT_VERSION, 1000000).
-export([init/0, -export([init/0,
list_commands/0, list_commands/0,
list_commands/1,
get_command_format/1, get_command_format/1,
get_command_format/2, get_command_format/2,
get_command_format/3,
get_command_definition/1, get_command_definition/1,
get_command_definition/2,
get_tags_commands/0, get_tags_commands/0,
get_tags_commands/1,
get_commands/0, get_commands/0,
register_commands/1, register_commands/1,
unregister_commands/1, unregister_commands/1,
execute_command/2, execute_command/2,
execute_command/3,
execute_command/4, execute_command/4,
execute_command/5,
opt_type/1, opt_type/1,
get_commands_spec/0 get_commands_spec/0
]). ]).
@ -226,6 +236,7 @@
-include("ejabberd_commands.hrl"). -include("ejabberd_commands.hrl").
-include("ejabberd.hrl"). -include("ejabberd.hrl").
-include("logger.hrl"). -include("logger.hrl").
-include_lib("stdlib/include/ms_transform.hrl").
-define(POLICY_ACCESS, '$policy'). -define(POLICY_ACCESS, '$policy').
@ -260,23 +271,26 @@ get_commands_spec() ->
args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"], args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"],
result_example = ok}]. result_example = ok}].
init() -> init() ->
ets:new(ejabberd_commands, [named_table, set, public, mnesia:delete_table(ejabberd_commands),
{keypos, #ejabberd_commands.name}]), mnesia:create_table(ejabberd_commands,
[{ram_copies, [node()]},
{local_content, true},
{attributes, record_info(fields, ejabberd_commands)},
{type, bag}]),
mnesia:add_table_copy(ejabberd_commands, node(), ram_copies),
register_commands(get_commands_spec()). register_commands(get_commands_spec()).
-spec register_commands([ejabberd_commands()]) -> ok. -spec register_commands([ejabberd_commands()]) -> ok.
%% @doc Register ejabberd commands. %% @doc Register ejabberd commands.
%% If a command is already registered, a warning is printed and the old command is preserved. %% If a command is already registered, a warning is printed and the
%% old command is preserved.
register_commands(Commands) -> register_commands(Commands) ->
lists:foreach( lists:foreach(
fun(Command) -> fun(Command) ->
case ets:insert_new(ejabberd_commands, Command) of % XXX check if command exists
true -> mnesia:dirty_write(Command)
ok; % ?DEBUG("This command is already defined:~n~p", [Command])
false ->
?DEBUG("This command is already defined:~n~p", [Command])
end
end, end,
Commands). Commands).
@ -286,7 +300,7 @@ register_commands(Commands) ->
unregister_commands(Commands) -> unregister_commands(Commands) ->
lists:foreach( lists:foreach(
fun(Command) -> fun(Command) ->
ets:delete_object(ejabberd_commands, Command) mnesia:dirty_delete_object(Command)
end, end,
Commands). Commands).
@ -294,94 +308,183 @@ unregister_commands(Commands) ->
%% @doc Get a list of all the available commands, arguments and description. %% @doc Get a list of all the available commands, arguments and description.
list_commands() -> list_commands() ->
Commands = ets:match(ejabberd_commands, list_commands(?DEFAULT_VERSION).
#ejabberd_commands{name = '$1',
args = '$2',
desc = '$3',
_ = '_'}),
[{A, B, C} || [A, B, C] <- Commands].
-spec list_commands_policy() -> [{atom(), [aterm()], string(), atom()}]. -spec list_commands(integer()) -> [{atom(), [aterm()], string()}].
%% @doc Get a list of all the available commands, arguments, description, and %% @doc Get a list of all the available commands, arguments and
%% policy. %% description in a given API verion.
list_commands_policy() -> list_commands(Version) ->
Commands = ets:match(ejabberd_commands, Commands = get_commands_definition(Version),
#ejabberd_commands{name = '$1', [{Name, Args, Desc} || #ejabberd_commands{name = Name,
args = '$2', args = Args,
desc = '$3', desc = Desc} <- Commands].
policy = '$4',
_ = '_'}),
[{A, B, C, D} || [A, B, C, D] <- Commands].
-spec get_command_format(atom()) -> {[aterm()], rterm()} | {error, command_unknown}.
-spec list_commands_policy(integer()) ->
[{atom(), [aterm()], string(), atom()}].
%% @doc Get a list of all the available commands, arguments,
%% description, and policy in a given API version.
list_commands_policy(Version) ->
Commands = get_commands_definition(Version),
[{Name, Args, Desc, Policy} ||
#ejabberd_commands{name = Name,
args = Args,
desc = Desc,
policy = Policy} <- Commands].
-spec get_command_format(atom()) -> {[aterm()], rterm()}.
%% @doc Get the format of arguments and result of a command. %% @doc Get the format of arguments and result of a command.
get_command_format(Name) -> get_command_format(Name) ->
get_command_format(Name, noauth). get_command_format(Name, noauth, ?DEFAULT_VERSION).
get_command_format(Name, Version) when is_integer(Version) ->
get_command_format(Name, noauth, Version);
get_command_format(Name, Auth) -> get_command_format(Name, Auth) ->
get_command_format(Name, Auth, ?DEFAULT_VERSION).
-spec get_command_format(atom(),
{binary(), binary(), binary(), boolean()} |
noauth | admin,
integer()) ->
{[aterm()], rterm()}.
get_command_format(Name, Auth, Version) ->
Admin = is_admin(Name, Auth), Admin = is_admin(Name, Auth),
Matched = ets:match(ejabberd_commands, #ejabberd_commands{args = Args,
#ejabberd_commands{name = Name, result = Result,
args = '$1', policy = Policy} =
result = '$2', get_command_definition(Name, Version),
policy = '$3', case Policy of
_ = '_'}), user when Admin;
case Matched of
[] ->
{error, command_unknown};
[[Args, Result, user]] when Admin;
Auth == noauth -> Auth == noauth ->
{[{user, binary}, {server, binary} | Args], Result}; {[{user, binary}, {server, binary} | Args], Result};
[[Args, Result, _]] -> _ ->
{Args, Result} {Args, Result}
end. end.
-spec get_command_definition(atom()) -> ejabberd_commands() | command_not_found. -spec get_command_definition(atom()) -> ejabberd_commands().
%% @doc Get the definition record of a command. %% @doc Get the definition record of a command.
get_command_definition(Name) -> get_command_definition(Name) ->
case ets:lookup(ejabberd_commands, Name) of get_command_definition(Name, ?DEFAULT_VERSION).
[E] -> E;
[] -> command_not_found -spec get_command_definition(atom(), integer()) -> ejabberd_commands().
%% @doc Get the definition record of a command in a given API version.
get_command_definition(Name, Version) ->
case lists:reverse(
lists:sort(
mnesia:dirty_select(
ejabberd_commands,
ets:fun2ms(
fun(#ejabberd_commands{name = N, version = V} = C)
when N == Name, V =< Version ->
{V, C}
end)))) of
[{_, Command} | _ ] -> Command;
_E -> throw(unknown_command)
end. end.
%% @spec (Name::atom(), Arguments) -> ResultTerm | {error, command_unknown} -spec get_commands_definition(integer()) -> [ejabberd_commands()].
%% @doc Execute a command.
execute_command(Name, Arguments) ->
execute_command([], noauth, Name, Arguments).
-spec execute_command([{atom(), [atom()], [any()]}] | undefined, % @doc Returns all commands for a given API version
get_commands_definition(Version) ->
L = lists:reverse(
lists:sort(
mnesia:dirty_select(
ejabberd_commands,
ets:fun2ms(
fun(#ejabberd_commands{name = Name, version = V} = C)
when V =< Version ->
{Name, V, C}
end)))),
F = fun({_Name, _V, Command}, []) ->
[Command];
({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) ->
Acc;
({_Name, _V, Command}, Acc) -> [Command | Acc]
end,
lists:foldl(F, [], L).
%% @spec (Name::atom(), Arguments) -> ResultTerm
%% where
%% Arguments = [any()]
%% @doc Execute a command.
%% Can return the following exceptions:
%% command_unknown | account_unprivileged | invalid_account_data |
%% no_auth_provided
execute_command(Name, Arguments) ->
execute_command(Name, Arguments, ?DEFAULT_VERSION).
-spec execute_command(atom(),
[any()],
integer() |
{binary(), binary(), binary(), boolean()} | {binary(), binary(), binary(), boolean()} |
noauth | admin, noauth | admin
atom(),
[any()]
) -> any(). ) -> any().
%% @spec (AccessCommands, Auth, Name::atom(), Arguments) -> ResultTerm | {error, Error} %% @spec (Name::atom(), Arguments, integer() | Auth) -> ResultTerm
%% where
%% Auth = {User::string(), Server::string(), Password::string(),
%% Admin::boolean()}
%% | noauth
%% | admin
%% Arguments = [any()]
%%
%% @doc Execute a command in a given API version
%% Can return the following exceptions:
%% command_unknown | account_unprivileged | invalid_account_data |
%% no_auth_provided
execute_command(Name, Arguments, Version) when is_integer(Version) ->
execute_command([], noauth, Name, Arguments, Version);
execute_command(Name, Arguments, Auth) ->
execute_command([], Auth, Name, Arguments, ?DEFAULT_VERSION).
%% @spec (AccessCommands, Auth, Name::atom(), Arguments) ->
%% ResultTerm | {error, Error}
%% where %% where
%% AccessCommands = [{Access, CommandNames, Arguments}] | undefined %% AccessCommands = [{Access, CommandNames, Arguments}] | undefined
%% Auth = {User::string(), Server::string(), Password::string(), Admin::boolean()} %% Auth = {User::string(), Server::string(), Password::string(), Admin::boolean()}
%% | noauth %% | noauth
%% | admin %% | admin
%% Method = atom()
%% Arguments = [any()] %% Arguments = [any()]
%% Error = command_unknown | account_unprivileged | invalid_account_data | no_auth_provided %%
execute_command(AccessCommands1, Auth1, Name, Arguments) -> %% @doc Execute a command
%% Can return the following exceptions:
%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided
execute_command(AccessCommands, Auth, Name, Arguments) ->
execute_command(AccessCommands, Auth, Name, Arguments, ?DEFAULT_VERSION).
-spec execute_command([{atom(), [atom()], [any()]}] | undefined,
{binary(), binary(), binary(), boolean()} |
noauth | admin,
atom(),
[any()],
integer()
) -> any().
%% @spec (AccessCommands, Auth, Name::atom(), Arguments, integer()) -> ResultTerm
%% where
%% AccessCommands = [{Access, CommandNames, Arguments}] | undefined
%% Auth = {User::string(), Server::string(), Password::string(), Admin::boolean()}
%% | noauth
%% | admin
%% Arguments = [any()]
%%
%% @doc Execute a command in a given API version
%% Can return the following exceptions:
%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided
execute_command(AccessCommands1, Auth1, Name, Arguments, Version) ->
Auth = case is_admin(Name, Auth1) of Auth = case is_admin(Name, Auth1) of
true -> admin; true -> admin;
false -> Auth1 false -> Auth1
end, end,
case ets:lookup(ejabberd_commands, Name) of Command = get_command_definition(Name, Version),
[Command] -> AccessCommands = get_access_commands(AccessCommands1, Version),
AccessCommands = get_access_commands(AccessCommands1), case check_access_commands(AccessCommands, Auth, Name, Command, Arguments) of
try check_access_commands(AccessCommands, Auth, Name, Command, Arguments) of
ok -> execute_command2(Auth, Command, Arguments) ok -> execute_command2(Auth, Command, Arguments)
catch
{error, Error} -> {error, Error}
end;
[] -> {error, command_unknown}
end. end.
execute_command2( execute_command2(
@ -407,26 +510,25 @@ execute_command2(Command, Arguments) ->
Module = Command#ejabberd_commands.module, Module = Command#ejabberd_commands.module,
Function = Command#ejabberd_commands.function, Function = Command#ejabberd_commands.function,
?DEBUG("Executing command ~p:~p with Args=~p", [Module, Function, Arguments]), ?DEBUG("Executing command ~p:~p with Args=~p", [Module, Function, Arguments]),
try apply(Module, Function, Arguments) of apply(Module, Function, Arguments).
Response ->
Response
catch
Problem ->
{error, Problem}
end.
-spec get_tags_commands() -> [{string(), [string()]}]. -spec get_tags_commands() -> [{string(), [string()]}].
%% @spec () -> [{Tag::string(), [CommandName::string()]}] %% @spec () -> [{Tag::string(), [CommandName::string()]}]
%% @doc Get all the tags and associated commands. %% @doc Get all the tags and associated commands.
get_tags_commands() -> get_tags_commands() ->
CommandTags = ets:match(ejabberd_commands, get_tags_commands(?DEFAULT_VERSION).
#ejabberd_commands{
name = '$1', -spec get_tags_commands(integer()) -> [{string(), [string()]}].
tags = '$2',
_ = '_'}), %% @spec (integer) -> [{Tag::string(), [CommandName::string()]}]
%% @doc Get all the tags and associated commands in a given API version
get_tags_commands(Version) ->
CommandTags = [{Name, Tags} ||
#ejabberd_commands{name = Name, tags = Tags}
<- get_commands_definition(Version)],
Dict = lists:foldl( Dict = lists:foldl(
fun([CommandNameAtom, CTags], D) -> fun({CommandNameAtom, CTags}, D) ->
CommandName = atom_to_list(CommandNameAtom), CommandName = atom_to_list(CommandNameAtom),
case CTags of case CTags of
[] -> [] ->
@ -445,7 +547,6 @@ get_tags_commands() ->
CommandTags), CommandTags),
orddict:to_list(Dict). orddict:to_list(Dict).
%% ----------------------------- %% -----------------------------
%% Access verification %% Access verification
%% ----------------------------- %% -----------------------------
@ -479,7 +580,8 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments) ->
fun({Access, Commands, ArgumentRestrictions}) -> fun({Access, Commands, ArgumentRestrictions}) ->
case check_access(Command, Access, Auth) of case check_access(Command, Access, Auth) of
true -> true ->
check_access_command(Commands, Command, ArgumentRestrictions, check_access_command(Commands, Command,
ArgumentRestrictions,
Method, Arguments); Method, Arguments);
false -> false ->
false false
@ -488,7 +590,8 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments) ->
ArgumentRestrictions = [], ArgumentRestrictions = [],
case check_access(Command, Access, Auth) of case check_access(Command, Access, Auth) of
true -> true ->
check_access_command(Commands, Command, ArgumentRestrictions, check_access_command(Commands, Command,
ArgumentRestrictions,
Method, Arguments); Method, Arguments);
false -> false ->
false false
@ -551,9 +654,11 @@ check_access2(Access, User, Server) ->
deny -> false deny -> false
end. end.
check_access_command(Commands, Command, ArgumentRestrictions, Method, Arguments) -> check_access_command(Commands, Command, ArgumentRestrictions,
Method, Arguments) ->
case Commands==all orelse lists:member(Method, Commands) of case Commands==all orelse lists:member(Method, Commands) of
true -> check_access_arguments(Command, ArgumentRestrictions, Arguments); true -> check_access_arguments(Command, ArgumentRestrictions,
Arguments);
false -> false false -> false
end. end.
@ -577,18 +682,20 @@ tag_arguments(ArgsDefs, Args) ->
Args). Args).
get_access_commands(undefined) -> get_access_commands(undefined, Version) ->
Cmds = get_commands(), Cmds = get_commands(Version),
[{?POLICY_ACCESS, Cmds, []}]; [{?POLICY_ACCESS, Cmds, []}];
get_access_commands(AccessCommands) -> get_access_commands(AccessCommands, _Version) ->
AccessCommands. AccessCommands.
get_commands() -> get_commands() ->
get_commands(?DEFAULT_VERSION).
get_commands(Version) ->
Opts = ejabberd_config:get_option( Opts = ejabberd_config:get_option(
commands, commands,
fun(V) when is_list(V) -> V end, fun(V) when is_list(V) -> V end,
[]), []),
CommandsList = list_commands_policy(), CommandsList = list_commands_policy(Version),
OpenCmds = [N || {N, _, _, open} <- CommandsList], OpenCmds = [N || {N, _, _, open} <- CommandsList],
RestrictedCmds = [N || {N, _, _, restricted} <- CommandsList], RestrictedCmds = [N || {N, _, _, restricted} <- CommandsList],
AdminCmds = [N || {N, _, _, admin} <- CommandsList], AdminCmds = [N || {N, _, _, admin} <- CommandsList],

View File

@ -48,7 +48,7 @@
-behaviour(ejabberd_config). -behaviour(ejabberd_config).
-author('alexey@process-one.net'). -author('alexey@process-one.net').
-export([start/0, init/0, process/1, process2/2, -export([start/0, init/0, process/1,
register_commands/3, unregister_commands/3, register_commands/3, unregister_commands/3,
opt_type/1]). opt_type/1]).
@ -57,6 +57,8 @@
-include("ejabberd.hrl"). -include("ejabberd.hrl").
-include("logger.hrl"). -include("logger.hrl").
-define(DEFAULT_VERSION, 1000000).
%%----------------------------- %%-----------------------------
%% Module %% Module
@ -69,7 +71,7 @@ start() ->
[SNode3 | Args3] -> [SNode3 | Args3] ->
[SNode3, 60000, Args3]; [SNode3, 60000, Args3];
_ -> _ ->
print_usage(), print_usage(?DEFAULT_VERSION),
halt(?STATUS_USAGE) halt(?STATUS_USAGE)
end, end,
SNode1 = case string:tokens(SNode, "@") of SNode1 = case string:tokens(SNode, "@") of
@ -93,6 +95,9 @@ start() ->
[Node, Reason]), [Node, Reason]),
%% TODO: show minimal start help %% TODO: show minimal start help
?STATUS_BADRPC; ?STATUS_BADRPC;
{invalid_version, V} ->
print("Invalid API version number: ~p~n", [V]),
?STATUS_ERROR;
S -> S ->
S S
end, end,
@ -126,11 +131,17 @@ unregister_commands(CmdDescs, Module, Function) ->
%% Process %% Process
%%----------------------------- %%-----------------------------
-spec process([string()]) -> non_neg_integer(). -spec process([string()]) -> non_neg_integer().
process(Args) ->
process(Args, ?DEFAULT_VERSION).
-spec process([string()], non_neg_integer()) -> non_neg_integer().
%% The commands status, stop and restart are defined here to ensure %% The commands status, stop and restart are defined here to ensure
%% they are usable even if ejabberd is completely stopped. %% they are usable even if ejabberd is completely stopped.
process(["status"]) -> process(["status"], _Version) ->
{InternalStatus, ProvidedStatus} = init:get_status(), {InternalStatus, ProvidedStatus} = init:get_status(),
print("The node ~p is ~p with status: ~p~n", print("The node ~p is ~p with status: ~p~n",
[node(), InternalStatus, ProvidedStatus]), [node(), InternalStatus, ProvidedStatus]),
@ -146,24 +157,24 @@ process(["status"]) ->
?STATUS_SUCCESS ?STATUS_SUCCESS
end; end;
process(["stop"]) -> process(["stop"], _Version) ->
%%ejabberd_cover:stop(), %%ejabberd_cover:stop(),
init:stop(), init:stop(),
?STATUS_SUCCESS; ?STATUS_SUCCESS;
process(["restart"]) -> process(["restart"], _Version) ->
init:restart(), init:restart(),
?STATUS_SUCCESS; ?STATUS_SUCCESS;
process(["mnesia"]) -> process(["mnesia"], _Version) ->
print("~p~n", [mnesia:system_info(all)]), print("~p~n", [mnesia:system_info(all)]),
?STATUS_SUCCESS; ?STATUS_SUCCESS;
process(["mnesia", "info"]) -> process(["mnesia", "info"], _Version) ->
mnesia:info(), mnesia:info(),
?STATUS_SUCCESS; ?STATUS_SUCCESS;
process(["mnesia", Arg]) -> process(["mnesia", Arg], _Version) ->
case catch mnesia:system_info(list_to_atom(Arg)) of case catch mnesia:system_info(list_to_atom(Arg)) of
{'EXIT', Error} -> print("Error: ~p~n", [Error]); {'EXIT', Error} -> print("Error: ~p~n", [Error]);
Return -> print("~p~n", [Return]) Return -> print("~p~n", [Return])
@ -172,23 +183,23 @@ process(["mnesia", Arg]) ->
%% The arguments --long and --dual are not documented because they are %% The arguments --long and --dual are not documented because they are
%% automatically selected depending in the number of columns of the shell %% automatically selected depending in the number of columns of the shell
process(["help" | Mode]) -> process(["help" | Mode], Version) ->
{MaxC, ShCode} = get_shell_info(), {MaxC, ShCode} = get_shell_info(),
case Mode of case Mode of
[] -> [] ->
print_usage(dual, MaxC, ShCode), print_usage(dual, MaxC, ShCode, Version),
?STATUS_USAGE; ?STATUS_USAGE;
["--dual"] -> ["--dual"] ->
print_usage(dual, MaxC, ShCode), print_usage(dual, MaxC, ShCode, Version),
?STATUS_USAGE; ?STATUS_USAGE;
["--long"] -> ["--long"] ->
print_usage(long, MaxC, ShCode), print_usage(long, MaxC, ShCode, Version),
?STATUS_USAGE; ?STATUS_USAGE;
["--tags"] -> ["--tags"] ->
print_usage_tags(MaxC, ShCode), print_usage_tags(MaxC, ShCode, Version),
?STATUS_SUCCESS; ?STATUS_SUCCESS;
["--tags", Tag] -> ["--tags", Tag] ->
print_usage_tags(Tag, MaxC, ShCode), print_usage_tags(Tag, MaxC, ShCode, Version),
?STATUS_SUCCESS; ?STATUS_SUCCESS;
["help"] -> ["help"] ->
print_usage_help(MaxC, ShCode), print_usage_help(MaxC, ShCode),
@ -196,13 +207,22 @@ process(["help" | Mode]) ->
[CmdString | _] -> [CmdString | _] ->
CmdStringU = ejabberd_regexp:greplace( CmdStringU = ejabberd_regexp:greplace(
list_to_binary(CmdString), <<"-">>, <<"_">>), list_to_binary(CmdString), <<"-">>, <<"_">>),
print_usage_commands(binary_to_list(CmdStringU), MaxC, ShCode), print_usage_commands2(binary_to_list(CmdStringU), MaxC, ShCode, Version),
?STATUS_SUCCESS ?STATUS_SUCCESS
end; end;
process(Args) -> process(["--version", Arg | Args], _) ->
Version =
try
list_to_integer(Arg)
catch _:_ ->
throw({invalid_version, Arg})
end,
process(Args, Version);
process(Args, Version) ->
AccessCommands = get_accesscommands(), AccessCommands = get_accesscommands(),
{String, Code} = process2(Args, AccessCommands), {String, Code} = process2(Args, AccessCommands, Version),
case String of case String of
[] -> ok; [] -> ok;
_ -> _ ->
@ -211,18 +231,21 @@ process(Args) ->
Code. Code.
%% @spec (Args::[string()], AccessCommands) -> {String::string(), Code::integer()} %% @spec (Args::[string()], AccessCommands) -> {String::string(), Code::integer()}
process2(["--auth", User, Server, Pass | Args], AccessCommands) -> process2(["--auth", User, Server, Pass | Args], AccessCommands, Version) ->
process2(Args, {list_to_binary(User), list_to_binary(Server), list_to_binary(Pass), true}, AccessCommands); process2(Args, AccessCommands, {list_to_binary(User), list_to_binary(Server),
process2(Args, AccessCommands) -> list_to_binary(Pass), true}, Version);
process2(Args, noauth, AccessCommands). process2(Args, AccessCommands, Version) ->
process2(Args, AccessCommands, admin, Version).
process2(Args, Auth, AccessCommands) ->
case try_run_ctp(Args, Auth, AccessCommands) of
process2(Args, AccessCommands, Auth, Version) ->
case try_run_ctp(Args, Auth, AccessCommands, Version) of
{String, wrong_command_arguments} {String, wrong_command_arguments}
when is_list(String) -> when is_list(String) ->
io:format(lists:flatten(["\n" | String]++["\n"])), io:format(lists:flatten(["\n" | String]++["\n"])),
[CommandString | _] = Args, [CommandString | _] = Args,
process(["help" | [CommandString]]), process(["help" | [CommandString]], Version),
{lists:flatten(String), ?STATUS_ERROR}; {lists:flatten(String), ?STATUS_ERROR};
{String, Code} {String, Code}
when is_list(String) and is_integer(Code) -> when is_list(String) and is_integer(Code) ->
@ -246,29 +269,29 @@ get_accesscommands() ->
%%----------------------------- %%-----------------------------
%% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()} %% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()}
try_run_ctp(Args, Auth, AccessCommands) -> try_run_ctp(Args, Auth, AccessCommands, Version) ->
try ejabberd_hooks:run_fold(ejabberd_ctl_process, false, [Args]) of try ejabberd_hooks:run_fold(ejabberd_ctl_process, false, [Args]) of
false when Args /= [] -> false when Args /= [] ->
try_call_command(Args, Auth, AccessCommands); try_call_command(Args, Auth, AccessCommands, Version);
false -> false ->
print_usage(), print_usage(Version),
{"", ?STATUS_USAGE}; {"", ?STATUS_USAGE};
Status -> Status ->
{"", Status} {"", Status}
catch catch
exit:Why -> exit:Why ->
print_usage(), print_usage(Version),
{io_lib:format("Error in ejabberd ctl process: ~p", [Why]), ?STATUS_USAGE}; {io_lib:format("Error in ejabberd ctl process: ~p", [Why]), ?STATUS_USAGE};
Error:Why -> Error:Why ->
%% In this case probably ejabberd is not started, so let's show Status %% In this case probably ejabberd is not started, so let's show Status
process(["status"]), process(["status"], Version),
print("~n", []), print("~n", []),
{io_lib:format("Error in ejabberd ctl process: '~p' ~p", [Error, Why]), ?STATUS_USAGE} {io_lib:format("Error in ejabberd ctl process: '~p' ~p", [Error, Why]), ?STATUS_USAGE}
end. end.
%% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()} %% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()}
try_call_command(Args, Auth, AccessCommands) -> try_call_command(Args, Auth, AccessCommands, Version) ->
try call_command(Args, Auth, AccessCommands) of try call_command(Args, Auth, AccessCommands, Version) of
{error, command_unknown} -> {error, command_unknown} ->
{io_lib:format("Error: command ~p not known.", [hd(Args)]), ?STATUS_ERROR}; {io_lib:format("Error: command ~p not known.", [hd(Args)]), ?STATUS_ERROR};
{error, wrong_command_arguments} -> {error, wrong_command_arguments} ->
@ -276,24 +299,28 @@ try_call_command(Args, Auth, AccessCommands) ->
Res -> Res ->
Res Res
catch catch
throw:Error ->
{io_lib:format("~p", [Error]), ?STATUS_ERROR};
A:Why -> A:Why ->
Stack = erlang:get_stacktrace(), Stack = erlang:get_stacktrace(),
{io_lib:format("Problem '~p ~p' occurred executing the command.~nStacktrace: ~p", [A, Why, Stack]), ?STATUS_ERROR} {io_lib:format("Problem '~p ~p' occurred executing the command.~nStacktrace: ~p", [A, Why, Stack]), ?STATUS_ERROR}
end. end.
%% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()} | {error, ErrorType} %% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()} | {error, ErrorType}
call_command([CmdString | Args], Auth, AccessCommands) -> call_command([CmdString | Args], Auth, AccessCommands, Version) ->
CmdStringU = ejabberd_regexp:greplace( CmdStringU = ejabberd_regexp:greplace(
list_to_binary(CmdString), <<"-">>, <<"_">>), list_to_binary(CmdString), <<"-">>, <<"_">>),
Command = list_to_atom(binary_to_list(CmdStringU)), Command = list_to_atom(binary_to_list(CmdStringU)),
case ejabberd_commands:get_command_format(Command, Auth) of case ejabberd_commands:get_command_format(Command, Auth, Version) of
{error, command_unknown} -> {error, command_unknown} ->
{error, command_unknown}; {error, command_unknown};
{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, Auth, Command, Result = ejabberd_commands:execute_command(AccessCommands,
ArgsFormatted), Auth, Command,
ArgsFormatted,
Version),
format_result(Result, ResultFormat); format_result(Result, ResultFormat);
{'EXIT', {function_clause,[{lists,zip,[A1, A2], _} | _]}} -> {'EXIT', {function_clause,[{lists,zip,[A1, A2], _} | _]}} ->
{NumCompa, TextCompa} = {NumCompa, TextCompa} =
@ -404,8 +431,8 @@ make_status(ok) -> ?STATUS_SUCCESS;
make_status(true) -> ?STATUS_SUCCESS; make_status(true) -> ?STATUS_SUCCESS;
make_status(_Error) -> ?STATUS_ERROR. make_status(_Error) -> ?STATUS_ERROR.
get_list_commands() -> get_list_commands(Version) ->
try ejabberd_commands:list_commands() of try ejabberd_commands:list_commands(Version) of
Commands -> Commands ->
[tuple_command_help(Command) [tuple_command_help(Command)
|| {N,_,_}=Command <- Commands, || {N,_,_}=Command <- Commands,
@ -458,10 +485,10 @@ get_list_ctls() ->
-define(U2, "\e[24m"). -define(U2, "\e[24m").
-define(U(S), case ShCode of true -> [?U1, S, ?U2]; false -> S end). -define(U(S), case ShCode of true -> [?U1, S, ?U2]; false -> S end).
print_usage() -> print_usage(Version) ->
{MaxC, ShCode} = get_shell_info(), {MaxC, ShCode} = get_shell_info(),
print_usage(dual, MaxC, ShCode). print_usage(dual, MaxC, ShCode, Version).
print_usage(HelpMode, MaxC, ShCode) -> print_usage(HelpMode, MaxC, ShCode, Version) ->
AllCommands = AllCommands =
[ [
{"status", [], "Get ejabberd status"}, {"status", [], "Get ejabberd status"},
@ -469,11 +496,11 @@ print_usage(HelpMode, MaxC, ShCode) ->
{"restart", [], "Restart ejabberd"}, {"restart", [], "Restart ejabberd"},
{"help", ["[--tags [tag] | com?*]"], "Show help (try: ejabberdctl help help)"}, {"help", ["[--tags [tag] | com?*]"], "Show help (try: ejabberdctl help help)"},
{"mnesia", ["[info]"], "show information of Mnesia system"}] ++ {"mnesia", ["[info]"], "show information of Mnesia system"}] ++
get_list_commands() ++ get_list_commands(Version) ++
get_list_ctls(), get_list_ctls(),
print( print(
["Usage: ", ?B("ejabberdctl"), " [--no-timeout] [--node ", ?U("nodename"), "] [--auth ", ["Usage: ", ?B("ejabberdctl"), " [--no-timeout] [--node ", ?U("nodename"), "] [--version ", ?U("api_version"), "] [--auth ",
?U("user"), " ", ?U("host"), " ", ?U("password"), "] ", ?U("user"), " ", ?U("host"), " ", ?U("password"), "] ",
?U("command"), " [", ?U("options"), "]\n" ?U("command"), " [", ?U("options"), "]\n"
"\n" "\n"
@ -598,9 +625,9 @@ format_command_lines(CALD, _MaxCmdLen, MaxC, ShCode, long) ->
%% Print Tags %% Print Tags
%%----------------------------- %%-----------------------------
print_usage_tags(MaxC, ShCode) -> print_usage_tags(MaxC, ShCode, Version) ->
print("Available tags and commands:", []), print("Available tags and commands:", []),
TagsCommands = ejabberd_commands:get_tags_commands(), TagsCommands = ejabberd_commands:get_tags_commands(Version),
lists:foreach( lists:foreach(
fun({Tag, Commands} = _TagCommands) -> fun({Tag, Commands} = _TagCommands) ->
print(["\n\n ", ?B(Tag), "\n "], []), print(["\n\n ", ?B(Tag), "\n "], []),
@ -611,10 +638,10 @@ print_usage_tags(MaxC, ShCode) ->
TagsCommands), TagsCommands),
print("\n\n", []). print("\n\n", []).
print_usage_tags(Tag, MaxC, ShCode) -> print_usage_tags(Tag, MaxC, ShCode, Version) ->
print(["Available commands with tag ", ?B(Tag), ":", "\n"], []), print(["Available commands with tag ", ?B(Tag), ":", "\n"], []),
HelpMode = long, HelpMode = long,
TagsCommands = ejabberd_commands:get_tags_commands(), TagsCommands = ejabberd_commands:get_tags_commands(Version),
CommandsNames = case lists:keysearch(Tag, 1, TagsCommands) of CommandsNames = case lists:keysearch(Tag, 1, TagsCommands) of
{value, {Tag, CNs}} -> CNs; {value, {Tag, CNs}} -> CNs;
false -> [] false -> []
@ -622,7 +649,7 @@ print_usage_tags(Tag, MaxC, ShCode) ->
CommandsList = lists:map( CommandsList = lists:map(
fun(NameString) -> fun(NameString) ->
C = ejabberd_commands:get_command_definition( C = ejabberd_commands:get_command_definition(
list_to_atom(NameString)), list_to_atom(NameString), Version),
#ejabberd_commands{name = Name, #ejabberd_commands{name = Name,
args = Args, args = Args,
desc = Desc} = C, desc = Desc} = C,
@ -673,20 +700,20 @@ print_usage_help(MaxC, ShCode) ->
%%----------------------------- %%-----------------------------
%% @spec (CmdSubString::string(), MaxC::integer(), ShCode::boolean()) -> ok %% @spec (CmdSubString::string(), MaxC::integer(), ShCode::boolean()) -> ok
print_usage_commands(CmdSubString, MaxC, ShCode) -> print_usage_commands2(CmdSubString, MaxC, ShCode, Version) ->
%% Get which command names match this substring %% Get which command names match this substring
AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands()], AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands(Version)],
Cmds = filter_commands(AllCommandsNames, CmdSubString), Cmds = filter_commands(AllCommandsNames, CmdSubString),
case Cmds of case Cmds of
[] -> io:format("Error: not command found that match: ~p~n", [CmdSubString]); [] -> io:format("Error: no command found that match: ~p~n", [CmdSubString]);
_ -> print_usage_commands2(lists:sort(Cmds), MaxC, ShCode) _ -> print_usage_commands3(lists:sort(Cmds), MaxC, ShCode, Version)
end. end.
print_usage_commands2(Cmds, MaxC, ShCode) -> print_usage_commands3(Cmds, MaxC, ShCode, Version) ->
%% Then for each one print it %% Then for each one print it
lists:mapfoldl( lists:mapfoldl(
fun(Cmd, Remaining) -> fun(Cmd, Remaining) ->
print_usage_command(Cmd, MaxC, ShCode), print_usage_command(Cmd, MaxC, ShCode, Version),
case Remaining > 1 of case Remaining > 1 of
true -> print([" ", lists:duplicate(MaxC, 126), " \n"], []); true -> print([" ", lists:duplicate(MaxC, 126), " \n"], []);
false -> ok false -> ok
@ -716,16 +743,16 @@ filter_commands_regexp(All, Glob) ->
All). All).
%% @spec (Cmd::string(), MaxC::integer(), ShCode::boolean()) -> ok %% @spec (Cmd::string(), MaxC::integer(), ShCode::boolean()) -> ok
print_usage_command(Cmd, MaxC, ShCode) -> print_usage_command(Cmd, MaxC, ShCode, Version) ->
Name = list_to_atom(Cmd), Name = list_to_atom(Cmd),
case ejabberd_commands:get_command_definition(Name) of case ejabberd_commands:get_command_definition(Name, Version) of
command_not_found -> command_not_found ->
io:format("Error: command ~p not known.~n", [Cmd]); io:format("Error: command ~p not known.~n", [Cmd]);
C -> C ->
print_usage_command(Cmd, C, MaxC, ShCode) print_usage_command2(Cmd, C, MaxC, ShCode)
end. end.
print_usage_command(Cmd, C, MaxC, ShCode) -> print_usage_command2(Cmd, C, MaxC, ShCode) ->
#ejabberd_commands{ #ejabberd_commands{
tags = TagsAtoms, tags = TagsAtoms,
desc = Desc, desc = Desc,

File diff suppressed because it is too large Load Diff

View File

@ -29,6 +29,11 @@
%% request_handlers: %% request_handlers:
%% "/api": mod_http_api %% "/api": mod_http_api
%% %%
%% 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
%%
%% Access rights are defined with: %% Access rights are defined with:
%% commands_admin_access: configure %% commands_admin_access: configure
%% commands: %% commands:
@ -76,6 +81,8 @@
-include("logger.hrl"). -include("logger.hrl").
-include("ejabberd_http.hrl"). -include("ejabberd_http.hrl").
-define(DEFAULT_API_VERSION, 0).
-define(CT_PLAIN, -define(CT_PLAIN,
{<<"Content-Type">>, <<"text/plain">>}). {<<"Content-Type">>, <<"text/plain">>}).
@ -179,7 +186,8 @@ check_permissions2(#request{ip={IP, _Port}}, Call) ->
true -> {allowed, Call, admin}; true -> {allowed, Call, admin};
_ -> unauthorized_response() _ -> unauthorized_response()
end; end;
_ -> _E ->
?DEBUG("Unauthorized: ~p", [_E]),
unauthorized_response() unauthorized_response()
end. end.
@ -192,10 +200,13 @@ oauth_check_token(Scope, Token) ->
%% command processing %% command processing
%% ------------------ %% ------------------
%process(Call, Request) ->
% ?DEBUG("~p~n~p", [Call, Request]), ok;
process(_, #request{method = 'POST', data = <<>>}) -> process(_, #request{method = 'POST', data = <<>>}) ->
?DEBUG("Bad Request: no data", []), ?DEBUG("Bad Request: no data", []),
badrequest_response(); badrequest_response(<<"Missing POST data">>);
process([Call], #request{method = 'POST', data = Data, ip = IP} = Req) -> process([Call], #request{method = 'POST', data = Data, ip = IP} = Req) ->
Version = get_api_version(Req),
try try
Args = case jiffy:decode(Data) of Args = case jiffy:decode(Data) of
List when is_list(List) -> List; List when is_list(List) -> List;
@ -205,16 +216,20 @@ process([Call], #request{method = 'POST', data = 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), {Code, Result} = handle(Cmd, Auth, Args, Version),
json_response(Code, jiffy:encode(Result)); json_response(Code, jiffy:encode(Result));
ErrorResponse -> %% Should we reply 403 ? ErrorResponse -> %% Should we reply 403 ?
ErrorResponse ErrorResponse
end end
catch _:Error -> catch _:{error,{_,invalid_json}} = _Err ->
?DEBUG("Bad Request: ~p", [Error]), ?DEBUG("Bad Request: ~p", [_Err]),
badrequest_response(<<"Invalid JSON input">>);
_:_Error ->
?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
badrequest_response() badrequest_response()
end; end;
process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) -> process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) ->
Version = get_api_version(Req),
try try
Args = case Data of Args = case Data of
[{nokey, <<>>}] -> []; [{nokey, <<>>}] -> [];
@ -223,13 +238,13 @@ 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), {Code, Result} = handle(Cmd, Auth, Args, Version),
json_response(Code, jiffy:encode(Result)); json_response(Code, jiffy:encode(Result));
ErrorResponse -> ErrorResponse ->
ErrorResponse ErrorResponse
end end
catch _:Error -> catch _:_Error ->
?DEBUG("Bad Request: ~p", [Error]), ?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
badrequest_response() badrequest_response()
end; end;
process([], #request{method = 'OPTIONS', data = <<>>}) -> process([], #request{method = 'OPTIONS', data = <<>>}) ->
@ -238,13 +253,28 @@ process(_Path, Request) ->
?DEBUG("Bad Request: no handler ~p", [Request]), ?DEBUG("Bad Request: no handler ~p", [Request]),
badrequest_response(). badrequest_response().
% 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.
%% ---------------- %% ----------------
%% command handlers %% command handlers
%% ---------------- %% ----------------
% generic ejabberd command handler % generic ejabberd command handler
handle(Call, Auth, Args) when is_atom(Call), is_list(Args) -> handle(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
case ejabberd_commands:get_command_format(Call, Auth) of case ejabberd_commands:get_command_format(Call, Auth, Version) of
{ArgsSpec, _} when is_list(ArgsSpec) -> {ArgsSpec, _} when is_list(ArgsSpec) ->
Args2 = [{jlib:binary_to_atom(Key), Value} || {Key, Value} <- Args], Args2 = [{jlib:binary_to_atom(Key), Value} || {Key, Value} <- Args],
Spec = lists:foldr( Spec = lists:foldr(
@ -259,22 +289,51 @@ handle(Call, Auth, Args) when is_atom(Call), is_list(Args) ->
({Key, atom}, Acc) -> ({Key, atom}, Acc) ->
[{Key, undefined}|Acc] [{Key, undefined}|Acc]
end, [], ArgsSpec), end, [], ArgsSpec),
handle2(Call, Auth, match(Args2, Spec)); 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)};
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;
{error, Msg} -> {error, Msg} ->
?ERROR_MSG("REST API Error: ~p", [Msg]),
{400, Msg}; {400, Msg};
_Error -> _Error ->
?ERROR_MSG("REST API Error: ~p", [_Error]),
{400, <<"Error">>} {400, <<"Error">>}
end. end.
handle2(Call, Auth, Args) when is_atom(Call), is_list(Args) -> handle2(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
{ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth), {ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth, Version),
ArgsFormatted = format_args(Args, ArgsF), ArgsFormatted = format_args(Args, ArgsF),
case ejabberd_command(Auth, Call, ArgsFormatted, 400) of case ejabberd_commands:execute_command(undefined, Auth,
0 -> {200, <<"OK">>}; Call, ArgsFormatted, Version) of
1 -> {500, <<"500 Internal server error">>}; {error, Error} ->
400 -> {400, <<"400 Bad Request">>}; throw(Error);
404 -> {404, <<"404 Not found">>}; Res ->
Res -> format_command_result(Call, Auth, Res) format_command_result(Call, Auth, Res, Version)
end. end.
get_elem_delete(A, L) -> get_elem_delete(A, L) ->
@ -339,7 +398,9 @@ format_arg(undefined, binary) -> <<>>;
format_arg(undefined, string) -> <<>>; format_arg(undefined, string) -> <<>>;
format_arg(Arg, Format) -> format_arg(Arg, Format) ->
?ERROR_MSG("don't know how to format Arg ~p for format ~p", [Arg, Format]), ?ERROR_MSG("don't know how to format Arg ~p for format ~p", [Arg, Format]),
error. throw({invalid_parameter,
io_lib:format("Arg ~p is not in format ~p",
[Arg, Format])}).
process_unicode_codepoints(Str) -> process_unicode_codepoints(Str) ->
iolist_to_binary(lists:map(fun(X) when X > 255 -> unicode:characters_to_binary([X]); iolist_to_binary(lists:map(fun(X) when X > 255 -> unicode:characters_to_binary([X]);
@ -353,24 +414,14 @@ process_unicode_codepoints(Str) ->
match(Args, Spec) -> match(Args, Spec) ->
[{Key, proplists:get_value(Key, Args, Default)} || {Key, Default} <- Spec]. [{Key, proplists:get_value(Key, Args, Default)} || {Key, Default} <- Spec].
ejabberd_command(Auth, Cmd, Args, Default) ->
Access = case Auth of
admin -> [];
_ -> undefined
end,
case catch ejabberd_commands:execute_command(Access, Auth, Cmd, Args) of
{'EXIT', _} -> Default;
{error, _} -> Default;
Result -> Result
end.
format_command_result(Cmd, Auth, Result) -> format_command_result(Cmd, Auth, Result, Version) ->
{_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth), {_, 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, <<"">>}; {200, 0};
{{_, rescode}, _} -> {{_, rescode}, _} ->
{500, <<"">>}; {200, 1};
{{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok -> {{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok ->
{200, iolist_to_binary(Text1)}; {200, iolist_to_binary(Text1)};
{{_, restuple}, {_, Text2}} -> {{_, restuple}, {_, Text2}} ->
@ -421,14 +472,15 @@ format_result(404, {_Name, _}) ->
"not_found". "not_found".
unauthorized_response() -> unauthorized_response() ->
{401, ?HEADER(?CT_XML), unauthorized_response(<<"401 Unauthorized">>).
#xmlel{name = <<"h1">>, attrs = [], unauthorized_response(Body) ->
children = [{xmlcdata, <<"401 Unauthorized">>}]}}. json_response(401, jiffy:encode(Body)).
badrequest_response() -> badrequest_response() ->
{400, ?HEADER(?CT_XML), badrequest_response(<<"400 Bad Request">>).
#xmlel{name = <<"h1">>, attrs = [], badrequest_response(Body) ->
children = [{xmlcdata, <<"400 Bad Request">>}]}}. json_response(400, jiffy:encode(Body)).
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

@ -0,0 +1,79 @@
# ----------------------------------------------------------------------
#
# ejabberd, Copyright (C) 2002-2015 ProcessOne
#
# 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.
#
# ----------------------------------------------------------------------
defmodule EjabberdAdminTest do
use ExUnit.Case, async: false
@author "jsautret@process-one.net"
setup_all do
:mnesia.start
# For some myterious reason, :ejabberd_commands.init mays
# sometimes fails if module is not loaded before
{:module, :ejabberd_commands} = Code.ensure_loaded(:ejabberd_commands)
:ejabberd_commands.init
:ejabberd_admin.start
:ok
end
setup do
:ok
end
test "Logvel can be set and retrieved" do
:ejabberd_logger.start()
assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [1])
assert {1, :critical, 'Critical'} ==
:ejabberd_commands.execute_command(:get_loglevel, [])
assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [2])
assert {2, :error, 'Error'} ==
:ejabberd_commands.execute_command(:get_loglevel, [])
assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [3])
assert {3, :warning, 'Warning'} ==
:ejabberd_commands.execute_command(:get_loglevel, [])
assert {:wrong_loglevel, 6} ==
catch_throw :ejabberd_commands.execute_command(:set_loglevel, [6])
assert {3, :warning, 'Warning'} ==
:ejabberd_commands.execute_command(:get_loglevel, [])
assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [4])
assert {4, :info, 'Info'} ==
:ejabberd_commands.execute_command(:get_loglevel, [])
assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [5])
assert {5, :debug, 'Debug'} ==
:ejabberd_commands.execute_command(:get_loglevel, [])
assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [0])
assert {0, :no_log, 'No log'} ==
:ejabberd_commands.execute_command(:get_loglevel, [])
end
test "command status works with ejabberd stopped" do
assert :ejabberd_not_running ==
elem(:ejabberd_commands.execute_command(:status, []), 0)
end
end

View File

@ -0,0 +1,57 @@
# ejabberd_auth mock
######################
defmodule EjabberdAuthMock do
@author "jsautret@process-one.net"
@agent __MODULE__
def init do
try do
Agent.stop(@agent)
catch
:exit, _e -> :ok
end
{:ok, _pid} = Agent.start_link(fn -> %{} end, name: @agent)
mock(:ejabberd_auth, :is_user_exists,
fn (user, domain) ->
Agent.get(@agent, fn users -> Map.get(users, {user, domain}) end) != nil
end)
mock(:ejabberd_auth, :get_password_s,
fn (user, domain) ->
Agent.get(@agent, fn users -> Map.get(users, {user, domain}, "") end )
end)
mock(:ejabberd_auth, :check_password,
fn (user, domain, password) ->
Agent.get(@agent, fn users ->
Map.get(users, {user, domain}) end) == password
end)
mock(:ejabberd_auth, :set_password,
fn (user, domain, password) ->
Agent.update(@agent, fn users ->
Map.put(users, {user, domain}, password) end)
end)
end
def create_user(user, domain, password) do
Agent.update(@agent, fn users -> Map.put(users, {user, domain}, password) end)
end
####################################################################
# Helpers
####################################################################
# TODO refactor: Move to ejabberd_test_mock
def mock(module, function, fun) do
try do
:meck.new(module)
catch
:error, {:already_started, _pid} -> :ok
end
:meck.expect(module, function, fun)
end
end

View File

@ -19,39 +19,406 @@
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
defmodule EjabberdCommandsTest do defmodule EjabberdCommandsTest do
@author "mremond@process-one.net" use ExUnit.Case, async: false
use ExUnit.Case, async: true @author "jsautret@process-one.net"
# mocked callback module
@module :test_module
# Admin user
@admin "admin"
@adminpass "adminpass"
# Non admin user
@user "user"
@userpass "userpass"
# XMPP domain
@domain "domain"
require Record require Record
Record.defrecord :ejabberd_commands, Record.extract(:ejabberd_commands, from_lib: "ejabberd/include/ejabberd_commands.hrl") Record.defrecord :ejabberd_commands, Record.extract(:ejabberd_commands,
from: "ejabberd_commands.hrl")
setup_all do setup_all do
try do
:stringprep.start
rescue
_ -> :ok
end
:mnesia.start
EjabberdOauthMock.init
:ok
end
setup do
:meck.unload
:meck.new(@module, [:non_strict])
:ejabberd_commands.init :ejabberd_commands.init
end end
test "Check that we can register a command" do test "API command can be registered, listed and unregistered" do
assert :ejabberd_commands.register_commands([user_test_command]) == :ok command = ejabberd_commands name: :test, module: @module,
function: :test_command
assert :ok == :ejabberd_commands.register_commands [command]
commands = :ejabberd_commands.list_commands commands = :ejabberd_commands.list_commands
assert Enum.member?(commands, {:test_user, [], "Test user"}) assert Enum.member? commands, {:test, [], ''}
assert :ok == :ejabberd_commands.unregister_commands [command]
commands = :ejabberd_commands.list_commands
refute Enum.member? commands, {:test, [], ''}
end end
# test "Check that a user can use a user command" do
# [Command] = ets:lookup(ejabberd_commands, test_user),
# AccessCommands = ejabberd_commands:get_access_commands(undefined),
# ejabberd_commands:check_access_commands(AccessCommands, {<<"test">>,<<"localhost">>, {oauth,<<"MyToken">>}, false}, test_user, Command, []).
# end
defp user_test_command do test "API command with versions can be registered, listed and unregistered" do
ejabberd_commands(name: :test_user, tags: [:roster], command1 = ejabberd_commands name: :test, module: @module,
desc: "Test user", function: :test_command, version: 1, desc: 'version1'
policy: :user, command3 = ejabberd_commands name: :test, module: @module,
module: __MODULE__, function: :test_command, version: 3, desc: 'version3'
function: :test_user, assert :ejabberd_commands.register_commands [command1, command3]
args: [],
result: {:contacts, {:list, {:contact, {:tuple, [ version1 = {:test, [], 'version1'}
{:jid, :string}, version3 = {:test, [], 'version3'}
{:nick, :string}
]}}}}) # default version is latest one
commands = :ejabberd_commands.list_commands
refute Enum.member? commands, version1
assert Enum.member? commands, version3
# no such command in APIv0
commands = :ejabberd_commands.list_commands 0
refute Enum.member? commands, version1
refute Enum.member? commands, version3
commands = :ejabberd_commands.list_commands 1
assert Enum.member? commands, version1
refute Enum.member? commands, version3
commands = :ejabberd_commands.list_commands 2
assert Enum.member? commands, version1
refute Enum.member? commands, version3
commands = :ejabberd_commands.list_commands 3
refute Enum.member? commands, version1
assert Enum.member? commands, version3
commands = :ejabberd_commands.list_commands 4
refute Enum.member? commands, version1
assert Enum.member? commands, version3
assert :ok == :ejabberd_commands.unregister_commands [command1]
commands = :ejabberd_commands.list_commands 1
refute Enum.member? commands, version1
refute Enum.member? commands, version3
commands = :ejabberd_commands.list_commands 3
refute Enum.member? commands, version1
assert Enum.member? commands, version3
assert :ok == :ejabberd_commands.unregister_commands [command3]
commands = :ejabberd_commands.list_commands 1
refute Enum.member? commands, version1
refute Enum.member? commands, version3
commands = :ejabberd_commands.list_commands 3
refute Enum.member? commands, version1
refute Enum.member? commands, version3
end end
test "API command can be registered and executed" do
# Create & register a mocked command test() -> :result
command_name = :test
function = :test_command
command = ejabberd_commands(name: command_name,
module: @module,
function: function)
:meck.expect @module, function, fn -> :result end
assert :ok == :ejabberd_commands.register_commands [command]
assert :result == :ejabberd_commands.execute_command(command_name, [])
assert :meck.validate @module
end
test "API command with versions can be registered and executed" do
command_name = :test
function1 = :test_command1
command1 = ejabberd_commands(name: command_name,
version: 1,
module: @module,
function: function1)
:meck.expect(@module, function1, fn -> :result1 end)
function3 = :test_command3
command3 = ejabberd_commands(name: command_name,
version: 3,
module: @module,
function: function3)
:meck.expect(@module, function3, fn -> :result3 end)
assert :ok == :ejabberd_commands.register_commands [command1, command3]
# default version is latest one
assert :result3 == :ejabberd_commands.execute_command(command_name, [])
# no such command in APIv0
assert :unknown_command ==
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, [], 2)
assert :result3 == :ejabberd_commands.execute_command(command_name, [], 3)
assert :result3 == :ejabberd_commands.execute_command(command_name, [], 4)
assert :meck.validate @module
end
test "API command with user policy" do
mock_commands_config
# Register a command test(user, domain) -> {:versionN, user, domain}
# with policy=user and versions 1 & 3
command_name = :test
command1 = ejabberd_commands(name: command_name,
module: @module,
function: :test_command1,
policy: :user, version: 1)
command3 = ejabberd_commands(name: command_name,
module: @module,
function: :test_command3,
policy: :user, version: 3)
:meck.expect(@module, :test_command1,
fn(user, domain) when is_binary(user) and is_binary(domain) ->
{:version1, user, domain}
end)
:meck.expect(@module, :test_command3,
fn(user, domain) when is_binary(user) and is_binary(domain) ->
{:version3, user, domain}
end)
assert :ok == :ejabberd_commands.register_commands [command1, command3]
# A normal user must not pass user info as parameter
assert {:version1, @user, @domain} ==
:ejabberd_commands.execute_command(:undefined,
{@user, @domain,
@userpass, false},
command_name,
[], 2)
assert {:version3, @user, @domain} ==
:ejabberd_commands.execute_command(:undefined,
{@user, @domain,
@userpass, false},
command_name,
[], 3)
token = EjabberdOauthMock.get_token @user, @domain, command_name
assert {:version3, @user, @domain} ==
:ejabberd_commands.execute_command(:undefined,
{@user, @domain,
{:oauth, token}, false},
command_name,
[], 4)
# Expired oauth token
token = EjabberdOauthMock.get_token @user, @domain, command_name, 1
:timer.sleep 1500
assert {:error, :invalid_account_data} ==
catch_throw :ejabberd_commands.execute_command(:undefined,
{@user, @domain,
{:oauth, token}, false},
command_name,
[], 4)
# Wrong oauth scope
token = EjabberdOauthMock.get_token @user, @domain, :bad_command
assert {:error, :invalid_account_data} ==
catch_throw :ejabberd_commands.execute_command(:undefined,
{@user, @domain,
{:oauth, token}, false},
command_name,
[], 4)
assert :function_clause ==
catch_error :ejabberd_commands.execute_command(:undefined,
{@user, @domain,
@userpass, false},
command_name,
[@user, @domain], 2)
# @user is not admin
assert {:error, :account_unprivileged} ==
catch_throw :ejabberd_commands.execute_command(:undefined,
{@user, @domain,
@userpass, true},
command_name,
[], 2)
assert {:error, :account_unprivileged} ==
catch_throw :ejabberd_commands.execute_command(:undefined,
{@user, @domain,
@userpass, true},
command_name,
[@user, @domain], 2)
assert {:error, :account_unprivileged} ==
catch_throw :ejabberd_commands.execute_command(:undefined,
{@user, @domain,
{:oauth, token}, true},
command_name,
[@user, @domain], 2)
# An admin must explicitely pass user info
assert {:version1, @user, @domain} ==
:ejabberd_commands.execute_command(:undefined, :admin,
command_name, [@user, @domain], 2)
assert {:version3, @user, @domain} ==
:ejabberd_commands.execute_command(:undefined, :admin,
command_name, [@user, @domain], 4)
assert {:version1, @user, @domain} ==
:ejabberd_commands.execute_command(:undefined,
{@admin, @domain, @adminpass, true},
command_name, [@user, @domain], 1)
token = EjabberdOauthMock.get_token @admin, @domain, command_name
assert {:version3, @user, @domain} ==
:ejabberd_commands.execute_command(:undefined,
{@admin, @domain, {:oauth, token}, true},
command_name, [@user, @domain], 3)
# Wrong @admin password
assert {:error, :account_unprivileged} ==
catch_throw :ejabberd_commands.execute_command(:undefined,
{@admin, @domain,
@adminpass<>"bad", true},
command_name,
[@user, @domain], 3)
# @admin calling as a normal user
assert {:version3, @admin, @domain} ==
:ejabberd_commands.execute_command(:undefined,
{@admin, @domain,
@adminpass, false},
command_name, [], 5)
assert {:version3, @admin, @domain} ==
:ejabberd_commands.execute_command(:undefined,
{@admin, @domain,
{:oauth, token}, false},
command_name, [], 6)
assert :function_clause ==
catch_error :ejabberd_commands.execute_command(:undefined,
{@admin, @domain,
@adminpass, false},
command_name,
[@user, @domain], 5)
assert :meck.validate @module
end
test "API command with admin policy" do
mock_commands_config
# Register a command test(user, domain) -> {user, domain}
# with policy=admin
command_name = :test
function = :test_command
command = ejabberd_commands(name: command_name,
args: [{:user, :binary}, {:host, :binary}],
module: @module,
function: function,
policy: :admin)
:meck.expect(@module, function,
fn(user, domain) when is_binary(user) and is_binary(domain) ->
{user, domain}
end)
assert :ok == :ejabberd_commands.register_commands [command]
# A normal user cannot call the command
assert {:error, :account_unprivileged} ==
catch_throw :ejabberd_commands.execute_command(:undefined,
{@user, @domain,
@userpass, false},
command_name,
[@user, @domain])
# An admin can call the command
assert {@user, @domain} ==
:ejabberd_commands.execute_command(:undefined,
{@admin, @domain,
@adminpass, true},
command_name,
[@user, @domain])
# An admin can call the command with oauth token
token = EjabberdOauthMock.get_token @admin, @domain, command_name
assert {@user, @domain} ==
:ejabberd_commands.execute_command(:undefined,
{@admin, @domain,
{:oauth, token}, true},
command_name,
[@user, @domain])
# An admin with bad password cannot call the command
assert {:error, :account_unprivileged} ==
catch_throw :ejabberd_commands.execute_command(:undefined,
{@admin, @domain,
"bad"<>@adminpass, false},
command_name,
[@user, @domain])
# An admin cannot call the command with bad oauth token
assert {:error, :account_unprivileged} ==
catch_throw :ejabberd_commands.execute_command(:undefined,
{@admin, @domain,
{:oauth, "bad"<>token}, true},
command_name,
[@user, @domain])
# An admin as a normal user cannot call the command
assert {:error, :account_unprivileged} ==
catch_throw :ejabberd_commands.execute_command(:undefined,
{@admin, @domain,
@adminpass, false},
command_name,
[@user, @domain])
# An admin as a normal user cannot call the command with oauth token
assert {:error, :account_unprivileged} ==
catch_throw :ejabberd_commands.execute_command(:undefined,
{@admin, @domain,
{:oauth, token}, false},
command_name,
[@user, @domain])
assert :meck.validate @module
end
##########################################################
# Utils
# Mock a config where only @admin user is allowed to call commands
# as admin
def mock_commands_config do
EjabberdAuthMock.init
EjabberdAuthMock.create_user @user, @domain, @userpass
EjabberdAuthMock.create_user @admin, @domain, @adminpass
:meck.new :ejabberd_config
:meck.expect(:ejabberd_config, :get_option,
fn(:commands_admin_access, _, _) -> :commands_admin_access
(:oauth_access, _, _) -> :all
(_, _, default) -> default
end)
:meck.expect(:ejabberd_config, :get_myhosts,
fn() -> [@domain] end)
:meck.new :acl
:meck.expect(:acl, :match_rule,
fn(@domain, :commands_admin_access, user) ->
case :jlib.make_jid(@admin, @domain, "") do
^user -> :allow
_ -> :deny
end
(@domain, :all, _user) ->
:allow
end)
end
end end

View File

@ -30,14 +30,14 @@
# log as we are exercising hook handler recovery from that situation. # log as we are exercising hook handler recovery from that situation.
defmodule EjabberdHooksTest do defmodule EjabberdHooksTest do
use ExUnit.Case, async: true use ExUnit.Case, async: false
@author "mremond@process-one.net" @author "mremond@process-one.net"
@host <<"domain.net">> @host <<"domain.net">>
@self __MODULE__ @self __MODULE__
setup_all do setup_all do
{:ok, _Pid} = :ejabberd_hooks.start_link {:ok, _pid} = :ejabberd_hooks.start_link
:ok :ok
end end

View File

@ -0,0 +1,30 @@
# ejabberd_oauth mock
######################
defmodule EjabberdOauthMock do
@author "jsautret@process-one.net"
def init() do
:mnesia.start
:mnesia.create_table(:oauth_token,
[ram_copies: [node],
attributes: [:oauth_token, :us, :scope, :expire]])
end
def get_token(user, domain, command, expiration \\ 3600) do
now = {megasecs, secs, _} = :os.timestamp
expire = 1000000 * megasecs + secs + expiration
:random.seed now
token = to_string :random.uniform(100000000)
{:ok, _} = :ejabberd_oauth.associate_access_token(token,
[{"resource_owner",
{:user, user, domain}},
{"scope", [to_string command]},
{"expiry_time", expire}],
:undefined)
token
end
end

106
test/ejabberd_sm_mock.exs Normal file
View File

@ -0,0 +1,106 @@
# ejabberd_sm mock
######################
defmodule EjabberdSmMock do
@author "jsautret@process-one.net"
require Record
Record.defrecord :session, Record.extract(:session,
from: "ejabberd_sm.hrl")
Record.defrecord :jid, Record.extract(:jid,
from: "jlib.hrl")
@agent __MODULE__
def init do
ModLastMock.init
try do
Agent.stop(@agent)
catch
:exit, _e -> :ok
end
{:ok, _pid} = Agent.start_link(fn -> [] end, name: @agent)
mock(:ejabberd_sm, :get_user_resources,
fn (user, domain) -> for s <- get_sessions(user, domain), do: s.resource end)
mock(:ejabberd_sm, :route,
fn (_from, to, {:broadcast, {:exit, _reason}}) ->
user = jid(to, :user)
domain = jid(to, :server)
resource = jid(to, :resource)
disconnect_resource(user, domain, resource)
:ok
(_, _, _) -> :ok
end)
end
def connect_resource(user, domain, resource,
opts \\ [priority: 1, conn: :c2s]) do
Agent.update(@agent, fn sessions ->
session = %{user: user, domain: domain, resource: resource,
timestamp: :os.timestamp, pid: self, node: node,
auth_module: :ejabberd_auth, ip: :undefined,
priority: opts[:priority], conn: opts[:conn]}
[session | sessions]
end)
end
def disconnect_resource(user, domain, resource) do
disconnect_resource(user, domain, resource, ModLastMock.now)
end
def disconnect_resource(user, domain, resource, timestamp) do
Agent.update(@agent, fn sessions ->
for s <- sessions,
s.user != user or s.domain != domain or s.resource != resource, do: s
end)
ModLastMock.set_last user, domain, "", timestamp
end
def get_sessions() do
Agent.get(@agent, fn sessions -> sessions end)
end
def get_sessions(user, domain) do
Agent.get(@agent, fn sessions ->
for s <- sessions, s.user == user, s.domain == domain, do: s
end)
end
def get_session(user, domain, resource) do
Agent.get(@agent, fn sessions ->
for s <- sessions,
s.user == user, s.domain == domain, s.resource == resource, do: s
end)
end
def to_record(s) do
session(usr: {s.user, s.domain, s.ressource},
us: {s.user, s.domain},
sid: {s.timestamp, s.pid},
priority: s.priority,
info: [conn: s.conn, ip: s.ip, node: s.node,
oor: false, auth_module: s.auth_module])
end
####################################################################
# Helpers
####################################################################
# TODO refactor: Move to ejabberd_test_mock
def mock(module, function, fun) do
try do
:meck.new(module)
catch
:error, {:already_started, _pid} -> :ok
end
:meck.expect(module, function, fun)
end
end

View File

@ -19,6 +19,7 @@
init_per_suite(Config) -> init_per_suite(Config) ->
check_meck(), check_meck(),
code:add_pathz(filename:join(test_dir(), "../include")),
Config. Config.
init_per_testcase(_TestCase, Config) -> init_per_testcase(_TestCase, Config) ->
@ -66,6 +67,11 @@ undefined_function(Module, Func, Args) ->
run_elixir_test(Func) -> run_elixir_test(Func) ->
'Elixir.ExUnit':start([]), 'Elixir.ExUnit':start([]),
filelib:fold_files(test_dir(), ".*\\.exs\$", true,
fun (File, N) ->
'Elixir.Code':require_file(list_to_binary(File)),
N+1
end, 0),
'Elixir.Code':load_file(list_to_binary(filename:join(test_dir(), atom_to_list(Func)))), 'Elixir.Code':load_file(list_to_binary(filename:join(test_dir(), atom_to_list(Func)))),
%% I did not use map syntax, so that this file can still be build under R16 %% I did not use map syntax, so that this file can still be build under R16
ResultMap = 'Elixir.ExUnit':run(), ResultMap = 'Elixir.ExUnit':run(),

View File

@ -0,0 +1,699 @@
# ----------------------------------------------------------------------
#
# ejabberd, Copyright (C) 2002-2015 ProcessOne
#
# 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.
#
# ----------------------------------------------------------------------
defmodule EjabberdModAdminExtraTest do
use ExUnit.Case, async: false
@author "jsautret@process-one.net"
@user "user"
@domain "domain"
@password "password"
@resource "resource"
require Record
Record.defrecord :jid, Record.extract(:jid,
from: "jlib.hrl")
setup_all do
try do
:stringprep.start
:mnesia.start
:p1_sha.load_nif
rescue
_ -> :ok
end
:ejabberd_commands.init
:mod_admin_extra.start(@domain, [])
:sel_application.start_app(:moka)
{:ok, _pid} = :ejabberd_hooks.start_link
:ok
end
setup do
:meck.unload
EjabberdAuthMock.init
EjabberdSmMock.init
ModRosterMock.init(@domain, :mod_admin_extra)
:ok
end
###################### Accounts
test "check_account works" do
EjabberdAuthMock.create_user @user, @domain, @password
assert :ejabberd_commands.execute_command(:check_account, [@user, @domain])
refute :ejabberd_commands.execute_command(:check_account, [@user, "bad_domain"])
refute :ejabberd_commands.execute_command(:check_account, ["bad_user", @domain])
assert :meck.validate :ejabberd_auth
end
test "check_password works" do
EjabberdAuthMock.create_user @user, @domain, @password
assert :ejabberd_commands.execute_command(:check_password,
[@user, @domain, @password])
refute :ejabberd_commands.execute_command(:check_password,
[@user, @domain, "bad_password"])
refute :ejabberd_commands.execute_command(:check_password,
[@user, "bad_domain", @password])
refute :ejabberd_commands.execute_command(:check_password,
["bad_user", @domain, @password])
assert :meck.validate :ejabberd_auth
end
test "check_password_hash works" do
EjabberdAuthMock.create_user @user, @domain, @password
hash = "5F4DCC3B5AA765D61D8327DEB882CF99" # echo -n password|md5
assert :ejabberd_commands.execute_command(:check_password_hash,
[@user, @domain, hash, "md5"])
refute :ejabberd_commands.execute_command(:check_password_hash,
[@user, @domain, "bad_hash", "md5"])
refute :ejabberd_commands.execute_command(:check_password_hash,
[@user, "bad_domain", hash, "md5"])
refute :ejabberd_commands.execute_command(:check_password_hash,
["bad_user", @domain, hash, "md5"])
hash = "5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8" # echo -n password|shasum
assert :ejabberd_commands.execute_command(:check_password_hash,
[@user, @domain, hash, "sha"])
assert :unkown_hash_method ==
catch_throw :ejabberd_commands.execute_command(:check_password_hash,
[@user, @domain, hash, "bad_method"])
assert :meck.validate :ejabberd_auth
end
test "change_password works" do
EjabberdAuthMock.create_user @user, @domain, @password
assert :ejabberd_commands.execute_command(:change_password,
[@user, @domain, "new_password"])
refute :ejabberd_commands.execute_command(:check_password,
[@user, @domain, @password])
assert :ejabberd_commands.execute_command(:check_password,
[@user, @domain, "new_password"])
assert {:not_found, 'unknown_user'} ==
catch_throw :ejabberd_commands.execute_command(:change_password,
["bad_user", @domain,
@password])
assert :meck.validate :ejabberd_auth
end
test "check_users_registration works" do
EjabberdAuthMock.create_user @user<>"1", @domain, @password
EjabberdAuthMock.create_user @user<>"2", @domain, @password
EjabberdAuthMock.create_user @user<>"3", @domain, @password
assert [{@user<>"0", @domain, 0},
{@user<>"1", @domain, 1},
{@user<>"2", @domain, 1},
{@user<>"3", @domain, 1}] ==
:ejabberd_commands.execute_command(:check_users_registration,
[[{@user<>"0", @domain},
{@user<>"1", @domain},
{@user<>"2", @domain},
{@user<>"3", @domain}]])
assert :meck.validate :ejabberd_auth
end
###################### Sessions
test "num_resources works" do
assert 0 == :ejabberd_commands.execute_command(:num_resources,
[@user, @domain])
EjabberdSmMock.connect_resource @user, @domain, @resource
assert 1 == :ejabberd_commands.execute_command(:num_resources,
[@user, @domain])
EjabberdSmMock.connect_resource @user, @domain, @resource<>"2"
assert 2 == :ejabberd_commands.execute_command(:num_resources,
[@user, @domain])
EjabberdSmMock.connect_resource @user<>"1", @domain, @resource
assert 2 == :ejabberd_commands.execute_command(:num_resources,
[@user, @domain])
EjabberdSmMock.disconnect_resource @user, @domain, @resource
assert 1 == :ejabberd_commands.execute_command(:num_resources,
[@user, @domain])
assert :meck.validate :ejabberd_sm
end
test "resource_num works" do
EjabberdSmMock.connect_resource @user, @domain, @resource<>"3"
EjabberdSmMock.connect_resource @user, @domain, @resource<>"2"
EjabberdSmMock.connect_resource @user, @domain, @resource<>"1"
assert :bad_argument ==
elem(catch_throw(:ejabberd_commands.execute_command(:resource_num,
[@user, @domain, 0])), 0)
assert @resource<>"1" ==
:ejabberd_commands.execute_command(:resource_num, [@user, @domain, 1])
assert @resource<>"3" ==
:ejabberd_commands.execute_command(:resource_num, [@user, @domain, 3])
assert :bad_argument ==
elem(catch_throw(:ejabberd_commands.execute_command(:resource_num,
[@user, @domain, 4])), 0)
assert :meck.validate :ejabberd_sm
end
test "kick_session works" do
EjabberdSmMock.connect_resource @user, @domain, @resource<>"1"
EjabberdSmMock.connect_resource @user, @domain, @resource<>"2"
EjabberdSmMock.connect_resource @user, @domain, @resource<>"3"
assert 3 == length EjabberdSmMock.get_sessions @user, @domain
assert 1 == length EjabberdSmMock.get_session @user, @domain, @resource<>"2"
assert :ok ==
:ejabberd_commands.execute_command(:kick_session,
[@user, @domain,
@resource<>"2", "kick"])
assert 2 == length EjabberdSmMock.get_sessions @user, @domain
assert 0 == length EjabberdSmMock.get_session @user, @domain, @resource<>"2"
assert :meck.validate :ejabberd_sm
end
###################### Last
test "get_last works" do
assert 'Never' ==
:ejabberd_commands.execute_command(:get_last, [@user, @domain])
EjabberdSmMock.connect_resource @user, @domain, @resource<>"1"
EjabberdSmMock.connect_resource @user, @domain, @resource<>"2"
assert 'Online' ==
:ejabberd_commands.execute_command(:get_last, [@user, @domain])
EjabberdSmMock.disconnect_resource @user, @domain, @resource<>"1"
assert 'Online' ==
:ejabberd_commands.execute_command(:get_last, [@user, @domain])
now = {megasecs, secs, _microsecs} = :os.timestamp
timestamp = megasecs * 1000000 + secs
EjabberdSmMock.disconnect_resource(@user, @domain, @resource<>"2",
timestamp)
{{year, month, day}, {hour, minute, second}} = :calendar.now_to_local_time now
result = List.flatten(:io_lib.format(
"~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w ",
[year, month, day, hour, minute, second]))
assert result ==
:ejabberd_commands.execute_command(:get_last, [@user, @domain])
assert :meck.validate :mod_last
end
###################### Roster
test "add_rosteritem and delete_rosteritem work" do
# Connect user
# Add user1 & user2 to user's roster
# Remove user1 & user2 from user's roster
EjabberdSmMock.connect_resource @user, @domain, @resource
assert [] == ModRosterMock.get_roster(@user, @domain)
assert :ok ==
:ejabberd_commands.execute_command(:add_rosteritem, [@user, @domain,
@user<>"1", @domain,
"nick1",
"group1",
"both"])
# Check that user1 is the only item of the user's roster
result = ModRosterMock.get_roster(@user, @domain)
assert 1 == length result
[{{@user, @domain, jid}, opts}] = result
assert @user<>"1@"<>@domain == jid
assert "nick1" == opts.nick
assert ["group1"] == opts.groups
assert :both == opts.subs
# Check that the item roster user1 was pushed with subscription
# 'both' to user online ressource
jid = :jlib.make_jid(@user, @domain, @resource)
assert 1 ==
:meck.num_calls(:ejabberd_sm, :route,
[jid, jid,
{:broadcast, {:item, {@user<>"1", @domain, ""}, :both}}])
assert :ok ==
:ejabberd_commands.execute_command(:add_rosteritem, [@user, @domain,
@user<>"2", @domain,
"nick2",
"group2",
"both"])
result = ModRosterMock.get_roster(@user, @domain)
assert 2 == length result
# Check that the item roster user2 was pushed with subscription
# 'both' to user online ressource
assert 1 ==
:meck.num_calls(:ejabberd_sm, :route,
[jid, jid,
{:broadcast, {:item, {@user<>"2", @domain, ""}, :both}}])
:ejabberd_commands.execute_command(:delete_rosteritem, [@user, @domain,
@user<>"1", @domain])
result = ModRosterMock.get_roster(@user, @domain)
assert 1 == length result
[{{@user, @domain, jid}, opts}] = result
assert @user<>"2@"<>@domain == jid
assert "nick2" == opts.nick
assert ["group2"] == opts.groups
assert :both == opts.subs
# Check that the item roster user1 was pushed with subscription
# 'none' to user online ressource
jid = :jlib.make_jid(@user, @domain, @resource)
assert 1 ==
:meck.num_calls(:ejabberd_sm, :route,
[jid, jid,
{:broadcast, {:item, {@user<>"1", @domain, ""}, :none}}])
:ejabberd_commands.execute_command(:delete_rosteritem, [@user, @domain,
@user<>"2", @domain])
# Check that the item roster user2 was pushed with subscription
# 'none' to user online ressource
assert 1 ==
:meck.num_calls(:ejabberd_sm, :route,
[jid, jid,
{:broadcast, {:item, {@user<>"2", @domain, ""}, :none}}])
# Check that nothing else was pushed to user resource
jid = jid(user: @user, server: @domain, resource: :_,
luser: @user, lserver: @domain, lresource: :_)
assert 4 ==
:meck.num_calls(:ejabberd_sm, :route,
[jid, jid,
{:broadcast, {:item, :_, :_}}])
assert [] == ModRosterMock.get_roster(@user, @domain)
assert :meck.validate :ejabberd_sm
end
test "get_roster works" do
assert [] == ModRosterMock.get_roster(@user, @domain)
assert [] == :ejabberd_commands.execute_command(:get_roster, [@user, @domain],
:admin)
assert :ok ==
:ejabberd_commands.execute_command(:add_rosteritem, [@user, @domain,
@user<>"1", @domain,
"nick1",
"group1",
"both"])
assert [{@user<>"1@"<>@domain, "", 'both', 'none', "group1"}] ==
:ejabberd_commands.execute_command(:get_roster, [@user, @domain], :admin)
assert :ok ==
:ejabberd_commands.execute_command(:add_rosteritem, [@user, @domain,
@user<>"2", @domain,
"nick2",
"group2",
"none"])
result = :ejabberd_commands.execute_command(:get_roster, [@user, @domain], :admin)
assert 2 == length result
assert Enum.member?(result, {@user<>"1@"<>@domain, "", 'both', 'none', "group1"})
assert Enum.member?(result, {@user<>"2@"<>@domain, "", 'none', 'none', "group2"})
end
test "link_contacts & unlink_contacts work" do
# Create user1 and keep it offline
EjabberdAuthMock.create_user @user<>"1", @domain, @password
# fail if one of the users doesn't exist locally
assert 404 ==
:ejabberd_commands.execute_command(:link_contacts, [@user<>"1@"<>@domain,
"nick1",
"group1",
@user<>"2@"<>@domain,
"nick2",
"group2"])
# Create user2 and connect 2 resources
EjabberdAuthMock.create_user @user<>"2", @domain, @password
EjabberdSmMock.connect_resource @user<>"2", @domain, @resource<>"1"
EjabberdSmMock.connect_resource @user<>"2", @domain, @resource<>"2"
# Link both user1 & user2 (returns 0 if OK)
assert 0 ==
:ejabberd_commands.execute_command(:link_contacts, [@user<>"1@"<>@domain,
"nick1",
"group2",
@user<>"2@"<>@domain,
"nick2",
"group1"])
assert [{@user<>"2@"<>@domain, "", 'both', 'none', "group2"}] ==
:ejabberd_commands.execute_command(:get_roster, [@user<>"1", @domain], :admin)
assert [{@user<>"1@"<>@domain, "", 'both', 'none', "group1"}] ==
:ejabberd_commands.execute_command(:get_roster, [@user<>"2", @domain], :admin)
# Check that the item roster user1 was pushed with subscription
# 'both' to the 2 user2 online ressources
jid = :jlib.make_jid(@user<>"2", @domain, @resource<>"1")
assert 1 ==
:meck.num_calls(:ejabberd_sm, :route,
[jid, jid,
{:broadcast, {:item, {@user<>"1", @domain, ""}, :both}}])
jid = :jlib.make_jid(@user<>"2", @domain, @resource<>"2")
assert 1 ==
:meck.num_calls(:ejabberd_sm, :route,
[jid, jid,
{:broadcast, {:item, {@user<>"1", @domain, ""}, :both}}])
# Ulink both user1 & user2 (returns 0 if OK)
assert 0 ==
:ejabberd_commands.execute_command(:unlink_contacts, [@user<>"1@"<>@domain,
@user<>"2@"<>@domain])
assert [] ==
:ejabberd_commands.execute_command(:get_roster, [@user<>"1", @domain], :admin)
assert [] ==
:ejabberd_commands.execute_command(:get_roster, [@user<>"2", @domain], :admin)
# Check that the item roster user1 was pushed with subscription
# 'none' to the 2 user2 online ressources
jid = :jlib.make_jid(@user<>"2", @domain, @resource<>"1")
assert 1 ==
:meck.num_calls(:ejabberd_sm, :route,
[jid, jid,
{:broadcast, {:item, {@user<>"1", @domain, ""}, :none}}])
jid = :jlib.make_jid(@user<>"2", @domain, @resource<>"2")
assert 1 ==
:meck.num_calls(:ejabberd_sm, :route,
[jid, jid,
{:broadcast, {:item, {@user<>"1", @domain, ""}, :none}}])
# Check that nothing else was pushed to user2 resources
jid = jid(user: @user<>"2", server: @domain, resource: :_,
luser: @user<>"2", lserver: @domain, lresource: :_)
assert 4 ==
:meck.num_calls(:ejabberd_sm, :route,
[jid, jid,
{:broadcast, {:item, :_, :_}}])
# Check nothing was pushed to user1
jid = jid(user: @user<>"1", server: @domain, resource: :_,
luser: @user<>"1", lserver: @domain, lresource: :_)
refute :meck.called(:ejabberd_sm, :route,
[jid, jid,
{:broadcast, {:item, :_, :_}}])
assert :meck.validate :ejabberd_sm
assert :meck.validate :ejabberd_auth
end
test "add_contacts and delete_contacts work" do
# Create user, user1 & user2
# Connect user & user1
# Add user0, user1 & user2 to user's roster
# Remove user0, user1 & user2 from user's roster
# user doesn't exists yet, command must fail
assert 404 ==
:ejabberd_commands.execute_command(:add_contacts, [@user, @domain,
[{@user<>"1"<>@domain,
"group1",
"nick1"},
{@user<>"2"<>@domain,
"group2",
"nick2"}]
])
EjabberdAuthMock.create_user @user, @domain, @password
EjabberdSmMock.connect_resource @user, @domain, @resource
EjabberdAuthMock.create_user @user<>"1", @domain, @password
EjabberdSmMock.connect_resource @user<>"1", @domain, @resource
EjabberdAuthMock.create_user @user<>"2", @domain, @password
# Add user1 & user2 in user's roster. Try also to add user0 that
# doesn't exists. Command is supposed to return number of added items.
assert 2 ==
:ejabberd_commands.execute_command(:add_contacts, [@user, @domain,
[{@user<>"0@"<>@domain,
"group0",
"nick0"},
{@user<>"1@"<>@domain,
"group1",
"nick1"},
{@user<>"2@"<>@domain,
"group2",
"nick2"}]
])
# Check that user1 & user2 are the only items in user's roster
result = ModRosterMock.get_roster(@user, @domain)
assert 2 == length result
opts1 = %{nick: "nick1", groups: ["group1"], subs: :both,
ask: :none, askmessage: ""}
assert Enum.member?(result, {{@user, @domain, @user<>"1@"<>@domain}, opts1})
opts2 = %{nick: "nick2", groups: ["group2"], subs: :both,
ask: :none, askmessage: ""}
assert Enum.member?(result, {{@user, @domain, @user<>"2@"<>@domain}, opts2})
# Check that user is the only item in user1's roster
assert [{{@user<>"1", @domain, @user<>"@"<>@domain}, %{opts1|:nick => ""}}] ==
ModRosterMock.get_roster(@user<>"1", @domain)
# Check that user is the only item in user2's roster
assert [{{@user<>"2", @domain, @user<>"@"<>@domain}, %{opts2|:nick => ""}}] ==
ModRosterMock.get_roster(@user<>"2", @domain)
# Check that the roster items user1 & user2 were pushed with subscription
# 'both' to the user online ressource
jid = :jlib.make_jid(@user, @domain, @resource)
assert 1 ==
:meck.num_calls(:ejabberd_sm, :route,
[jid, jid,
{:broadcast, {:item, {@user<>"1", @domain, ""}, :both}}])
assert 1 ==
:meck.num_calls(:ejabberd_sm, :route,
[jid, jid,
{:broadcast, {:item, {@user<>"2", @domain, ""}, :both}}])
# Check that the roster item user was pushed with subscription
# 'both' to the user1 online ressource
jid = :jlib.make_jid(@user<>"1", @domain, @resource)
assert 1 ==
:meck.num_calls(:ejabberd_sm, :route,
[jid, jid,
{:broadcast, {:item, {@user, @domain, ""}, :both}}])
# Check that nothing else was pushed to online resources
assert 3 ==
:meck.num_calls(:ejabberd_sm, :route,
[:_, :_,
{:broadcast, {:item, :_, :_}}])
# Remove user1 & user2 from user's roster. Try also to remove
# user0 that doesn't exists. Command is supposed to return number
# of removed items.
assert 2 ==
:ejabberd_commands.execute_command(:remove_contacts, [@user, @domain,
[@user<>"0@"<>@domain,
@user<>"1@"<>@domain,
@user<>"2@"<>@domain]
])
# Check that roster of user, user1 & user2 are empty
assert [] == ModRosterMock.get_roster(@user, @domain)
assert [] == ModRosterMock.get_roster(@user<>"1", @domain)
assert [] == ModRosterMock.get_roster(@user<>"2", @domain)
# Check that the roster items user1 & user2 were pushed with subscription
# 'none' to the user online ressource
jid = :jlib.make_jid(@user, @domain, @resource)
assert 1 ==
:meck.num_calls(:ejabberd_sm, :route,
[jid, jid,
{:broadcast, {:item, {@user<>"1", @domain, ""}, :none}}])
assert 1 ==
:meck.num_calls(:ejabberd_sm, :route,
[jid, jid,
{:broadcast, {:item, {@user<>"2", @domain, ""}, :none}}])
# Check that the roster item user was pushed with subscription
# 'none' to the user1 online ressource
jid = :jlib.make_jid(@user<>"1", @domain, @resource)
assert 1 ==
:meck.num_calls(:ejabberd_sm, :route,
[jid, jid,
{:broadcast, {:item, {@user, @domain, ""}, :none}}])
# Check that nothing else was pushed to online resources
assert 6 ==
:meck.num_calls(:ejabberd_sm, :route,
[:_, :_,
{:broadcast, {:item, :_, :_}}])
assert :meck.validate :ejabberd_sm
assert :meck.validate :ejabberd_auth
end
test "update_roster works" do
# user doesn't exists yet, command must fail
result =
:ejabberd_commands.execute_command(:update_roster,
[@user, @domain,
[{@user<>"1"<>@domain,
"group1",
"nick1"},
{@user<>"2"<>@domain,
"group2",
"nick2"}],
[]
])
assert :invalid_user == elem(result, 0)
EjabberdAuthMock.create_user @user, @domain, @password
EjabberdSmMock.connect_resource @user, @domain, @resource
EjabberdAuthMock.create_user @user<>"1", @domain, @password
EjabberdSmMock.connect_resource @user<>"1", @domain, @resource
EjabberdAuthMock.create_user @user<>"2", @domain, @password
EjabberdAuthMock.create_user @user<>"3", @domain, @password
assert :ok ==
:ejabberd_commands.execute_command(:update_roster,
[@user, @domain,
[{[{"username", @user<>"1"},
{"nick", "nick1"}]},
{[{"username", @user<>"2"},
{"nick", "nick2"},
{"subscription", "from"}]}],
[]])
# Check that user1 & user2 are the only items in user's roster
result = ModRosterMock.get_roster(@user, @domain)
assert 2 == length result
opts1 = %{nick: "nick1", groups: [""], subs: :both,
ask: :none, askmessage: ""}
assert Enum.member?(result, {{@user, @domain, @user<>"1@"<>@domain}, opts1})
opts2 = %{nick: "nick2", groups: [""], subs: :from,
ask: :none, askmessage: ""}
assert Enum.member?(result, {{@user, @domain, @user<>"2@"<>@domain}, opts2})
# Check that the roster items user1 & user2 were pushed with subscription
# 'both' to the user online ressource
jid = :jlib.make_jid(@user, @domain, @resource)
assert 1 ==
:meck.num_calls(:ejabberd_sm, :route,
[jid, jid,
{:broadcast, {:item, {@user<>"1", @domain, ""}, :both}}])
assert 1 ==
:meck.num_calls(:ejabberd_sm, :route,
[jid, jid,
{:broadcast, {:item, {@user<>"2", @domain, ""}, :from}}])
# Check that nothing else was pushed to online resources
assert 2 ==
:meck.num_calls(:ejabberd_sm, :route,
[:_, :_,
{:broadcast, {:item, :_, :_}}])
# Add user3 & remove user1
assert :ok ==
:ejabberd_commands.execute_command(:update_roster,
[@user, @domain,
[{[{"username", @user<>"3"},
{"nick", "nick3"},
{"subscription", "to"}]}],
[{[{"username", @user<>"1"}]}]
])
# Check that user2 & user3 are the only items in user's roster
result = ModRosterMock.get_roster(@user, @domain)
assert 2 == length result
opts2 = %{nick: "nick2", groups: [""], subs: :from,
ask: :none, askmessage: ""}
assert Enum.member?(result, {{@user, @domain, @user<>"2@"<>@domain}, opts2})
opts1 = %{nick: "nick3", groups: [""], subs: :to,
ask: :none, askmessage: ""}
assert Enum.member?(result, {{@user, @domain, @user<>"3@"<>@domain}, opts1})
# Check that the roster items user1 & user3 were pushed with subscription
# 'none' & 'to' to the user online ressource
jid = :jlib.make_jid(@user, @domain, @resource)
assert 1 ==
:meck.num_calls(:ejabberd_sm, :route,
[jid, jid,
{:broadcast, {:item, {@user<>"1", @domain, ""}, :none}}])
assert 1 ==
:meck.num_calls(:ejabberd_sm, :route,
[jid, jid,
{:broadcast, {:item, {@user<>"3", @domain, ""}, :to}}])
# Check that nothing else was pushed to online resources
assert 4 ==
:meck.num_calls(:ejabberd_sm, :route,
[:_, :_,
{:broadcast, {:item, :_, :_}}])
assert :meck.validate :ejabberd_sm
assert :meck.validate :ejabberd_auth
end
# kick_user command is defined in ejabberd_sm, move to extra?
# test "kick_user works" do
# assert 0 == :ejabberd_commands.execute_command(:num_resources,
# [@user, @domain])
# EjabberdSmMock.connect_resource(@user, @domain, @resource<>"1")
# EjabberdSmMock.connect_resource(@user, @domain, @resource<>"2")
# assert 2 ==
# :ejabberd_commands.execute_command(:kick_user, [@user, @domain])
# assert 0 == :ejabberd_commands.execute_command(:num_resources,
# [@user, @domain])
# assert :meck.validate :ejabberd_sm
# end
end

188
test/mod_http_api_test.exs Normal file
View File

@ -0,0 +1,188 @@
# ----------------------------------------------------------------------
#
# ejabberd, Copyright (C) 2002-2015 ProcessOne
#
# 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.
#
# ----------------------------------------------------------------------
defmodule ModHttpApiTest do
use ExUnit.Case, async: false
@author "jsautret@process-one.net"
# Admin user
@admin "admin"
@adminpass "adminpass"
# Non admin user
@user "user"
@userpass "userpass"
# XMPP domain
@domain "domain"
# mocked command
@command "command_test"
@acommand String.to_atom(@command)
# default API version
@version 0
require Record
Record.defrecord :request, Record.extract(:request,
from: "ejabberd_http.hrl")
setup_all do
try do
:stringprep.start
rescue
_ -> :ok
end
:mod_http_api.start(@domain, [])
EjabberdOauthMock.init
:ok
end
setup do
:meck.unload
:meck.new :ejabberd_commands
EjabberdAuthMock.init
:ok
end
test "HTTP GET simple command call with Basic Auth" do
EjabberdAuthMock.create_user @user, @domain, @userpass
# Mock a simple command() -> :ok
:meck.expect(:ejabberd_commands, :get_command_format,
fn (@acommand, {@user, @domain, @userpass, false}, @version) ->
{[], {:res, :rescode}}
end)
:meck.expect(:ejabberd_commands, :execute_command,
fn (:undefined, {@user, @domain, @userpass, false}, @acommand, [], @version) ->
:ok
end)
#:ejabberd_logger.start
#:ejabberd_logger.set 5
# Correct Basic Auth call
req = request(method: :GET,
path: ["api", @command],
q: [nokey: ""],
# Basic auth
auth: {@user<>"@"<>@domain, @userpass},
ip: {{127,0,0,1},60000},
host: @domain)
result = :mod_http_api.process([@command], req)
assert 200 == elem(result, 0) # HTTP code
assert "0" == elem(result, 2) # command result
# Bad password
req = request(method: :GET,
path: ["api", @command],
q: [nokey: ""],
# Basic auth
auth: {@user<>"@"<>@domain, @userpass<>"bad"},
ip: {{127,0,0,1},60000},
host: @domain)
result = :mod_http_api.process([@command], req)
assert 401 == elem(result, 0) # HTTP code
# Check that the command was executed only once
assert 1 ==
:meck.num_calls(:ejabberd_commands, :execute_command, :_)
assert :meck.validate :ejabberd_auth
assert :meck.validate :ejabberd_commands
#assert :ok = :meck.history(:ejabberd_commands)
end
test "HTTP GET simple command call with OAuth" do
EjabberdAuthMock.create_user @user, @domain, @userpass
# Mock a simple command() -> :ok
:meck.expect(:ejabberd_commands, :get_command_format,
fn (@acommand, {@user, @domain, {:oauth, _token}, false}, @version) ->
{[], {:res, :rescode}}
end)
:meck.expect(:ejabberd_commands, :execute_command,
fn (:undefined, {@user, @domain, {:oauth, _token}, false},
@acommand, [], @version) ->
:ok
end)
#:ejabberd_logger.start
#:ejabberd_logger.set 5
# Correct OAuth call
token = EjabberdOauthMock.get_token @user, @domain, @command
req = request(method: :GET,
path: ["api", @command],
q: [nokey: ""],
# OAuth
auth: {:oauth, token, []},
ip: {{127,0,0,1},60000},
host: @domain)
result = :mod_http_api.process([@command], req)
assert 200 == elem(result, 0) # HTTP code
assert "0" == elem(result, 2) # command result
# Wrong OAuth token
req = request(method: :GET,
path: ["api", @command],
q: [nokey: ""],
# OAuth
auth: {:oauth, "bad"<>token, []},
ip: {{127,0,0,1},60000},
host: @domain)
result = :mod_http_api.process([@command], req)
assert 401 == elem(result, 0) # HTTP code
# Expired OAuth token
token = EjabberdOauthMock.get_token @user, @domain, @command, 1
:timer.sleep 1500
req = request(method: :GET,
path: ["api", @command],
q: [nokey: ""],
# OAuth
auth: {:oauth, token, []},
ip: {{127,0,0,1},60000},
host: @domain)
result = :mod_http_api.process([@command], req)
assert 401 == elem(result, 0) # HTTP code
# Wrong OAuth scope
token = EjabberdOauthMock.get_token @user, @domain, "bad_command"
:timer.sleep 1500
req = request(method: :GET,
path: ["api", @command],
q: [nokey: ""],
# OAuth
auth: {:oauth, token, []},
ip: {{127,0,0,1},60000},
host: @domain)
result = :mod_http_api.process([@command], req)
assert 401 == elem(result, 0) # HTTP code
# Check that the command was executed only once
assert 1 ==
:meck.num_calls(:ejabberd_commands, :execute_command, :_)
assert :meck.validate :ejabberd_auth
assert :meck.validate :ejabberd_commands
#assert :ok = :meck.history(:ejabberd_commands)
end
end

65
test/mod_last_mock.exs Normal file
View File

@ -0,0 +1,65 @@
# mod_last mock
######################
defmodule ModLastMock do
require Record
Record.defrecord :session, Record.extract(:session,
from: "ejabberd_sm.hrl")
Record.defrecord :jid, Record.extract(:jid,
from: "jlib.hrl")
@author "jsautret@process-one.net"
@agent __MODULE__
def init do
try do
Agent.stop(@agent)
catch
:exit, _e -> :ok
end
{:ok, _pid} = Agent.start_link(fn -> %{} end, name: @agent)
mock(:mod_last, :get_last_info,
fn (user, domain) ->
Agent.get(@agent, fn last ->
case Map.get(last, {user, domain}, :not_found) do
{ts, status} -> {:ok, ts, status}
result -> result
end
end)
end)
end
def set_last(user, domain, status) do
set_last(user, domain, status, now)
end
def set_last(user, domain, status, timestamp) do
Agent.update(@agent, fn last ->
Map.put(last, {user, domain}, {timestamp, status})
end)
end
####################################################################
# Helpers
####################################################################
def now() do
{megasecs, secs, _microsecs} = :os.timestamp
megasecs * 1000000 + secs
end
# TODO refactor: Move to ejabberd_test_mock
def mock(module, function, fun) do
try do
:meck.new(module)
catch
:error, {:already_started, _pid} -> :ok
end
:meck.expect(module, function, fun)
end
end

192
test/mod_roster_mock.exs Normal file
View File

@ -0,0 +1,192 @@
# mod_roster mock
######################
defmodule ModRosterMock do
@author "jsautret@process-one.net"
require Record
Record.defrecord :roster, Record.extract(:roster,
from: "mod_roster.hrl")
@agent __MODULE__
def init(domain, module) do
try do
Agent.stop(@agent)
catch
:exit, _e -> :ok
end
{:ok, _pid} = Agent.start_link(fn -> %{} end, name: @agent)
mock_with_moka module
#:mod_roster.stop(domain)
:mod_roster.start(domain, [])
end
def mock_with_moka(module) do
try do
module_mock = :moka.start(module)
:moka.replace(module_mock, :mod_roster, :invalidate_roster_cache,
fn (_user, _server) ->
:ok
end)
:moka.load(module_mock)
roster_mock = :moka.start(:mod_roster)
:moka.replace(roster_mock, :gen_mod, :db_type,
fn (_host, _opts) ->
{:none}
end)
:moka.replace(roster_mock, :gen_iq_handler, :add_iq_handler,
fn (_module, _host, _ns, _m, _f, _iqdisc) ->
:ok
end)
:moka.replace(roster_mock, :gen_iq_handler, :remove_iq_handler,
fn (_module, _host, _ns) ->
:ok
end)
:moka.replace(roster_mock, :transaction,
fn (_server, function) ->
{:atomic, function.()}
end)
:moka.replace(roster_mock, :get_roster,
fn (user, domain) ->
to_records(get_roster(user, domain))
end)
:moka.replace(roster_mock, :update_roster_t,
fn (user, domain, {u, d, _r}, item) ->
add_roster_item(user, domain, u<>"@"<>d,
roster(item, :name),
roster(item, :subscription),
roster(item, :groups),
roster(item, :ask),
roster(item, :askmessage))
end)
:moka.replace(roster_mock, :del_roster_t,
fn (user, domain, jid) ->
remove_roster_item(user, domain, :jlib.jid_to_string(jid))
end)
:moka.load(roster_mock)
catch
{:already_started, _pid} -> :ok
end
end
def mock_with_meck do
# mock(:gen_mod, :db_type,
# fn (_server, :mod_roster) ->
# :mnesia
# end)
#
# mock(:mnesia, :transaction,
# fn (_server, function) ->
# {:atomic, function.()}
# end)
#
# mock(:mnesia, :write,
# fn (Item) ->
# throw Item
# {:atomic, :ok}
# end)
mock(:mod_roster, :transaction,
fn (_server, function) ->
{:atomic, function.()}
end)
mock(:mod_roster, :update_roster_t,
fn (user, domain, {u, d, _r}, item) ->
add_roster_item(user, domain, u<>"@"<>d,
roster(item, :name),
roster(item, :subscription),
roster(item, :groups),
roster(item, :ask),
roster(item, :askmessage))
end)
mock(:mod_roster, :invalidate_roster_cache,
fn (_user, _server) ->
:ok
end)
end
def add_roster_item(user, domain, jid, nick, subs \\ :none, groups \\ [],
ask \\ :none, askmessage \\ "")
when is_binary(user) and byte_size(user) > 0
and is_binary(domain) and byte_size(domain) > 0
and is_binary(jid) and byte_size(jid) > 0
and is_binary(nick)
and is_atom(subs)
and is_list(groups)
and is_atom(ask)
and is_binary(askmessage)
do
Agent.update(@agent, fn roster ->
Map.put(roster, {user, domain, jid}, %{nick: nick,
subs: subs, groups: groups,
ask: ask, askmessage: askmessage})
end)
end
def remove_roster_item(user, domain, jid) do
Agent.update(@agent, fn roster ->
Map.delete(roster, {user, domain, jid})
end)
end
def get_rosters() do
Agent.get(@agent, fn roster -> roster end)
end
def get_roster(user, domain) do
Agent.get(@agent, fn roster ->
for {u, d, jid} <- Map.keys(roster), u == user, d == domain,
do: {{u, d, jid}, Map.fetch!(roster, {u, d, jid})}
end)
end
def to_record({{user, domain, jid}, r}) do
roster(usj: {user, domain, jid},
us: {user, domain},
jid: :jlib.string_to_usr(jid),
subscription: r.subs,
ask: r.ask,
groups: r.groups,
askmessage: r.askmessage
)
end
def to_records(rosters) do
for item <- rosters, do: to_record(item)
end
####################################################################
# Helpers
####################################################################
# TODO refactor: Move to ejabberd_test_mock
def mock(module, function, fun) do
try do
:meck.new(module, [:non_strict, :passthrough, :unstick])
catch
:error, {:already_started, _pid} -> :ok
end
:meck.expect(module, function, fun)
end
end