From 3dc55c6d47e3093a6147ce275c7269a7d08ffc45 Mon Sep 17 00:00:00 2001 From: Alexey Shchepin Date: Thu, 31 Mar 2016 14:53:31 +0300 Subject: [PATCH] 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 Date: Fri Oct 30 11:43:18 2015 +0100 Merge branch '3.2.x' into api commit 14e8ffce78cbea6c8605371d1fc50a0c1d1e012c Author: Jerome Sautret Date: Tue Oct 27 16:35:17 2015 +0100 Added OAuth tests to ejabberd_commands commit f81c550c14628edfe4861c228576cb767924366a Author: Jerome Sautret Date: Tue Oct 27 16:34:55 2015 +0100 Added some mod_http_api tests commit 6a64578d5b2ba532a2feb6503ed98561e56d5d53 Author: Jerome Sautret 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 Date: Fri Oct 23 17:59:34 2015 +0200 Add tests on admin command policy commit 19dad8d54f54c9fabd454280483cccfb06c8e78a Author: Jerome Sautret Date: Fri Oct 23 16:49:36 2015 +0200 Added command related tests (http api & user policy) commit e0e596ab4a3f3a70aba5f374f028939ab794de33 Author: Jerome Sautret Date: Fri Oct 23 16:49:16 2015 +0200 Fix command call. commit 128cd7d1ede3c47a34f8ec3a750c980ccad2c61d Merge: 60c4c4c 447313c Author: Jerome Sautret Date: Thu Oct 22 14:48:39 2015 +0200 Merge branch '3.2.x' into api commit 60c4c4c0751302524c14219c6bc8c56a6069a689 Author: Jerome Sautret Date: Thu Oct 22 14:45:57 2015 +0200 Fix ejabberd_commands spec. commit 8e145c28c5da762c2b93ee32327eff1db94ebfed Merge: 397273a f13dc94 Author: Jerome Sautret Date: Wed Oct 21 18:26:07 2015 +0200 Merge branch '3.2.x' into api commit 397273a23ed415feac87aed33da6452229793387 Merge: c30e89b f289e27 Author: Jerome Sautret Date: Wed Oct 21 15:27:45 2015 +0200 Merge branch '3.2.x' into api commit c30e89bb8a0013bff37e61e4c6953350c9c1f313 Author: Jerome Sautret Date: Wed Oct 21 12:47:02 2015 +0200 Merge mod_http_api commit 7b0db22b4acd48ff6fabce41c1b2525e6580a3c5 Author: Jerome Sautret 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 Date: Thu Oct 15 11:39:45 2015 +0200 Merge branch '3.2.x' into api commit 2879ae87ff3eee369ef3d780136b96ecff5285d1 Author: Jerome Sautret Date: Wed Oct 14 14:53:44 2015 +0200 Fix update_roster command. commit a1d453dd7a3afda9861a8d747494a45057ad574b Author: Jerome Sautret 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 Date: Wed Oct 7 15:10:01 2015 +0200 Add tests on commands commit 6711687bee9c672cb3d5aed0744e13420ecf6dbd Author: Jerome Sautret Date: Tue Sep 29 15:58:16 2015 +0200 Add ejabberd_commands tests commit df8682f419cf3877e77e36a19bca0fc55dc991f8 Author: Jerome Sautret Date: Mon Sep 28 14:54:39 2015 +0200 Added API versioning for ejabberdctl and rest commands commit cd017b0e3aac431bc3ee807ceb7f8641e1523ef5 Author: Jerome Sautret Date: Fri Sep 18 11:21:45 2015 +0200 Better error handling of HTTP API commands. commit ca5cb6acd8e4643f9d6c484d2277b0d7e88471e5 Author: Jerome Sautret 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 Date: Tue Sep 15 15:02:16 2015 +0200 Improve REST API error handling commit 14753b1c02cdce434a786b7f80f6c09f0d210075 Author: Jerome Sautret Date: Mon Sep 14 10:51:17 2015 +0200 Change REST API return codes for integer type. --- Makefile.in | 3 + include/ejabberd_commands.hrl | 6 +- rebar.config | 2 + src/ejabberd_commands.erl | 297 +++++++---- src/ejabberd_ctl.erl | 141 +++--- src/mod_admin_extra.erl | 873 ++++++++++++++++++++++++++------ src/mod_http_api.erl | 160 ++++-- test/ejabberd_admin_test.exs | 79 +++ test/ejabberd_auth_mock.exs | 57 +++ test/ejabberd_commands_test.exs | 425 ++++++++++++++-- test/ejabberd_hooks_test.exs | 6 +- test/ejabberd_oauth_mock.exs | 30 ++ test/ejabberd_sm_mock.exs | 106 ++++ test/elixir_SUITE.erl | 28 +- test/mod_admin_extra_test.exs | 699 +++++++++++++++++++++++++ test/mod_http_api_test.exs | 188 +++++++ test/mod_last_mock.exs | 65 +++ test/mod_roster_mock.exs | 192 +++++++ 18 files changed, 2962 insertions(+), 395 deletions(-) create mode 100644 test/ejabberd_admin_test.exs create mode 100644 test/ejabberd_auth_mock.exs create mode 100644 test/ejabberd_oauth_mock.exs create mode 100644 test/ejabberd_sm_mock.exs create mode 100644 test/mod_admin_extra_test.exs create mode 100644 test/mod_http_api_test.exs create mode 100644 test/mod_last_mock.exs create mode 100644 test/mod_roster_mock.exs diff --git a/Makefile.in b/Makefile.in index 0d9134485..28c05166e 100644 --- a/Makefile.in +++ b/Makefile.in @@ -336,6 +336,9 @@ test: quicktest: $(REBAR) skip_deps=true ct suites=elixir +eunit: + $(REBAR) skip_deps=true exunit + .PHONY: src edoc dialyzer Makefile TAGS clean clean-rel distclean rel \ install uninstall uninstall-binary uninstall-all translations deps test spec \ quicktest erlang_plt deps_plt ejabberd_plt diff --git a/include/ejabberd_commands.hrl b/include/ejabberd_commands.hrl index 0742e3ba4..5874b3d26 100644 --- a/include/ejabberd_commands.hrl +++ b/include/ejabberd_commands.hrl @@ -31,8 +31,10 @@ tags = [] :: [atom()] | '_' | '$2', desc = "" :: string() | '_' | '$3', longdesc = "" :: string() | '_', - module :: atom(), - function :: atom(), + version = 0 :: integer(), + jabs = 1 :: integer(), + module :: atom() | '_', + function :: atom() | '_', args = [] :: [aterm()] | '_' | '$1' | '$2', policy = restricted :: open | restricted | admin | user, result = {res, rescode} :: rterm() | '_' | '$2', diff --git a/rebar.config b/rebar.config index 210e623ae..dd88a4e5c 100644 --- a/rebar.config +++ b/rebar.config @@ -43,6 +43,8 @@ {tag, "1.0.0"}}}}, {if_var_true, tools, {meck, "0.8.2", {git, "https://github.com/eproxus/meck", {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", {tag, "v1.0.8"}}}}]}. diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index 21872aa33..fd8ba03fe 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -90,7 +90,8 @@ %%% PowFloat = math:pow(Base, Exponent), %%% round(PowFloat). %%% -%%% 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: %%%
-export([calc_power/2]).
%%% @@ -201,24 +202,33 @@ %%% TODO: consider this feature: %%% All commands are catched. If an error happens, return the restuple: %%% {error, flattened error string} -%%% This means that ecomm call APIs (ejabberd_ctl, ejabberd_xmlrpc) need to allows this. -%%% And ejabberd_xmlrpc must be prepared to handle such an unexpected response. +%%% This means that ecomm call APIs (ejabberd_ctl, ejabberd_xmlrpc) +%%% need to allows this. And ejabberd_xmlrpc must be prepared to +%%% handle such an unexpected response. -module(ejabberd_commands). -author('badlop@process-one.net'). +-define(DEFAULT_VERSION, 1000000). + -export([init/0, list_commands/0, + list_commands/1, get_command_format/1, - get_command_format/2, + get_command_format/2, + get_command_format/3, get_command_definition/1, + get_command_definition/2, get_tags_commands/0, + get_tags_commands/1, get_commands/0, register_commands/1, unregister_commands/1, execute_command/2, - execute_command/4, + execute_command/3, + execute_command/4, + execute_command/5, opt_type/1, get_commands_spec/0 ]). @@ -226,6 +236,7 @@ -include("ejabberd_commands.hrl"). -include("ejabberd.hrl"). -include("logger.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). -define(POLICY_ACCESS, '$policy'). @@ -260,23 +271,26 @@ get_commands_spec() -> args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"], result_example = ok}]. init() -> - ets:new(ejabberd_commands, [named_table, set, public, - {keypos, #ejabberd_commands.name}]), + mnesia:delete_table(ejabberd_commands), + 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()). -spec register_commands([ejabberd_commands()]) -> ok. %% @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) -> lists:foreach( fun(Command) -> - case ets:insert_new(ejabberd_commands, Command) of - true -> - ok; - false -> - ?DEBUG("This command is already defined:~n~p", [Command]) - end + % XXX check if command exists + mnesia:dirty_write(Command) + % ?DEBUG("This command is already defined:~n~p", [Command]) end, Commands). @@ -286,7 +300,7 @@ register_commands(Commands) -> unregister_commands(Commands) -> lists:foreach( fun(Command) -> - ets:delete_object(ejabberd_commands, Command) + mnesia:dirty_delete_object(Command) end, Commands). @@ -294,94 +308,183 @@ unregister_commands(Commands) -> %% @doc Get a list of all the available commands, arguments and description. list_commands() -> - Commands = ets:match(ejabberd_commands, - #ejabberd_commands{name = '$1', - args = '$2', - desc = '$3', - _ = '_'}), - [{A, B, C} || [A, B, C] <- Commands]. + list_commands(?DEFAULT_VERSION). --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 -%% policy. -list_commands_policy() -> - Commands = ets:match(ejabberd_commands, - #ejabberd_commands{name = '$1', - args = '$2', - desc = '$3', - policy = '$4', - _ = '_'}), - [{A, B, C, D} || [A, B, C, D] <- Commands]. +%% @doc Get a list of all the available commands, arguments and +%% description in a given API verion. +list_commands(Version) -> + Commands = get_commands_definition(Version), + [{Name, Args, Desc} || #ejabberd_commands{name = Name, + args = Args, + desc = Desc} <- 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. 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, ?DEFAULT_VERSION). -get_command_format(Name, Auth) -> +-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), - Matched = ets:match(ejabberd_commands, - #ejabberd_commands{name = Name, - args = '$1', - result = '$2', - policy = '$3', - _ = '_'}), - case Matched of - [] -> - {error, command_unknown}; - [[Args, Result, user]] when Admin; - Auth == noauth -> + #ejabberd_commands{args = Args, + result = Result, + policy = Policy} = + get_command_definition(Name, Version), + case Policy of + user when Admin; + Auth == noauth -> {[{user, binary}, {server, binary} | Args], Result}; - [[Args, Result, _]] -> + _ -> {Args, Result} 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. get_command_definition(Name) -> - case ets:lookup(ejabberd_commands, Name) of - [E] -> E; - [] -> command_not_found + get_command_definition(Name, ?DEFAULT_VERSION). + +-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. -%% @spec (Name::atom(), Arguments) -> ResultTerm | {error, command_unknown} -%% @doc Execute a command. -execute_command(Name, Arguments) -> - execute_command([], noauth, Name, Arguments). +-spec get_commands_definition(integer()) -> [ejabberd_commands()]. --spec execute_command([{atom(), [atom()], [any()]}] | undefined, - {binary(), binary(), binary(), boolean()} | - noauth | admin, - atom(), - [any()] +% @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()} | + noauth | admin ) -> 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 %% AccessCommands = [{Access, CommandNames, Arguments}] | undefined %% Auth = {User::string(), Server::string(), Password::string(), Admin::boolean()} %% | noauth %% | admin -%% Method = atom() %% 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 true -> admin; false -> Auth1 end, - case ets:lookup(ejabberd_commands, Name) of - [Command] -> - AccessCommands = get_access_commands(AccessCommands1), - try check_access_commands(AccessCommands, Auth, Name, Command, Arguments) of - ok -> execute_command2(Auth, Command, Arguments) - catch - {error, Error} -> {error, Error} - end; - [] -> {error, command_unknown} + Command = get_command_definition(Name, Version), + AccessCommands = get_access_commands(AccessCommands1, Version), + case check_access_commands(AccessCommands, Auth, Name, Command, Arguments) of + ok -> execute_command2(Auth, Command, Arguments) end. execute_command2( @@ -407,26 +510,25 @@ execute_command2(Command, Arguments) -> Module = Command#ejabberd_commands.module, Function = Command#ejabberd_commands.function, ?DEBUG("Executing command ~p:~p with Args=~p", [Module, Function, Arguments]), - try apply(Module, Function, Arguments) of - Response -> - Response - catch - Problem -> - {error, Problem} - end. + apply(Module, Function, Arguments). -spec get_tags_commands() -> [{string(), [string()]}]. %% @spec () -> [{Tag::string(), [CommandName::string()]}] %% @doc Get all the tags and associated commands. get_tags_commands() -> - CommandTags = ets:match(ejabberd_commands, - #ejabberd_commands{ - name = '$1', - tags = '$2', - _ = '_'}), + get_tags_commands(?DEFAULT_VERSION). + +-spec get_tags_commands(integer()) -> [{string(), [string()]}]. + +%% @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( - fun([CommandNameAtom, CTags], D) -> + fun({CommandNameAtom, CTags}, D) -> CommandName = atom_to_list(CommandNameAtom), case CTags of [] -> @@ -445,7 +547,6 @@ get_tags_commands() -> CommandTags), orddict:to_list(Dict). - %% ----------------------------- %% Access verification %% ----------------------------- @@ -479,7 +580,8 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments) -> fun({Access, Commands, ArgumentRestrictions}) -> case check_access(Command, Access, Auth) of true -> - check_access_command(Commands, Command, ArgumentRestrictions, + check_access_command(Commands, Command, + ArgumentRestrictions, Method, Arguments); false -> false @@ -488,7 +590,8 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments) -> ArgumentRestrictions = [], case check_access(Command, Access, Auth) of true -> - check_access_command(Commands, Command, ArgumentRestrictions, + check_access_command(Commands, Command, + ArgumentRestrictions, Method, Arguments); false -> false @@ -551,9 +654,11 @@ check_access2(Access, User, Server) -> deny -> false 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 - true -> check_access_arguments(Command, ArgumentRestrictions, Arguments); + true -> check_access_arguments(Command, ArgumentRestrictions, + Arguments); false -> false end. @@ -577,18 +682,20 @@ tag_arguments(ArgsDefs, Args) -> Args). -get_access_commands(undefined) -> - Cmds = get_commands(), +get_access_commands(undefined, Version) -> + Cmds = get_commands(Version), [{?POLICY_ACCESS, Cmds, []}]; -get_access_commands(AccessCommands) -> +get_access_commands(AccessCommands, _Version) -> AccessCommands. get_commands() -> + get_commands(?DEFAULT_VERSION). +get_commands(Version) -> Opts = ejabberd_config:get_option( commands, fun(V) when is_list(V) -> V end, []), - CommandsList = list_commands_policy(), + CommandsList = list_commands_policy(Version), OpenCmds = [N || {N, _, _, open} <- CommandsList], RestrictedCmds = [N || {N, _, _, restricted} <- CommandsList], AdminCmds = [N || {N, _, _, admin} <- CommandsList], diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index bf4e4675a..edec5a07e 100644 --- a/src/ejabberd_ctl.erl +++ b/src/ejabberd_ctl.erl @@ -48,7 +48,7 @@ -behaviour(ejabberd_config). -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, opt_type/1]). @@ -57,6 +57,8 @@ -include("ejabberd.hrl"). -include("logger.hrl"). +-define(DEFAULT_VERSION, 1000000). + %%----------------------------- %% Module @@ -69,7 +71,7 @@ start() -> [SNode3 | Args3] -> [SNode3, 60000, Args3]; _ -> - print_usage(), + print_usage(?DEFAULT_VERSION), halt(?STATUS_USAGE) end, SNode1 = case string:tokens(SNode, "@") of @@ -93,6 +95,9 @@ start() -> [Node, Reason]), %% TODO: show minimal start help ?STATUS_BADRPC; + {invalid_version, V} -> + print("Invalid API version number: ~p~n", [V]), + ?STATUS_ERROR; S -> S end, @@ -126,11 +131,17 @@ unregister_commands(CmdDescs, Module, Function) -> %% Process %%----------------------------- + -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 %% they are usable even if ejabberd is completely stopped. -process(["status"]) -> +process(["status"], _Version) -> {InternalStatus, ProvidedStatus} = init:get_status(), print("The node ~p is ~p with status: ~p~n", [node(), InternalStatus, ProvidedStatus]), @@ -146,24 +157,24 @@ process(["status"]) -> ?STATUS_SUCCESS end; -process(["stop"]) -> +process(["stop"], _Version) -> %%ejabberd_cover:stop(), init:stop(), ?STATUS_SUCCESS; -process(["restart"]) -> +process(["restart"], _Version) -> init:restart(), ?STATUS_SUCCESS; -process(["mnesia"]) -> +process(["mnesia"], _Version) -> print("~p~n", [mnesia:system_info(all)]), ?STATUS_SUCCESS; -process(["mnesia", "info"]) -> +process(["mnesia", "info"], _Version) -> mnesia:info(), ?STATUS_SUCCESS; -process(["mnesia", Arg]) -> +process(["mnesia", Arg], _Version) -> case catch mnesia:system_info(list_to_atom(Arg)) of {'EXIT', Error} -> print("Error: ~p~n", [Error]); Return -> print("~p~n", [Return]) @@ -172,23 +183,23 @@ process(["mnesia", Arg]) -> %% The arguments --long and --dual are not documented because they are %% automatically selected depending in the number of columns of the shell -process(["help" | Mode]) -> +process(["help" | Mode], Version) -> {MaxC, ShCode} = get_shell_info(), case Mode of [] -> - print_usage(dual, MaxC, ShCode), + print_usage(dual, MaxC, ShCode, Version), ?STATUS_USAGE; ["--dual"] -> - print_usage(dual, MaxC, ShCode), + print_usage(dual, MaxC, ShCode, Version), ?STATUS_USAGE; ["--long"] -> - print_usage(long, MaxC, ShCode), + print_usage(long, MaxC, ShCode, Version), ?STATUS_USAGE; ["--tags"] -> - print_usage_tags(MaxC, ShCode), + print_usage_tags(MaxC, ShCode, Version), ?STATUS_SUCCESS; ["--tags", Tag] -> - print_usage_tags(Tag, MaxC, ShCode), + print_usage_tags(Tag, MaxC, ShCode, Version), ?STATUS_SUCCESS; ["help"] -> print_usage_help(MaxC, ShCode), @@ -196,13 +207,22 @@ process(["help" | Mode]) -> [CmdString | _] -> CmdStringU = ejabberd_regexp:greplace( 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 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(), - {String, Code} = process2(Args, AccessCommands), + {String, Code} = process2(Args, AccessCommands, Version), case String of [] -> ok; _ -> @@ -211,18 +231,21 @@ process(Args) -> Code. %% @spec (Args::[string()], AccessCommands) -> {String::string(), Code::integer()} -process2(["--auth", User, Server, Pass | Args], AccessCommands) -> - process2(Args, {list_to_binary(User), list_to_binary(Server), list_to_binary(Pass), true}, AccessCommands); -process2(Args, AccessCommands) -> - process2(Args, noauth, AccessCommands). +process2(["--auth", User, Server, Pass | Args], AccessCommands, Version) -> + process2(Args, AccessCommands, {list_to_binary(User), list_to_binary(Server), + list_to_binary(Pass), true}, Version); +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} when is_list(String) -> io:format(lists:flatten(["\n" | String]++["\n"])), [CommandString | _] = Args, - process(["help" | [CommandString]]), + process(["help" | [CommandString]], Version), {lists:flatten(String), ?STATUS_ERROR}; {String, Code} when is_list(String) and is_integer(Code) -> @@ -246,29 +269,29 @@ get_accesscommands() -> %%----------------------------- %% @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 false when Args /= [] -> - try_call_command(Args, Auth, AccessCommands); + try_call_command(Args, Auth, AccessCommands, Version); false -> - print_usage(), + print_usage(Version), {"", ?STATUS_USAGE}; Status -> {"", Status} catch exit:Why -> - print_usage(), + print_usage(Version), {io_lib:format("Error in ejabberd ctl process: ~p", [Why]), ?STATUS_USAGE}; Error:Why -> %% In this case probably ejabberd is not started, so let's show Status - process(["status"]), + process(["status"], Version), print("~n", []), {io_lib:format("Error in ejabberd ctl process: '~p' ~p", [Error, Why]), ?STATUS_USAGE} end. %% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()} -try_call_command(Args, Auth, AccessCommands) -> - try call_command(Args, Auth, AccessCommands) of +try_call_command(Args, Auth, AccessCommands, Version) -> + try call_command(Args, Auth, AccessCommands, Version) of {error, command_unknown} -> {io_lib:format("Error: command ~p not known.", [hd(Args)]), ?STATUS_ERROR}; {error, wrong_command_arguments} -> @@ -276,24 +299,28 @@ try_call_command(Args, Auth, AccessCommands) -> Res -> Res catch + throw:Error -> + {io_lib:format("~p", [Error]), ?STATUS_ERROR}; A:Why -> Stack = erlang:get_stacktrace(), {io_lib:format("Problem '~p ~p' occurred executing the command.~nStacktrace: ~p", [A, Why, Stack]), ?STATUS_ERROR} end. %% @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( list_to_binary(CmdString), <<"-">>, <<"_">>), 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}; {ArgsFormat, ResultFormat} -> case (catch format_args(Args, ArgsFormat)) of ArgsFormatted when is_list(ArgsFormatted) -> - Result = ejabberd_commands:execute_command(AccessCommands, Auth, Command, - ArgsFormatted), + Result = ejabberd_commands:execute_command(AccessCommands, + Auth, Command, + ArgsFormatted, + Version), format_result(Result, ResultFormat); {'EXIT', {function_clause,[{lists,zip,[A1, A2], _} | _]}} -> {NumCompa, TextCompa} = @@ -404,8 +431,8 @@ make_status(ok) -> ?STATUS_SUCCESS; make_status(true) -> ?STATUS_SUCCESS; make_status(_Error) -> ?STATUS_ERROR. -get_list_commands() -> - try ejabberd_commands:list_commands() of +get_list_commands(Version) -> + try ejabberd_commands:list_commands(Version) of Commands -> [tuple_command_help(Command) || {N,_,_}=Command <- Commands, @@ -458,10 +485,10 @@ get_list_ctls() -> -define(U2, "\e[24m"). -define(U(S), case ShCode of true -> [?U1, S, ?U2]; false -> S end). -print_usage() -> +print_usage(Version) -> {MaxC, ShCode} = get_shell_info(), - print_usage(dual, MaxC, ShCode). -print_usage(HelpMode, MaxC, ShCode) -> + print_usage(dual, MaxC, ShCode, Version). +print_usage(HelpMode, MaxC, ShCode, Version) -> AllCommands = [ {"status", [], "Get ejabberd status"}, @@ -469,11 +496,11 @@ print_usage(HelpMode, MaxC, ShCode) -> {"restart", [], "Restart ejabberd"}, {"help", ["[--tags [tag] | com?*]"], "Show help (try: ejabberdctl help help)"}, {"mnesia", ["[info]"], "show information of Mnesia system"}] ++ - get_list_commands() ++ + get_list_commands(Version) ++ get_list_ctls(), 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("command"), " [", ?U("options"), "]\n" "\n" @@ -598,9 +625,9 @@ format_command_lines(CALD, _MaxCmdLen, MaxC, ShCode, long) -> %% Print Tags %%----------------------------- -print_usage_tags(MaxC, ShCode) -> +print_usage_tags(MaxC, ShCode, Version) -> print("Available tags and commands:", []), - TagsCommands = ejabberd_commands:get_tags_commands(), + TagsCommands = ejabberd_commands:get_tags_commands(Version), lists:foreach( fun({Tag, Commands} = _TagCommands) -> print(["\n\n ", ?B(Tag), "\n "], []), @@ -611,10 +638,10 @@ print_usage_tags(MaxC, ShCode) -> TagsCommands), print("\n\n", []). -print_usage_tags(Tag, MaxC, ShCode) -> +print_usage_tags(Tag, MaxC, ShCode, Version) -> print(["Available commands with tag ", ?B(Tag), ":", "\n"], []), HelpMode = long, - TagsCommands = ejabberd_commands:get_tags_commands(), + TagsCommands = ejabberd_commands:get_tags_commands(Version), CommandsNames = case lists:keysearch(Tag, 1, TagsCommands) of {value, {Tag, CNs}} -> CNs; false -> [] @@ -622,7 +649,7 @@ print_usage_tags(Tag, MaxC, ShCode) -> CommandsList = lists:map( fun(NameString) -> C = ejabberd_commands:get_command_definition( - list_to_atom(NameString)), + list_to_atom(NameString), Version), #ejabberd_commands{name = Name, args = Args, desc = Desc} = C, @@ -673,20 +700,20 @@ print_usage_help(MaxC, ShCode) -> %%----------------------------- %% @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 - 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), case Cmds of - [] -> io:format("Error: not command found that match: ~p~n", [CmdSubString]); - _ -> print_usage_commands2(lists:sort(Cmds), MaxC, ShCode) + [] -> io:format("Error: no command found that match: ~p~n", [CmdSubString]); + _ -> print_usage_commands3(lists:sort(Cmds), MaxC, ShCode, Version) end. -print_usage_commands2(Cmds, MaxC, ShCode) -> +print_usage_commands3(Cmds, MaxC, ShCode, Version) -> %% Then for each one print it lists:mapfoldl( fun(Cmd, Remaining) -> - print_usage_command(Cmd, MaxC, ShCode), + print_usage_command(Cmd, MaxC, ShCode, Version), case Remaining > 1 of true -> print([" ", lists:duplicate(MaxC, 126), " \n"], []); false -> ok @@ -716,16 +743,16 @@ filter_commands_regexp(All, Glob) -> All). %% @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), - case ejabberd_commands:get_command_definition(Name) of + case ejabberd_commands:get_command_definition(Name, Version) of command_not_found -> io:format("Error: command ~p not known.~n", [Cmd]); C -> - print_usage_command(Cmd, C, MaxC, ShCode) + print_usage_command2(Cmd, C, MaxC, ShCode) end. -print_usage_command(Cmd, C, MaxC, ShCode) -> +print_usage_command2(Cmd, C, MaxC, ShCode) -> #ejabberd_commands{ tags = TagsAtoms, desc = Desc, diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl index f0e567199..7962786af 100644 --- a/src/mod_admin_extra.erl +++ b/src/mod_admin_extra.erl @@ -30,24 +30,59 @@ -include("logger.hrl"). --export([start/2, stop/1, compile/1, get_cookie/0, - remove_node/1, set_password/3, - check_password_hash/4, delete_old_users/1, - delete_old_users_vhost/2, ban_account/3, - num_active_users/2, num_resources/2, resource_num/3, +-export([start/2, stop/1, mod_opt_type/1]). + +% Commands API +-export([ + % Adminsys + compile/1, get_cookie/0, remove_node/1, + restart_module/2, + + % Sessions + get_presence/2, num_active_users/2, num_resources/2, resource_num/3, kick_session/4, status_num/2, status_num/1, status_list/2, status_list/1, connected_users_info/0, connected_users_vhost/1, set_presence/7, - user_sessions_info/2, set_nickname/3, get_vcard/3, + user_sessions_info/2, get_last/2, + + % Accounts + change_password/3, check_password_hash/4, delete_old_users/1, + delete_old_users_vhost/2, ban_account/3, + rename_account/4, + check_users_registration/1, + + % vCard + set_nickname/3, get_vcard/3, get_vcard/4, get_vcard_multi/4, set_vcard/4, - set_vcard/5, add_rosteritem/7, delete_rosteritem/4, + set_vcard/5, + + % Roster + add_rosteritem/7, delete_rosteritem/4, process_rosteritems/5, get_roster/2, push_roster/3, - push_roster_all/1, push_alltoall/2, get_last/2, - private_get/4, private_set/3, srg_create/5, + push_roster_all/1, push_alltoall/2, + link_contacts/6, unlink_contacts/2, + add_contacts/3, remove_contacts/3, + update_roster/4, + + % Private storage + private_get/4, private_set/3, + + % Shared roster + srg_create/5, srg_delete/2, srg_list/1, srg_get_info/2, srg_get_members/2, srg_user_add/4, srg_user_del/4, - send_message/5, send_stanza/3, send_stanza_c2s/4, privacy_set/3, - stats/1, stats/2, mod_opt_type/1, get_commands_spec/0]). + + % Send message + send_message/5, send_stanza/3, send_stanza_c2s/4, + + % Privacy list + privacy_set/3, + + % Stats + stats/1, stats/2, + + get_commands_spec/0 + ]). -include("ejabberd.hrl"). @@ -79,7 +114,8 @@ get_commands_spec() -> " TITLE - Work: Position\n" " ROLE - Work: Role", - Vcard2FieldsString = "Some vcard field names and subnames in get/set_vcard2 are:\n" + Vcard2FieldsString = "Some vcard field names and subnames " + "in get/set_vcard2 are:\n" " N FAMILY - Family name\n" " N GIVEN - Given name\n" " N MIDDLE - Middle name\n" @@ -97,6 +133,7 @@ get_commands_spec() -> "http://www.xmpp.org/extensions/xep-0054.html", [ + % Adminsys #ejabberd_commands{name = compile, tags = [erlang], desc = "Recompile and reload Erlang source code file", module = ?MODULE, function = compile, @@ -151,8 +188,19 @@ get_commands_spec() -> result = {res, restuple}, result_example = {ok, <<"Deleted 2 users: [\"oldman@myserver.com\", \"test@myserver.com\"]">>}, result_desc = "Result tuple"}, + #ejabberd_commands{name = restart_module, + tags = [erlang], + desc = "Stop an ejabberd module, reload code and start", + longdesc = "Returns integer code:\n" + " - 0: code reloaded, module restarted\n" + " - 1: error: module not loaded\n" + " - 2: code not reloaded, but module restarted", + module = ?MODULE, function = restart_module, + args = [{module, binary}, {host, binary}], + result = {res, integer}}, + %%%%%%%%%%%%%%%%%% Accounts #ejabberd_commands{name = check_account, tags = [accounts], - desc = "Check if an account exists or not", + desc = "Returns 0 if user exists or 1 if not.", module = ejabberd_auth, function = is_user_exists, args = [{user, binary}, {host, binary}], args_example = [<<"peter">>, <<"myserver.com">>], @@ -161,7 +209,7 @@ get_commands_spec() -> result_example = ok, result_desc = "Status code: 0 on success, 1 otherwise"}, #ejabberd_commands{name = check_password, tags = [accounts], - desc = "Check if a password is correct", + desc = "Check if a password is correct (0 yes, 1 no)", module = ejabberd_auth, function = check_password, args = [{user, binary}, {host, binary}, {password, binary}], args_example = [<<"peter">>, <<"myserver.com">>, <<"secret">>], @@ -171,7 +219,8 @@ get_commands_spec() -> result_desc = "Status code: 0 on success, 1 otherwise"}, #ejabberd_commands{name = check_password_hash, tags = [accounts], desc = "Check if the password hash is correct", - longdesc = "Allowed hash methods: md5, sha.", + longdesc = "Hash must be uppercase.\n" + "Allowed hash methods are: md5, sha.", module = ?MODULE, function = check_password_hash, args = [{user, binary}, {host, binary}, {passwordhash, string}, {hashmethod, string}], @@ -184,7 +233,7 @@ get_commands_spec() -> result_desc = "Status code: 0 on success, 1 otherwise"}, #ejabberd_commands{name = change_password, tags = [accounts], desc = "Change the password of an account", - module = ?MODULE, function = set_password, + module = ?MODULE, function = change_password, args = [{user, binary}, {host, binary}, {newpass, binary}], args_example = [<<"peter">>, <<"myserver.com">>, <<"blank">>], args_desc = ["User name", "Server name", @@ -193,7 +242,8 @@ get_commands_spec() -> result_example = ok, result_desc = "Status code: 0 on success, 1 otherwise"}, #ejabberd_commands{name = ban_account, tags = [accounts], - desc = "Ban an account: kick sessions and set random password", + desc = "Ban an account: kick sessions and set " + "random password", module = ?MODULE, function = ban_account, args = [{user, binary}, {host, binary}, {reason, binary}], args_example = [<<"attacker">>, <<"myserver.com">>, <<"Spaming other users">>], @@ -202,6 +252,59 @@ get_commands_spec() -> result = {res, rescode}, result_example = ok, result_desc = "Status code: 0 on success, 1 otherwise"}, + % XXX Dangerous if lots of registered users + #ejabberd_commands{name = delete_old_users, tags = [accounts, purge], + desc = "Delete users that didn't log in last days, " + "or that never logged", + module = ?MODULE, function = delete_old_users, + args = [{days, integer}], + result = {res, restuple}}, + % XXX Dangerous if lots of registered users + #ejabberd_commands{name = delete_old_users_vhost, tags = [accounts, purge], + desc = "Delete users that didn't log in last days " + "in vhost, or that never logged", + module = ?MODULE, function = delete_old_users_vhost, + args = [{host, binary}, {days, integer}], + result = {res, restuple}}, + #ejabberd_commands{name = rename_account, + tags = [accounts], desc = "Change an acount name", + longdesc = + "Creates a new account and copies the " + "roster from the old one, and updates " + "the rosters of his contacts. Offline " + "messages and private storage are lost.", + module = ?MODULE, function = rename_account, + args = + [{user, binary}, {server, binary}, + {newuser, binary}, {newserver, binary}], + result = {res, integer}}, + #ejabberd_commands{name = check_users_registration, + tags = [roster], + desc = "List registration status for a list of users", + module = ?MODULE, function = check_users_registration, + args = + [{users, + {list, + {auser, + {tuple, [{user, binary}, {server, binary}]}}}}], + result = + {users, + {list, + {auser, + {tuple, + [{user, string}, {server, string}, + {status, integer}]}}}}}, + + + %%%%%%%%%%%%%%%%%% Sessions + #ejabberd_commands{name = num_active_users, tags = [accounts, stats], + desc = "Get number of users active in the last days", + policy = admin, + module = ?MODULE, function = num_active_users, + args = [{host, binary}, {days, integer}], + result = {users, integer}}, + + #ejabberd_commands{name = num_resources, tags = [session], desc = "Get the number of resources of a user", module = ?MODULE, function = num_resources, @@ -251,19 +354,22 @@ get_commands_spec() -> result = {users, integer}, result_example = 23, result_desc = "Number of connected sessions with given status type"}, + % XXX Dangerous if lots of online users #ejabberd_commands{name = status_list_host, tags = [session], desc = "List of users logged in host with their statuses", module = ?MODULE, function = status_list, args = [{host, binary}, {status, binary}], result = {users, {list, - {userstatus, {tuple, [ - {user, string}, - {host, string}, - {resource, string}, - {priority, integer}, - {status, string} - ]}} + {userstatus, {tuple, + [ + {user, string}, + {host, string}, + {resource, string}, + {priority, integer}, + {status, string} + ]}} }}}, + % XXX Dangerous if lots of online users #ejabberd_commands{name = status_list, tags = [session], desc = "List of logged users with this status", module = ?MODULE, function = status_list, @@ -277,9 +383,11 @@ get_commands_spec() -> {status, string} ]}} }}}, + % XXX Dangerous if lots of online users #ejabberd_commands{name = connected_users_info, tags = [session], - desc = "List all established sessions and their information", + desc = "List all established sessions and their " + "information", module = ?MODULE, function = connected_users_info, args = [], result = {connected_users_info, @@ -294,12 +402,14 @@ get_commands_spec() -> {uptime, integer} ]}} }}}, + % XXX Dangerous if lots of online users #ejabberd_commands{name = connected_users_vhost, tags = [session], desc = "Get the list of established sessions in a vhost", module = ?MODULE, function = connected_users_vhost, args = [{host, binary}], - result = {connected_users_vhost, {list, {sessions, string}}}}, + result = {connected_users_vhost, + {list, {sessions, string}}}}, #ejabberd_commands{name = user_sessions_info, tags = [session], desc = "Get information about all sessions of a user", @@ -319,7 +429,29 @@ get_commands_spec() -> {statustext, string} ]}} }}}, - + #ejabberd_commands{name = get_presence, + tags = [session], + desc = + "Retrieve the resource with highest priority, " + "and its presence (show and status message) " + "for a given user.", + longdesc = + "The 'jid' value contains the user jid " + "with resource.\nThe 'show' value contains " + "the user presence flag. It can take " + "limited values:\n - available\n - chat " + "(Free for chat)\n - away\n - dnd (Do " + "not disturb)\n - xa (Not available, " + "extended away)\n - unavailable (Not " + "connected)\n\n'status' is a free text " + "defined by the user client.", + module = ?MODULE, function = get_presence, + args = [{user, binary}, {server, binary}], + result = + {presence, + {tuple, + [{jid, string}, {show, string}, + {status, string}]}}}, #ejabberd_commands{name = set_presence, tags = [session], desc = "Set presence of a session", @@ -327,52 +459,82 @@ get_commands_spec() -> args = [{user, binary}, {host, binary}, {resource, binary}, {type, binary}, {show, binary}, {status, binary}, - {priority, binary}], + {priority, integer}], result = {res, rescode}}, + %%%%%%%%%%%%%%%%%% Last info + #ejabberd_commands{name = get_last, tags = [last], + desc = "Get last activity information " + "(timestamp and status)", + longdesc = "Timestamp is the seconds since" + "1970-01-01 00:00:00 UTC, for example: date +%s", + module = ?MODULE, function = get_last, + args = [{user, binary}, {host, binary}], + result = {last_activity, string}}, + #ejabberd_commands{name = set_last, tags = [last], + desc = "Set last activity information", + longdesc = "Timestamp is the seconds since" + "1970-01-01 00:00:00 UTC, for example: date +%s", + module = mod_last, function = store_last_info, + args = [{user, binary}, {host, binary}, + {timestamp, integer}, {status, binary}], + result = {res, rescode}}, + + %%%%%%%%%%%%%%%%%% vCard #ejabberd_commands{name = set_nickname, tags = [vcard], desc = "Set nickname in a user's vCard", module = ?MODULE, function = set_nickname, - args = [{user, binary}, {host, binary}, {nickname, binary}], + args = [{user, binary}, {host, binary}, + {nickname, binary}], result = {res, rescode}}, #ejabberd_commands{name = get_vcard, tags = [vcard], desc = "Get content from a vCard field", - longdesc = Vcard1FieldsString ++ "\n" ++ Vcard2FieldsString ++ "\n\n" ++ VcardXEP, + longdesc = Vcard1FieldsString ++ "\n" ++ + Vcard2FieldsString ++ "\n\n" ++ VcardXEP, module = ?MODULE, function = get_vcard, args = [{user, binary}, {host, binary}, {name, binary}], result = {content, string}}, #ejabberd_commands{name = get_vcard2, tags = [vcard], desc = "Get content from a vCard field", - longdesc = Vcard2FieldsString ++ "\n\n" ++ Vcard1FieldsString ++ "\n" ++ VcardXEP, + longdesc = Vcard2FieldsString ++ "\n\n" ++ + Vcard1FieldsString ++ "\n" ++ VcardXEP, module = ?MODULE, function = get_vcard, - args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}], + args = [{user, binary}, {host, binary}, + {name, binary}, {subname, binary}], result = {content, string}}, #ejabberd_commands{name = get_vcard2_multi, tags = [vcard], desc = "Get multiple contents from a vCard field", - longdesc = Vcard2FieldsString ++ "\n\n" ++ Vcard1FieldsString ++ "\n" ++ VcardXEP, + longdesc = Vcard2FieldsString ++ "\n\n" ++ + Vcard1FieldsString ++ "\n" ++ VcardXEP, module = ?MODULE, function = get_vcard_multi, - args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}], + args = [{user, binary}, {host, binary}, {name, binary}, + {subname, binary}], result = {contents, {list, {value, string}}}}, - #ejabberd_commands{name = set_vcard, tags = [vcard], desc = "Set content in a vCard field", - longdesc = Vcard1FieldsString ++ "\n" ++ Vcard2FieldsString ++ "\n\n" ++ VcardXEP, + longdesc = Vcard1FieldsString ++ "\n" ++ + Vcard2FieldsString ++ "\n\n" ++ VcardXEP, module = ?MODULE, function = set_vcard, - args = [{user, binary}, {host, binary}, {name, binary}, {content, binary}], + args = [{user, binary}, {host, binary}, {name, binary}, + {content, binary}], result = {res, rescode}}, #ejabberd_commands{name = set_vcard2, tags = [vcard], desc = "Set content in a vCard subfield", - longdesc = Vcard2FieldsString ++ "\n\n" ++ Vcard1FieldsString ++ "\n" ++ VcardXEP, + longdesc = Vcard2FieldsString ++ "\n\n" ++ + Vcard1FieldsString ++ "\n" ++ VcardXEP, module = ?MODULE, function = set_vcard, - args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}, {content, binary}], + args = [{user, binary}, {host, binary}, {name, binary}, + {subname, binary}, {content, binary}], result = {res, rescode}}, #ejabberd_commands{name = set_vcard2_multi, tags = [vcard], desc = "Set multiple contents in a vCard subfield", - longdesc = Vcard2FieldsString ++ "\n\n" ++ Vcard1FieldsString ++ "\n" ++ VcardXEP, + longdesc = Vcard2FieldsString ++ "\n\n" ++ + Vcard1FieldsString ++ "\n" ++ VcardXEP, module = ?MODULE, function = set_vcard, args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}, {contents, {list, {value, binary}}}], result = {res, rescode}}, + %%%%%%%%%%%%%%%%%% Roster #ejabberd_commands{name = add_rosteritem, tags = [roster], desc = "Add an item to a user's roster (supports ODBC)", module = ?MODULE, function = add_rosteritem, @@ -385,13 +547,17 @@ get_commands_spec() -> %%{"", "example: add-roster peter localhost mike server.com MiKe Employees both"}, %%{"", "will add mike@server.com to peter@localhost roster"}, #ejabberd_commands{name = delete_rosteritem, tags = [roster], - desc = "Delete an item from a user's roster (supports ODBC)", + desc = "Delete an item from a user's roster " + "(supports ODBC)", module = ?MODULE, function = delete_rosteritem, args = [{localuser, binary}, {localserver, binary}, {user, binary}, {server, binary}], result = {res, rescode}}, + + % XXX Only works with mnesia #ejabberd_commands{name = process_rosteritems, tags = [roster], - desc = "List or delete rosteritems that match filtering options", + desc = "List or delete rosteritems that match " + "filtering options (only if roster is in Mnesia)", longdesc = "Explanation of each argument:\n" " - action: what to do with each rosteritem that " "matches all the filtering options\n" @@ -447,42 +613,109 @@ get_commands_spec() -> args = [{file, binary}, {user, binary}, {host, binary}], result = {res, rescode}}, #ejabberd_commands{name = push_roster_all, tags = [roster], - desc = "Push template roster from file to all those users", + desc = "Push template roster from file to all " + "those users", module = ?MODULE, function = push_roster_all, args = [{file, binary}], result = {res, rescode}}, #ejabberd_commands{name = push_alltoall, tags = [roster], - desc = "Add all the users to all the users of Host in Group", + desc = "Add all the users to all the users of " + "Host in Group", module = ?MODULE, function = push_alltoall, args = [{host, binary}, {group, binary}], result = {res, rescode}}, - #ejabberd_commands{name = get_last, tags = [last], - desc = "Get last activity information (timestamp and status)", - longdesc = "Timestamp is the seconds since" - "1970-01-01 00:00:00 UTC, for example: date +%s", - module = ?MODULE, function = get_last, - args = [{user, binary}, {host, binary}], - result = {last_activity, string}}, - #ejabberd_commands{name = set_last, tags = [last], - desc = "Set last activity information", - longdesc = "Timestamp is the seconds since" - "1970-01-01 00:00:00 UTC, for example: date +%s", - module = mod_last, function = store_last_info, - args = [{user, binary}, {host, binary}, {timestamp, integer}, {status, binary}], - result = {res, rescode}}, + #ejabberd_commands{name = link_contacts, + tags = [roster], + desc = "Add a symmetrical entry in two users roster", + longdesc = + "jid1 is the JabberID of the user1 you " + "would like to add in user2 roster on " + "the server.\nnick1 is the nick of user1.\ngro" + "up1 is the group name when adding user1 " + "to user2 roster.\njid2 is the JabberID " + "of the user2 you would like to add in " + "user1 roster on the server.\nnick2 is " + "the nick of user2.\ngroup2 is the group " + "name when adding user2 to user1 roster.\n\nTh" + "is mechanism bypasses the standard roster " + "approval addition mechanism and should " + "only be userd for server administration " + "or server integration purpose.", + module = ?MODULE, function = link_contacts, + args = + [{jid1, binary}, {nick1, binary}, {group1, binary}, + {jid2, binary}, {nick2, binary}, {group2, binary}], + result = {res, integer}}, + #ejabberd_commands{name = unlink_contacts, + tags = [roster], + desc = "Remove a symmetrical entry in two users roster", + longdesc = + "jid1 is the JabberID of the user1.\njid2 " + "is the JabberID of the user2.\n\nThis " + "mechanism bypass the standard roster " + "approval addition mechanism and should " + "only be used for server administration " + "or server integration purpose.", + module = ?MODULE, function = unlink_contacts, + args = [{jid1, binary}, {jid2, binary}], + result = {res, integer}}, + #ejabberd_commands{name = add_contacts, + tags = [roster], + desc = + "Call add_rosteritem with subscription " + "\"both\" for a given list of contacts. " + "Returns number of added items." , + module = ?MODULE, function = add_contacts, + args = + [{user, binary}, {server, binary}, + {contacts, + {list, + {contact, + {tuple, + [{jid, binary}, {group, binary}, + {nick, binary}]}}}}], + result = {res, integer}}, + #ejabberd_commands{name = remove_contacts, + tags = [roster], + desc = "Call del_rosteritem for a list of contacts", + module = ?MODULE, function = remove_contacts, + args = + [{user, binary}, {server, binary}, + {contacts, {list, {jid, binary}}}], + result = {res, integer}}, + #ejabberd_commands{name = update_roster, tags = [roster], + desc = "Add and remove contacts from user roster in one shot", + module = ?MODULE, function = update_roster, + args = [{username, binary}, {domain, binary}, + {add, {list, {contact, + {list, {property, + {tuple, [{name, binary}, + {value, binary} + ]}}}}}}, + {delete, + {list, {contact, + {list, {property, + {tuple, + [{name, binary},{value, binary}]} + }}}}}], + result = {res, restuple}}, + %%%%%%%%%%%%%%%%%% Private storage #ejabberd_commands{name = private_get, tags = [private], desc = "Get some information from a user private storage", module = ?MODULE, function = private_get, - args = [{user, binary}, {host, binary}, {element, binary}, {ns, binary}], + args = [{user, binary}, {host, binary}, + {element, binary}, {ns, binary}], result = {res, string}}, #ejabberd_commands{name = private_set, tags = [private], desc = "Set to the user private storage", module = ?MODULE, function = private_set, - args = [{user, binary}, {host, binary}, {element, binary}], + args = [{user, binary}, {host, binary}, + {element, binary}], result = {res, rescode}}, + %%%%%%%%%%%%%%%%%% Shared roster #ejabberd_commands{name = srg_create, tags = [shared_roster_group], desc = "Create a Shared Roster Group", longdesc = "If you want to specify several group " @@ -494,7 +727,8 @@ get_commands_spec() -> "name desc \\\"group1\\\\ngroup2\\\"", module = ?MODULE, function = srg_create, args = [{group, binary}, {host, binary}, - {name, binary}, {description, binary}, {display, binary}], + {name, binary}, {description, binary}, + {display, binary}], result = {res, rescode}}, #ejabberd_commands{name = srg_delete, tags = [shared_roster_group], desc = "Delete a Shared Roster Group", @@ -510,7 +744,10 @@ get_commands_spec() -> desc = "Get info of a Shared Roster Group", module = ?MODULE, function = srg_get_info, args = [{group, binary}, {host, binary}], - result = {informations, {list, {information, {tuple, [{key, string}, {value, string}]}}}}}, + result = {informations, + {list, {information, + {tuple, [{key, string}, + {value, string}]}}}}}, #ejabberd_commands{name = srg_get_members, tags = [shared_roster_group], desc = "Get members of a Shared Roster Group", module = ?MODULE, function = srg_get_members, @@ -519,23 +756,21 @@ get_commands_spec() -> #ejabberd_commands{name = srg_user_add, tags = [shared_roster_group], desc = "Add the JID user@host to the Shared Roster Group", module = ?MODULE, function = srg_user_add, - args = [{user, binary}, {host, binary}, {group, binary}, {grouphost, binary}], + args = [{user, binary}, {host, binary}, + {group, binary}, {grouphost, binary}], result = {res, rescode}}, #ejabberd_commands{name = srg_user_del, tags = [shared_roster_group], - desc = "Delete this JID user@host from the Shared Roster Group", + desc = "Delete this JID user@host from the " + "Shared Roster Group", module = ?MODULE, function = srg_user_del, - args = [{user, binary}, {host, binary}, {group, binary}, {grouphost, binary}], + args = [{user, binary}, {host, binary}, + {group, binary}, {grouphost, binary}], result = {res, rescode}}, - #ejabberd_commands{name = get_offline_count, - tags = [offline], - desc = "Get the number of unread offline messages", - policy = user, - module = mod_offline, function = count_offline_messages, - args = [], - result = {res, integer}}, + %%%%%%%%%%%%%%%%%% Stanza #ejabberd_commands{name = send_message, tags = [stanza], - desc = "Send a message to a local or remote bare of full JID", + desc = "Send a message to a local or remote " + "bare of full JID", module = ?MODULE, function = send_message, args = [{type, binary}, {from, binary}, {to, binary}, {subject, binary}, {body, binary}], @@ -543,19 +778,24 @@ get_commands_spec() -> #ejabberd_commands{name = send_stanza_c2s, tags = [stanza], desc = "Send a stanza as if sent from a c2s session", module = ?MODULE, function = send_stanza_c2s, - args = [{user, binary}, {host, binary}, {resource, binary}, {stanza, binary}], + args = [{user, binary}, {host, binary}, + {resource, binary}, {stanza, binary}], result = {res, rescode}}, #ejabberd_commands{name = send_stanza, tags = [stanza], desc = "Send a stanza; provide From JID and valid To JID", module = ?MODULE, function = send_stanza, args = [{from, binary}, {to, binary}, {stanza, binary}], result = {res, rescode}}, + + %%%%%%%%%%%%%%%%%% Privacy list #ejabberd_commands{name = privacy_set, tags = [stanza], desc = "Send a IQ set privacy stanza for a local account", module = ?MODULE, function = privacy_set, - args = [{user, binary}, {host, binary}, {xmlquery, binary}], + args = [{user, binary}, {host, binary}, + {xmlquery, binary}], result = {res, rescode}}, + %%%%%%%%%%%%%%%%%% Statistics #ejabberd_commands{name = stats, tags = [stats], desc = "Get statistical value: registeredusers onlineusers onlineusersnode uptimeseconds processes", policy = admin, @@ -563,13 +803,22 @@ get_commands_spec() -> args = [{name, binary}], result = {stat, integer}}, #ejabberd_commands{name = stats_host, tags = [stats], - desc = "Get statistical value for this host: registeredusers onlineusers", + desc = "Get statistical value for this host: " + "registeredusers onlineusers", policy = admin, module = ?MODULE, function = stats, args = [{name, binary}, {host, binary}], - result = {stat, integer}} - ]. + result = {stat, integer}}, + %%%%%%%%%%%%%%%%%% Offline + #ejabberd_commands{name = get_offline_count, + tags = [offline], + desc = "Get the number of unread offline messages", + policy = user, + module = mod_offline, function = get_queue_length, + args = [], + result = {res, integer}} + ]. %%% %%% Node @@ -585,47 +834,79 @@ remove_node(Node) -> mnesia:del_table_copy(schema, list_to_atom(Node)), ok. +%%% +%%% Adminsys +%%% + +restart_module(Module, Host) when is_binary(Module) -> + restart_module(jlib:binary_to_atom(Module), Host); +restart_module(Module, Host) when is_atom(Module) -> + List = gen_mod:loaded_modules_with_opts(Host), + case proplists:get_value(Module, List) of + undefined -> + % not a running module, force code reload anyway + code:purge(Module), + code:delete(Module), + code:load_file(Module), + 1; + Opts -> + gen_mod:stop_module(Host, Module), + case code:soft_purge(Module) of + true -> + code:delete(Module), + code:load_file(Module), + gen_mod:start_module(Host, Module, Opts), + 0; + false -> + gen_mod:start_module(Host, Module, Opts), + 2 + end + end. + + %%% %%% Accounts %%% -set_password(User, Host, Password) -> - case ejabberd_auth:set_password(User, Host, Password) of - ok -> - ok; - _ -> - error - end. +change_password(U, S, P) -> + Fun = fun () -> ejabberd_auth:set_password(U, S, P) end, + user_action(U, S, Fun, ok). + %% Copied some code from ejabberd_commands.erl check_password_hash(User, Host, PasswordHash, HashMethod) -> AccountPass = ejabberd_auth:get_password_s(User, Host), AccountPassHash = case {AccountPass, HashMethod} of {A, _} when is_tuple(A) -> scrammed; - {_, "md5"} -> get_md5(AccountPass); - {_, "sha"} -> get_sha(AccountPass); - _ -> undefined + {_, <<"md5">>} -> get_md5(AccountPass); + {_, <<"sha">>} -> get_sha(AccountPass); + {_, _Method} -> + ?ERROR_MSG("check_password_hash called " + "with hash method", [_Method]), + undefined end, case AccountPassHash of scrammed -> - ?ERROR_MSG("Passwords are scrammed, and check_password_hash can not work.", []), + ?ERROR_MSG("Passwords are scrammed " + "and check_password_hash can not work.", []), throw(passwords_scrammed_command_cannot_work); - undefined -> error; + undefined -> throw(unkown_hash_method); PasswordHash -> ok; - _ -> error + _ -> false end. get_md5(AccountPass) -> - lists:flatten([io_lib:format("~.16B", [X]) - || X <- binary_to_list(erlang:md5(AccountPass))]). + iolist_to_binary([io_lib:format("~2.16.0B", [X]) + || X <- binary_to_list(erlang:md5(AccountPass))]). get_sha(AccountPass) -> - lists:flatten([io_lib:format("~.16B", [X]) - || X <- binary_to_list(p1_sha:sha1(AccountPass))]). + iolist_to_binary([io_lib:format("~2.16.0B", [X]) + || X <- binary_to_list(p1_sha:sha1(AccountPass))]). num_active_users(Host, Days) -> - list_last_activity(Host, true, Days). + DB_Type = gen_mod:db_type(Host, mod_last), + list_last_activity(Host, true, Days, DB_Type). %% Code based on ejabberd/src/web/ejabberd_web_admin.erl -list_last_activity(Host, Integral, Days) -> +list_last_activity(Host, Integral, Days, mnesia) -> TimeStamp = p1_time_compat:system_time(seconds), TS = TimeStamp - Days * 86400, case catch mnesia:dirty_select( @@ -651,7 +932,11 @@ list_last_activity(Host, Integral, Days) -> end, lists:nth(Days, Hist ++ Tail) end - end. + end; +list_last_activity(_Host, _Integral, _Days, DB_Type) -> + throw({error, iolist_to_binary(io_lib:format("Unsupported backend: ~p", + [DB_Type]))}). + histogram(Values, Integral) -> histogram(lists:sort(Values), Integral, 0, 0, []). histogram([H | T], Integral, Current, Count, Hist) when Current == H -> @@ -734,6 +1019,77 @@ delete_old_users(Days, Users) -> Users_removed = lists:filter(F, Users), {removed, length(Users_removed), Users_removed}. +rename_account(U, S, NU, NS) -> + case ejabberd_auth:is_user_exists(U, S) of + true -> + case ejabberd_auth:get_password(U, S) of + false -> 1; + Password -> + case ejabberd_auth:try_register(NU, NS, Password) of + {atomic, ok} -> + OldJID = jlib:jid_to_string({U, S, <<"">>}), + NewJID = jlib:jid_to_string({NU, NS, <<"">>}), + Roster = get_roster2(U, S), + lists:foreach(fun (#roster{jid = {RU, RS, RE}, + name = Nick, + groups = Groups}) -> + NewGroup = extract_group(Groups), + {NewNick, Group} = case + lists:filter(fun + (#roster{jid + = + {PU, + PS, + _}}) -> + (PU + == + U) + and + (PS + == + S) + end, + get_roster2(RU, + RS)) + of + [#roster{name = + OldNick, + groups + = + OldGroups} + | _] -> + {OldNick, + extract_group(OldGroups)}; + [] -> {NU, []} + end, + JIDStr = jlib:jid_to_string({RU, RS, + RE}), + link_contacts2(NewJID, NewNick, + NewGroup, JIDStr, + Nick, Group), + unlink_contacts2(OldJID, JIDStr) + end, + Roster), + ejabberd_auth:remove_user(U, S), + 0; + {atomic, exists} -> 409; + _ -> 1 + end + end; + false -> 404 + end. + + +check_users_registration(Users) -> + lists:map(fun ({U, S}) -> + Registered = case ejabberd_auth:is_user_exists(U, S) of + true -> 1; + false -> 0 + end, + {U, S, Registered} + end, + Users). + %% %% Ban account @@ -750,6 +1106,22 @@ kick_sessions(User, Server, Reason) -> end, ejabberd_sm:get_user_resources(User, Server)). +get_presence(U, S) -> + case ejabberd_auth:is_user_exists(U, S) of + true -> + {Resource, Show, Status} = get_presence2(U, S), + FullJID = jlib:jid_to_string({U, S, Resource}), + {FullJID, Show, Status}; + false -> throw({not_found, <<"unknown_user">>}) + end. + +get_sessions(User, Server) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + Sessions = mnesia:dirty_index_read(session, {LUser, LServer}, #session.us), + true = is_list(Sessions), + Sessions. + set_random_password(User, Server, Reason) -> NewPass = build_random_password(Reason), set_password_auth(User, Server, NewPass). @@ -782,7 +1154,9 @@ resource_num(User, Host, Num) -> true -> lists:nth(Num, Resources); false -> - lists:flatten(io_lib:format("Error: Wrong resource number: ~p", [Num])) + throw({bad_argument, + lists:flatten(io_lib:format("Wrong resource number: ~p", + [Num]))}) end. kick_session(User, Server, Resource, ReasonText) -> @@ -908,26 +1282,48 @@ user_sessions_info(User, Host) -> Sessions). +%% ----------------------------- +%% Internal session handling +%% ----------------------------- + +get_presence2(User, Server) -> + case get_sessions(User, Server) of + [] -> {<<"">>, <<"unavailable">>, <<"">>}; + Ss -> + Session = hd(Ss), + if Session#session.priority >= 0 -> + Pid = element(2, Session#session.sid), + {_User, Resource, Show, Status} = + ejabberd_c2s:get_presence(Pid), + {Resource, Show, Status}; + true -> {<<"">>, <<"unavailable">>, <<"">>} + end + end. + %%% %%% Vcard %%% -set_nickname(User, Host, Nickname) -> - R = mod_vcard:process_sm_iq( - {jid, User, Host, <<>>, User, Host, <<>>}, - {jid, User, Host, <<>>, User, Host, <<>>}, - {iq, <<>>, set, <<>>, <<"en">>, - {xmlel, <<"vCard">>, [ - {<<"xmlns">>, <<"vcard-temp">>}], [ - {xmlel, <<"NICKNAME">>, [], [{xmlcdata, Nickname}]} - ] - }}), - case R of - {iq, <<>>, result, <<>>, _L, []} -> - ok; - _ -> - error - end. +set_nickname(U, S, N) -> + JID = jlib:make_jid({U, S, <<"">>}), + Fun = fun () -> + case mod_vcard:process_sm_iq( + JID, JID, + #iq{type = set, + lang = <<"en">>, + sub_el = + #xmlel{name = <<"vCard">>, + attrs = [{<<"xmlns">>, ?NS_VCARD}], + children = + [#xmlel{name = <<"NICKNAME">>, + attrs = [], + children = + [{xmlcdata, N}]}]}}) of + #iq{type = result} -> ok; + _ -> error + end + end, + user_action(U, S, Fun, ok). get_vcard(User, Host, Name) -> [Res | _] = get_vcard_content(User, Host, [Name]), @@ -1029,8 +1425,8 @@ take_vcard_tel(_TelType, [], NewEls, Taken) -> update_vcard_els([<<"TEL">>, TelType], [TelValue], OldEls) -> {_, NewEls} = take_vcard_tel(TelType, OldEls, [], not_found), NewEl = {xmlel,<<"TEL">>,[], - [{xmlel,TelType,[],[]}, - {xmlel,<<"NUMBER">>,[],[{xmlcdata,TelValue}]}]}, + [{xmlel,TelType,[],[]}, + {xmlel,<<"NUMBER">>,[],[{xmlcdata,TelValue}]}]}, [NewEl | NewEls]; update_vcard_els(Data, ContentList, Els1) -> @@ -1061,7 +1457,7 @@ update_vcard_els(Data, ContentList, Els1) -> add_rosteritem(LocalUser, LocalServer, User, Server, Nick, Group, Subs) -> case add_rosteritem(LocalUser, LocalServer, User, Server, Nick, Group, Subs, []) of - {atomic, ok} -> + {atomic, _} -> push_roster_item(LocalUser, LocalServer, User, Server, {add, Nick, Subs, Group}), ok; _ -> @@ -1076,12 +1472,12 @@ subscribe(LU, LS, User, Server, Nick, Group, Subscription, _Xattrs) -> mod_roster:set_items( LU, LS, {xmlel, <<"query">>, - [{<<"xmlns">>, ?NS_ROSTER}], - [ItemEl]}). + [{<<"xmlns">>, ?NS_ROSTER}], + [ItemEl]}). delete_rosteritem(LocalUser, LocalServer, User, Server) -> case unsubscribe(LocalUser, LocalServer, User, Server) of - {atomic, ok} -> + {atomic, _} -> push_roster_item(LocalUser, LocalServer, User, Server, remove), ok; _ -> @@ -1093,8 +1489,86 @@ unsubscribe(LU, LS, User, Server) -> mod_roster:set_items( LU, LS, {xmlel, <<"query">>, - [{<<"xmlns">>, ?NS_ROSTER}], - [ItemEl]}). + [{<<"xmlns">>, ?NS_ROSTER}], + [ItemEl]}). + + +link_contacts(JID1, Nick1, Group1, JID2, Nick2, Group2) -> + {U1, S1, _} = + jlib:jid_tolower(jlib:string_to_jid(JID1)), + {U2, S2, _} = + jlib:jid_tolower(jlib:string_to_jid(JID2)), + case {ejabberd_auth:is_user_exists(U1, S1), + ejabberd_auth:is_user_exists(U2, S2)} + of + {true, true} -> + case link_contacts2(JID1, Nick1, Group1, JID2, Nick2, + Group2) + of + ok -> 0; + _ -> 1 + end; + _ -> 404 + end. + +unlink_contacts(JID1, JID2) -> + {U1, S1, _} = + jlib:jid_tolower(jlib:string_to_jid(JID1)), + {U2, S2, _} = + jlib:jid_tolower(jlib:string_to_jid(JID2)), + case {ejabberd_auth:is_user_exists(U1, S1), + ejabberd_auth:is_user_exists(U2, S2)} + of + {true, true} -> + case unlink_contacts2(JID1, JID2) of + ok -> 0; + _ -> 1 + end; + _ -> 404 + end. + + +add_contacts(U, S, Contacts) -> + case ejabberd_auth:is_user_exists(U, S) of + true -> + JID1 = jlib:jid_to_string({U, S, <<"">>}), + lists:foldl(fun ({JID2, Group, Nick}, Acc) -> + {PU, PS, _} = + jlib:jid_tolower(jlib:string_to_jid(JID2)), + case ejabberd_auth:is_user_exists(PU, PS) of + true -> + case link_contacts2(JID1, <<"">>, Group, + JID2, Nick, Group) + of + ok -> Acc + 1; + _ -> Acc + end; + false -> Acc + end + end, + 0, Contacts); + false -> 404 + end. + +remove_contacts(U, S, Contacts) -> + case ejabberd_auth:is_user_exists(U, S) of + true -> + JID1 = jlib:jid_to_string({U, S, <<"">>}), + lists:foldl(fun (JID2, Acc) -> + {PU, PS, _} = + jlib:jid_tolower(jlib:string_to_jid(JID2)), + case ejabberd_auth:is_user_exists(PU, PS) of + true -> + case unlink_contacts2(JID1, JID2) of + ok -> Acc + 1; + _ -> Acc + end; + false -> Acc + end + end, + 0, Contacts); + false -> 404 + end. %% ----------------------------- %% Get Roster @@ -1170,6 +1644,7 @@ build_list_users(Group, [{User, Server}|Users], Res) -> %% @doc Push to the roster of account LU@LS the contact U@S. %% The specific action to perform is defined in Action. push_roster_item(LU, LS, U, S, Action) -> + mod_roster:invalidate_roster_cache(jlib:nodeprep(LU), jlib:nameprep(LS)), lists:foreach(fun(R) -> push_roster_item(LU, LS, R, U, S, Action) end, ejabberd_sm:get_user_resources(LU, LS)). @@ -1215,29 +1690,124 @@ build_broadcast(U, S, remove) -> build_broadcast(U, S, SubsAtom) when is_atom(SubsAtom) -> {broadcast, {item, {U, S, <<>>}, SubsAtom}}. + + +update_roster(User, Host, Add, Del) when is_list(Add), is_list(Del) -> + Server = case Host of + <<>> -> + [Default|_] = ejabberd_config:get_myhosts(), + Default; + _ -> + Host + end, + case ejabberd_auth:is_user_exists(User, Server) of + true -> + AddFun = fun({Item}) -> + [Contact, Nick, Sub] = match(Item, [ + {<<"username">>, <<>>}, + {<<"nick">>, <<>>}, + {<<"subscription">>, <<"both">>}]), + add_rosteritem(User, Server, + Contact, Server, Nick, <<>>, Sub) + end, + AddRes = [AddFun(I) || I <- Add], + case lists:all(fun(X) -> X==ok end, AddRes) of + true -> + DelFun = fun({Item}) -> + [Contact] = match(Item, [{<<"username">>, <<>>}]), + delete_rosteritem(User, Server, Contact, Server) + end, + [DelFun(I) || I <- Del], + ok; + false -> + %% try rollback if errors + DelFun = fun({Item}) -> + [Contact] = match(Item, [{<<"username">>, <<>>}]), + delete_rosteritem(User, Server, Contact, Server) + end, + [DelFun(I) || I <- Add], + String = iolist_to_binary(io_lib:format("Internal error updating " + "roster for user ~s@~s at node ~p", + [User, Host, node()])), + {roster_update_error, String} + end; + false -> + String = iolist_to_binary(io_lib:format("User ~s@~s not found at node ~p", + [User, Host, node()])), + {invalid_user, String} + end. + +match(Args, Spec) -> + [proplists:get_value(Key, Args, Default) || {Key, Default} <- Spec]. + + +%% ----------------------------- +%% Internal roster handling +%% ----------------------------- + +get_roster2(User, Server) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + ejabberd_hooks:run_fold(roster_get, LServer, [], [{LUser, LServer}]). + +extract_group([]) -> []; +%extract_group([Group|_Groups]) -> Group. +extract_group(Groups) -> str:join(Groups, <<";">>). + +link_contacts2(JID1, Nick1, Group1, JID2, Nick2, Group2) -> + {U1, S1, _} = + jlib:jid_tolower(jlib:string_to_jid(JID1)), + {U2, S2, _} = + jlib:jid_tolower(jlib:string_to_jid(JID2)), + case add_rosteritem2(U1, S1, JID2, Nick2, Group1, + <<"both">>) + of + ok -> + add_rosteritem2(U2, S2, JID1, Nick1, Group2, <<"both">>); + Error -> Error + end. + +unlink_contacts2(JID1, JID2) -> + {U1, S1, _} = + jlib:jid_tolower(jlib:string_to_jid(JID1)), + {U2, S2, _} = + jlib:jid_tolower(jlib:string_to_jid(JID2)), + case delete_rosteritem(U1, S1, JID2) of + ok -> delete_rosteritem(U2, S2, JID1); + Error -> Error + end. + +add_rosteritem2(User, Server, JID, Nick, Group, Subscription) -> + {U, S, _} = jlib:jid_tolower(jlib:string_to_jid(JID)), + add_rosteritem(User, Server, U, S, Nick, Group, Subscription). + +delete_rosteritem(User, Server, JID) -> + {U, S, _} = jlib:jid_tolower(jlib:string_to_jid(JID)), + delete_rosteritem(User, Server, U, S). + %%% %%% Last Activity %%% get_last(User, Server) -> case ejabberd_sm:get_user_resources(User, Server) of - [] -> - case mod_last:get_last_info(User, Server) of - not_found -> - "Never"; - {ok, Shift, Status} -> - TimeStamp = {Shift div 1000000, - Shift rem 1000000, - 0}, - {{Year, Month, Day}, {Hour, Minute, Second}} = - calendar:now_to_local_time(TimeStamp), - lists:flatten( - io_lib:format( - "~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w ~s", - [Year, Month, Day, Hour, Minute, Second, Status])) - end; - _ -> - "Online" + [] -> + case mod_last:get_last_info(User, Server) of + not_found -> + "Never"; + {ok, Shift, Status} -> + TimeStamp = {Shift div 1000000, + Shift rem 1000000, + 0}, + {{Year, Month, Day}, {Hour, Minute, Second}} = + calendar:now_to_local_time(TimeStamp), + lists:flatten( + io_lib:format( + "~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w ~s", + [Year, Month, Day, Hour, Minute, Second, Status])) + end; + _ -> + "Online" end. %%% @@ -1294,11 +1864,11 @@ srg_create(Group, Host, Name, Description, Display) -> Opts = [{name, Name}, {displayed_groups, DisplayList}, {description, Description}], - {atomic, ok} = mod_shared_roster:create_group(Host, Group, Opts), + {atomic, _} = mod_shared_roster:create_group(Host, Group, Opts), ok. srg_delete(Group, Host) -> - {atomic, ok} = mod_shared_roster:delete_group(Host, Group), + {atomic, _} = mod_shared_roster:delete_group(Host, Group), ok. srg_list(Host) -> @@ -1322,11 +1892,11 @@ srg_get_members(Group, Host) -> || {MUser, MServer} <- Members]. srg_user_add(User, Host, Group, GroupHost) -> - {atomic, ok} = mod_shared_roster:add_user_to_group(GroupHost, {User, Host}, Group), + {atomic, _} = mod_shared_roster:add_user_to_group(GroupHost, {User, Host}, Group), ok. srg_user_del(User, Host, Group, GroupHost) -> - {atomic, ok} = mod_shared_roster:remove_user_from_group(GroupHost, {User, Host}, Group), + {atomic, _} = mod_shared_roster:remove_user_from_group(GroupHost, {User, Host}, Group), ok. @@ -1569,6 +2139,21 @@ decide_rip_jid({UName, UServer}, Match_list) -> end, Match_list). +user_action(User, Server, Fun, OK) -> + case ejabberd_auth:is_user_exists(User, Server) of + true -> + case catch Fun() of + OK -> ok; + {error, Error} -> throw(Error); + _Error -> + ?ERROR_MSG("Command returned: ~p", [_Error]), + 1 + end; + false -> + throw({not_found, "unknown_user"}) + end. + + %% Copied from ejabberd-2.0.0/src/acl.erl is_regexp_match(String, RegExp) -> case ejabberd_regexp:run(String, RegExp) of diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index 15fe36364..f2b7a484b 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -29,6 +29,11 @@ %% request_handlers: %% "/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: %% commands_admin_access: configure %% commands: @@ -76,6 +81,8 @@ -include("logger.hrl"). -include("ejabberd_http.hrl"). +-define(DEFAULT_API_VERSION, 0). + -define(CT_PLAIN, {<<"Content-Type">>, <<"text/plain">>}). @@ -179,7 +186,8 @@ check_permissions2(#request{ip={IP, _Port}}, Call) -> true -> {allowed, Call, admin}; _ -> unauthorized_response() end; - _ -> + _E -> + ?DEBUG("Unauthorized: ~p", [_E]), unauthorized_response() end. @@ -192,10 +200,13 @@ oauth_check_token(Scope, Token) -> %% command processing %% ------------------ +%process(Call, Request) -> +% ?DEBUG("~p~n~p", [Call, Request]), ok; process(_, #request{method = 'POST', data = <<>>}) -> ?DEBUG("Bad Request: no data", []), - badrequest_response(); + badrequest_response(<<"Missing POST data">>); process([Call], #request{method = 'POST', data = Data, ip = IP} = Req) -> + Version = get_api_version(Req), try Args = case jiffy:decode(Data) of List when is_list(List) -> List; @@ -205,16 +216,20 @@ process([Call], #request{method = 'POST', data = Data, ip = IP} = Req) -> log(Call, Args, IP), case check_permissions(Req, Call) of {allowed, Cmd, Auth} -> - {Code, Result} = handle(Cmd, Auth, Args), + {Code, Result} = handle(Cmd, Auth, Args, Version), json_response(Code, jiffy:encode(Result)); ErrorResponse -> %% Should we reply 403 ? ErrorResponse end - catch _:Error -> - ?DEBUG("Bad Request: ~p", [Error]), + catch _:{error,{_,invalid_json}} = _Err -> + ?DEBUG("Bad Request: ~p", [_Err]), + badrequest_response(<<"Invalid JSON input">>); + _:_Error -> + ?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]), badrequest_response() end; process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) -> + Version = get_api_version(Req), try Args = case Data of [{nokey, <<>>}] -> []; @@ -223,13 +238,13 @@ process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) -> log(Call, Args, IP), case check_permissions(Req, Call) of {allowed, Cmd, Auth} -> - {Code, Result} = handle(Cmd, Auth, Args), + {Code, Result} = handle(Cmd, Auth, Args, Version), json_response(Code, jiffy:encode(Result)); ErrorResponse -> ErrorResponse end - catch _:Error -> - ?DEBUG("Bad Request: ~p", [Error]), + catch _:_Error -> + ?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]), badrequest_response() end; process([], #request{method = 'OPTIONS', data = <<>>}) -> @@ -238,13 +253,28 @@ process(_Path, Request) -> ?DEBUG("Bad Request: no handler ~p", [Request]), 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 %% ---------------- % generic ejabberd command handler -handle(Call, Auth, Args) when is_atom(Call), is_list(Args) -> - case ejabberd_commands:get_command_format(Call, Auth) of +handle(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) -> + case ejabberd_commands:get_command_format(Call, Auth, Version) of {ArgsSpec, _} when is_list(ArgsSpec) -> Args2 = [{jlib:binary_to_atom(Key), Value} || {Key, Value} <- Args], Spec = lists:foldr( @@ -259,22 +289,51 @@ handle(Call, Auth, Args) when is_atom(Call), is_list(Args) -> ({Key, atom}, Acc) -> [{Key, undefined}|Acc] 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("REST API Error: ~p", [Msg]), {400, Msg}; _Error -> + ?ERROR_MSG("REST API Error: ~p", [_Error]), {400, <<"Error">>} end. -handle2(Call, Auth, Args) when is_atom(Call), is_list(Args) -> - {ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth), +handle2(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) -> + {ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth, Version), ArgsFormatted = format_args(Args, ArgsF), - case ejabberd_command(Auth, Call, ArgsFormatted, 400) of - 0 -> {200, <<"OK">>}; - 1 -> {500, <<"500 Internal server error">>}; - 400 -> {400, <<"400 Bad Request">>}; - 404 -> {404, <<"404 Not found">>}; - Res -> format_command_result(Call, Auth, Res) + case ejabberd_commands:execute_command(undefined, Auth, + Call, ArgsFormatted, Version) of + {error, Error} -> + throw(Error); + Res -> + format_command_result(Call, Auth, Res, Version) end. get_elem_delete(A, L) -> @@ -339,7 +398,9 @@ format_arg(undefined, binary) -> <<>>; format_arg(undefined, string) -> <<>>; format_arg(Arg, Format) -> ?ERROR_MSG("don't know how to format Arg ~p for format ~p", [Arg, Format]), - error. + throw({invalid_parameter, + io_lib:format("Arg ~p is not in format ~p", + [Arg, Format])}). process_unicode_codepoints(Str) -> iolist_to_binary(lists:map(fun(X) when X > 255 -> unicode:characters_to_binary([X]); @@ -353,36 +414,26 @@ process_unicode_codepoints(Str) -> match(Args, 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) -> - {_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth), +format_command_result(Cmd, Auth, Result, Version) -> + {_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version), case {ResultFormat, Result} of - {{_, rescode}, V} when V == true; V == ok -> - {200, <<"">>}; - {{_, rescode}, _} -> - {500, <<"">>}; - {{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok -> - {200, iolist_to_binary(Text1)}; - {{_, restuple}, {_, Text2}} -> - {500, iolist_to_binary(Text2)}; - {{_, {list, _}}, _V} -> - {_, L} = format_result(Result, ResultFormat), - {200, L}; - {{_, {tuple, _}}, _V} -> - {_, T} = format_result(Result, ResultFormat), - {200, T}; - _ -> - {200, {[format_result(Result, ResultFormat)]}} + {{_, rescode}, V} when V == true; V == ok -> + {200, 0}; + {{_, rescode}, _} -> + {200, 1}; + {{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok -> + {200, iolist_to_binary(Text1)}; + {{_, restuple}, {_, Text2}} -> + {500, iolist_to_binary(Text2)}; + {{_, {list, _}}, _V} -> + {_, L} = format_result(Result, ResultFormat), + {200, L}; + {{_, {tuple, _}}, _V} -> + {_, T} = format_result(Result, ResultFormat), + {200, T}; + _ -> + {200, {[format_result(Result, ResultFormat)]}} end. format_result(Atom, {Name, atom}) -> @@ -421,14 +472,15 @@ format_result(404, {_Name, _}) -> "not_found". unauthorized_response() -> - {401, ?HEADER(?CT_XML), - #xmlel{name = <<"h1">>, attrs = [], - children = [{xmlcdata, <<"401 Unauthorized">>}]}}. + unauthorized_response(<<"401 Unauthorized">>). +unauthorized_response(Body) -> + json_response(401, jiffy:encode(Body)). badrequest_response() -> - {400, ?HEADER(?CT_XML), - #xmlel{name = <<"h1">>, attrs = [], - children = [{xmlcdata, <<"400 Bad Request">>}]}}. + badrequest_response(<<"400 Bad Request">>). +badrequest_response(Body) -> + json_response(400, jiffy:encode(Body)). + json_response(Code, Body) when is_integer(Code) -> {Code, ?HEADER(?CT_JSON), Body}. diff --git a/test/ejabberd_admin_test.exs b/test/ejabberd_admin_test.exs new file mode 100644 index 000000000..1c999314c --- /dev/null +++ b/test/ejabberd_admin_test.exs @@ -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 diff --git a/test/ejabberd_auth_mock.exs b/test/ejabberd_auth_mock.exs new file mode 100644 index 000000000..495c527f2 --- /dev/null +++ b/test/ejabberd_auth_mock.exs @@ -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 diff --git a/test/ejabberd_commands_test.exs b/test/ejabberd_commands_test.exs index 0c06fc2ca..b3f10000e 100644 --- a/test/ejabberd_commands_test.exs +++ b/test/ejabberd_commands_test.exs @@ -19,39 +19,406 @@ # ---------------------------------------------------------------------- defmodule EjabberdCommandsTest do - @author "mremond@process-one.net" + use ExUnit.Case, async: false - use ExUnit.Case, async: true + @author "jsautret@process-one.net" - require Record - Record.defrecord :ejabberd_commands, Record.extract(:ejabberd_commands, from_lib: "ejabberd/include/ejabberd_commands.hrl") + # mocked callback module + @module :test_module + # Admin user + @admin "admin" + @adminpass "adminpass" + # Non admin user + @user "user" + @userpass "userpass" + # XMPP domain + @domain "domain" - setup_all do - :ejabberd_commands.init - end + require Record + Record.defrecord :ejabberd_commands, Record.extract(:ejabberd_commands, + from: "ejabberd_commands.hrl") - test "Check that we can register a command" do - assert :ejabberd_commands.register_commands([user_test_command]) == :ok - commands = :ejabberd_commands.list_commands - assert Enum.member?(commands, {:test_user, [], "Test user"}) - end + setup_all do + try do + :stringprep.start + rescue + _ -> :ok + end + :mnesia.start + EjabberdOauthMock.init + :ok + 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 + setup do + :meck.unload + :meck.new(@module, [:non_strict]) + :ejabberd_commands.init + end + + test "API command can be registered, listed and unregistered" do + command = ejabberd_commands name: :test, module: @module, + function: :test_command + + assert :ok == :ejabberd_commands.register_commands [command] + commands = :ejabberd_commands.list_commands + assert Enum.member? commands, {:test, [], ''} + + assert :ok == :ejabberd_commands.unregister_commands [command] + commands = :ejabberd_commands.list_commands + refute Enum.member? commands, {:test, [], ''} + end + + + test "API command with versions can be registered, listed and unregistered" do + command1 = ejabberd_commands name: :test, module: @module, + function: :test_command, version: 1, desc: 'version1' + command3 = ejabberd_commands name: :test, module: @module, + function: :test_command, version: 3, desc: 'version3' + assert :ejabberd_commands.register_commands [command1, command3] + + version1 = {:test, [], 'version1'} + version3 = {:test, [], 'version3'} + + # 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 + + + 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 - defp user_test_command do - ejabberd_commands(name: :test_user, tags: [:roster], - desc: "Test user", - policy: :user, - module: __MODULE__, - function: :test_user, - args: [], - result: {:contacts, {:list, {:contact, {:tuple, [ - {:jid, :string}, - {:nick, :string} - ]}}}}) - end end diff --git a/test/ejabberd_hooks_test.exs b/test/ejabberd_hooks_test.exs index 6493642de..a69fbbd61 100644 --- a/test/ejabberd_hooks_test.exs +++ b/test/ejabberd_hooks_test.exs @@ -30,14 +30,14 @@ # log as we are exercising hook handler recovery from that situation. defmodule EjabberdHooksTest do - use ExUnit.Case, async: true - + use ExUnit.Case, async: false + @author "mremond@process-one.net" @host <<"domain.net">> @self __MODULE__ setup_all do - {:ok, _Pid} = :ejabberd_hooks.start_link + {:ok, _pid} = :ejabberd_hooks.start_link :ok end diff --git a/test/ejabberd_oauth_mock.exs b/test/ejabberd_oauth_mock.exs new file mode 100644 index 000000000..2c1b8cf93 --- /dev/null +++ b/test/ejabberd_oauth_mock.exs @@ -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 diff --git a/test/ejabberd_sm_mock.exs b/test/ejabberd_sm_mock.exs new file mode 100644 index 000000000..0c2fc1635 --- /dev/null +++ b/test/ejabberd_sm_mock.exs @@ -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 diff --git a/test/elixir_SUITE.erl b/test/elixir_SUITE.erl index b9a0b1a23..f2c64773b 100644 --- a/test/elixir_SUITE.erl +++ b/test/elixir_SUITE.erl @@ -19,6 +19,7 @@ init_per_suite(Config) -> check_meck(), + code:add_pathz(filename:join(test_dir(), "../include")), Config. init_per_testcase(_TestCase, Config) -> @@ -27,13 +28,13 @@ init_per_testcase(_TestCase, Config) -> all() -> case is_elixir_available() of - true -> - Dir = test_dir(), - filelib:fold_files(Dir, ".*\.exs", false, - fun(Filename, Acc) -> [list_to_atom(filename:basename(Filename)) | Acc] end, - []); - false -> - [] + true -> + Dir = test_dir(), + filelib:fold_files(Dir, ".*\.exs", false, + fun(Filename, Acc) -> [list_to_atom(filename:basename(Filename)) | Acc] end, + []); + false -> + [] end. check_meck() -> @@ -56,16 +57,21 @@ is_elixir_available() -> undefined_function(?MODULE, Func, Args) -> case lists:suffix(".exs", atom_to_list(Func)) of - true -> - run_elixir_test(Func); - false -> - error_handler:undefined_function(?MODULE, Func, Args) + true -> + run_elixir_test(Func); + false -> + error_handler:undefined_function(?MODULE, Func, Args) end; undefined_function(Module, Func, Args) -> error_handler:undefined_function(Module, Func,Args). run_elixir_test(Func) -> '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)))), %% I did not use map syntax, so that this file can still be build under R16 ResultMap = 'Elixir.ExUnit':run(), diff --git a/test/mod_admin_extra_test.exs b/test/mod_admin_extra_test.exs new file mode 100644 index 000000000..7fa39eef1 --- /dev/null +++ b/test/mod_admin_extra_test.exs @@ -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 diff --git a/test/mod_http_api_test.exs b/test/mod_http_api_test.exs new file mode 100644 index 000000000..ae62f28f9 --- /dev/null +++ b/test/mod_http_api_test.exs @@ -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 diff --git a/test/mod_last_mock.exs b/test/mod_last_mock.exs new file mode 100644 index 000000000..7e3dc5a1d --- /dev/null +++ b/test/mod_last_mock.exs @@ -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 diff --git a/test/mod_roster_mock.exs b/test/mod_roster_mock.exs new file mode 100644 index 000000000..b4991cfde --- /dev/null +++ b/test/mod_roster_mock.exs @@ -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