diff options
40 files changed, 909 insertions, 1325 deletions
diff --git a/configure.ac b/configure.ac index 884db5d4e..edf54722c 100644 --- a/configure.ac +++ b/configure.ac @@ -3,8 +3,8 @@ AC_PREREQ(2.53) AC_INIT(ejabberd, m4_esyscmd([echo `git describe --tags 2>/dev/null || echo 0.0` | sed 's/-g.*//;s/-/./' | tr -d '\012']), [ejabberd@process-one.net], [ejabberd]) -REQUIRE_ERLANG_MIN="6.1 (Erlang/OTP 17.1)" -REQUIRE_ERLANG_MAX="9.0.0 (No Max)" +REQUIRE_ERLANG_MIN="6.4 (Erlang/OTP 17.5)" +REQUIRE_ERLANG_MAX="100.0.0 (No Max)" AC_CONFIG_MACRO_DIR([m4]) diff --git a/ejabberdctl.template b/ejabberdctl.template index 9099bd3dd..edf9bea0d 100755 --- a/ejabberdctl.template +++ b/ejabberdctl.template @@ -13,23 +13,30 @@ ERLANG_NODE=ejabberd@localhost ERL="{{erl}}" IEX="{{bindir}}/iex" EPMD="{{epmd}}" -INSTALLUSER={{installuser}} +INSTALLUSER="{{installuser}}" -# check the proper system user is used if defined -EXEC_CMD="false" -if [ -n "$INSTALLUSER" ] ; then - if [ $(id -g) -eq $(id -g $INSTALLUSER || echo -1) ] ; then +# check the proper system user is used +case `id -un` in + "$INSTALLUSER") EXEC_CMD="as_current_user" - else - id -Gn | grep -q wheel && EXEC_CMD="as_install_user" - fi -else - EXEC_CMD="as_current_user" -fi -if [ "$EXEC_CMD" = "false" ] ; then - echo "ERROR: This command can only be run by root or the user $INSTALLUSER" >&2 - exit 7 -fi + ;; + root) + if [ -n "$INSTALLUSER" ] ; then + EXEC_CMD="as_install_user" + else + EXEC_CMD="as_current_user" + echo "WARNING: This is not recommended to run ejabberd as root" >&2 + fi + ;; + *) + if [ -n "$INSTALLUSER" ] ; then + echo "ERROR: This command can only be run by root or the user $INSTALLUSER" >&2 + exit 7 + else + EXEC_CMD="as_current_user" + fi + ;; +esac # parse command line parameters for arg; do @@ -62,14 +69,14 @@ done # define erl parameters ERLANG_OPTS="+K $POLL -smp $SMP +P $ERL_PROCESSES $ERL_OPTIONS" if [ "$FIREWALL_WINDOW" != "" ] ; then - ERLANG_OPTS="$ERLANG_OPTS -kernel " \ - "inet_dist_listen_min ${FIREWALL_WINDOW%-*} " \ - "inet_dist_listen_max ${FIREWALL_WINDOW#*-}" + ERLANG_OPTS="$ERLANG_OPTS -kernel \ + inet_dist_listen_min ${FIREWALL_WINDOW%-*} \ + inet_dist_listen_max ${FIREWALL_WINDOW#*-}" fi if [ "$INET_DIST_INTERFACE" != "" ] ; then INET_DIST_INTERFACE2=$("$ERL" -noshell -eval 'case inet:parse_address("'$INET_DIST_INTERFACE'") of {ok,IP} -> io:format("~p",[IP]); _ -> ok end.' -s erlang halt) if [ "$INET_DIST_INTERFACE2" != "" ] ; then - ERLANG_OPTS="$ERLANG_OPTS -kernel inet_dist_use_interface \"$INET_DIST_INTERFACE2\"" + ERLANG_OPTS="$ERLANG_OPTS -kernel inet_dist_use_interface $INET_DIST_INTERFACE2" fi fi ERL_LIBS={{libdir}} @@ -103,7 +110,7 @@ export ERL_LIBS exec_cmd() { case $EXEC_CMD in - as_install_user) su -c '"$0" $@"' "$INSTALLUSER" -- "$@" ;; + as_install_user) su -c '"$0" "$@"' "$INSTALLUSER" -- "$@" ;; as_current_user) "$@" ;; esac } @@ -223,12 +230,6 @@ check_start() "$EPMD" -kill >/dev/null } } - } || { - [ -d "$SPOOL_DIR" ] || exec_cmd mkdir -p "$SPOOL_DIR" - cd "$SPOOL_DIR" || { - echo "ERROR: ejabberd can not access directory $SPOOL_DIR" - exit 6 - } } } @@ -253,6 +254,13 @@ wait_status() [ $timeout -gt 0 ] } +# ensure we can change current directory to SPOOL_DIR +[ -d "$SPOOL_DIR" ] || exec_cmd mkdir -p "$SPOOL_DIR" +cd "$SPOOL_DIR" || { + echo "ERROR: can not access directory $SPOOL_DIR" + exit 6 +} + # main case $1 in start) diff --git a/rebar.config b/rebar.config index 240e2b6a4..160185511 100644 --- a/rebar.config +++ b/rebar.config @@ -21,17 +21,17 @@ {deps, [{lager, ".*", {git, "https://github.com/erlang-lager/lager", {tag, {if_version_above, "17", "3.4.2", "3.2.1"}}}}, {p1_utils, ".*", {git, "https://github.com/processone/p1_utils", {tag, "1.0.9"}}}, - {cache_tab, ".*", {git, "https://github.com/processone/cache_tab", {tag, "1.0.9"}}}, - {fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.0.13"}}}, + {cache_tab, ".*", {git, "https://github.com/processone/cache_tab", {tag, "1.0.10"}}}, + {fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.0.15"}}}, {stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.9"}}}, {fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.23"}}}, - {xmpp, ".*", {git, "https://github.com/processone/xmpp", {tag, "1.1.13"}}}, + {xmpp, ".*", {git, "https://github.com/processone/xmpp", {tag, "1.1.14"}}}, {fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.10"}}}, {jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "0.14.8"}}}, - {p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.1"}}}, + {p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.2"}}}, {luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "v0.2"}}}, - {if_var_true, stun, {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.12"}}}}, - {if_var_true, sip, {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.13"}}}}, + {if_var_true, stun, {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.14"}}}}, + {if_var_true, sip, {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.15"}}}}, {if_var_true, mysql, {p1_mysql, ".*", {git, "https://github.com/processone/p1_mysql", {tag, "1.0.3"}}}}, {if_var_true, pgsql, {p1_pgsql, ".*", {git, "https://github.com/processone/p1_pgsql", @@ -71,6 +71,7 @@ p1_utils, p1_mysql, p1_pgsql, + p1_oauth2, epam, ezlib, iconv]}}. diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index b52450d24..64edf508c 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -25,12 +25,11 @@ -module(ejabberd_app). --behaviour(ejabberd_config). -author('alexey@process-one.net'). -behaviour(application). --export([start/2, prep_stop/1, stop/1, opt_type/1]). +-export([start/2, prep_stop/1, stop/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -49,13 +48,12 @@ start(normal, _Args) -> setup_if_elixir_conf_used(), ejabberd_config:start(), ejabberd_mnesia:start(), - set_settings_from_config(), file_queue_init(), maybe_add_nameservers(), - connect_nodes(), case ejabberd_sup:start_link() of {ok, SupPid} -> register_elixir_config_hooks(), + ejabberd_cluster:wait_for_sync(infinity), {T2, _} = statistics(wall_clock), ?INFO_MSG("ejabberd ~s is started in the node ~p in ~.2fs", [?VERSION, node(), (T2-T1)/1000]), @@ -88,12 +86,6 @@ stop(_State) -> %%% Internal functions %%% -connect_nodes() -> - Nodes = ejabberd_config:get_option(cluster_nodes, []), - lists:foreach(fun(Node) -> - net_kernel:connect_node(Node) - end, Nodes). - %% If ejabberd is running on some Windows machine, get nameservers and add to Erlang maybe_add_nameservers() -> case os:type() of @@ -136,10 +128,6 @@ delete_pid_file() -> file:delete(PidFilename) end. -set_settings_from_config() -> - Ticktime = ejabberd_config:get_option(net_ticktime, 60), - net_kernel:set_net_ticktime(Ticktime). - file_queue_init() -> QueueDir = case ejabberd_config:queue_dir() of undefined -> @@ -160,15 +148,6 @@ start_apps() -> ejabberd:start_app(xmpp), ejabberd:start_app(cache_tab). --spec opt_type(net_ticktime) -> fun((pos_integer()) -> pos_integer()); - (cluster_nodes) -> fun(([node()]) -> [node()]); - (atom()) -> atom(). -opt_type(net_ticktime) -> - fun (P) when is_integer(P), P > 0 -> P end; -opt_type(cluster_nodes) -> - fun (Ns) -> true = lists:all(fun is_atom/1, Ns), Ns end; -opt_type(_) -> [cluster_nodes, net_ticktime]. - setup_if_elixir_conf_used() -> case ejabberd_config:is_using_elixir_config() of true -> 'Elixir.Ejabberd.Config.Store':start_link(); diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 7cbc16f9d..159cb4054 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -46,7 +46,7 @@ reject_unauthenticated_packet/2, process_closed/2, process_terminated/2, process_info/2]). %% API --export([get_presence/1, resend_presence/1, resend_presence/2, +-export([get_presence/1, set_presence/2, resend_presence/1, resend_presence/2, open_session/1, call/3, send/2, close/1, close/2, stop/1, reply/2, copy_state/2, set_timeout/2, route/2, host_up/1, host_down/1]). @@ -97,6 +97,10 @@ reply(Ref, Reply) -> get_presence(Ref) -> call(Ref, get_presence, 1000). +-spec set_presence(pid(), presence()) -> ok. +set_presence(Ref, Pres) -> + call(Ref, {set_presence, Pres}, 1000). + -spec resend_presence(pid()) -> ok. resend_presence(Pid) -> resend_presence(Pid, undefined). @@ -525,6 +529,9 @@ handle_call(get_presence, From, #{jid := JID} = State) -> end, reply(From, Pres), State; +handle_call({set_presence, Pres}, From, State) -> + reply(From, ok), + process_self_presence(State, Pres); handle_call(Request, From, #{lserver := LServer} = State) -> ejabberd_hooks:run_fold( c2s_handle_call, LServer, State, [Request, From]). diff --git a/src/ejabberd_cluster.erl b/src/ejabberd_cluster.erl index aeae294b0..d9429a108 100644 --- a/src/ejabberd_cluster.erl +++ b/src/ejabberd_cluster.erl @@ -1,8 +1,6 @@ -%%%---------------------------------------------------------------------- -%%% File : ejabberd_cluster.erl -%%% Author : Christophe Romain <christophe.romain@process-one.net> -%%% Purpose : Ejabberd clustering management -%%% Created : 7 Oct 2015 by Christophe Romain <christophe.romain@process-one.net> +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Created : 5 Jul 2017 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2017 ProcessOne @@ -21,132 +19,188 @@ %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% -%%%---------------------------------------------------------------------- - +%%%------------------------------------------------------------------- -module(ejabberd_cluster). +-behaviour(ejabberd_config). +-behaviour(gen_server). %% API --export([get_nodes/0, call/4, multicall/3, multicall/4, - eval_everywhere/3, eval_everywhere/4]). --export([join/1, leave/1, get_known_nodes/0]). --export([node_id/0, get_node_by_id/1]). +-export([start_link/0, call/4, multicall/3, multicall/4, eval_everywhere/3, + eval_everywhere/4]). +%% Backend dependent API +-export([get_nodes/0, get_known_nodes/0, join/1, leave/1, subscribe/0, + subscribe/1, node_id/0, get_node_by_id/1, send/2, wait_for_sync/1]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). +-export([opt_type/1]). --include("ejabberd.hrl"). -include("logger.hrl"). --spec get_nodes() -> [node()]. +-type dst() :: pid() | atom() | {atom(), node()}. -get_nodes() -> - mnesia:system_info(running_db_nodes). +-callback init() -> ok | {error, any()}. +-callback get_nodes() -> [node()]. +-callback get_known_nodes() -> [node()]. +-callback join(node()) -> ok | {error, any()}. +-callback leave(node()) -> ok | {error, any()}. +-callback node_id() -> binary(). +-callback get_node_by_id(binary()) -> node(). +-callback send({atom(), node()}, term()) -> boolean(). +-callback wait_for_sync(timeout()) -> ok | {error, any()}. +-callback subscribe(dst()) -> ok. --spec get_known_nodes() -> [node()]. +-record(state, {}). -get_known_nodes() -> - lists:usort(mnesia:system_info(db_nodes) - ++ mnesia:system_info(extra_db_nodes)). +%%%=================================================================== +%%% API +%%%=================================================================== +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec call(node(), module(), atom(), [any()]) -> any(). - call(Node, Module, Function, Args) -> - rpc:call(Node, Module, Function, Args, 5000). + rpc:call(Node, Module, Function, Args, rpc_timeout()). -spec multicall(module(), atom(), [any()]) -> {list(), [node()]}. - multicall(Module, Function, Args) -> multicall(get_nodes(), Module, Function, Args). -spec multicall([node()], module(), atom(), list()) -> {list(), [node()]}. - multicall(Nodes, Module, Function, Args) -> - rpc:multicall(Nodes, Module, Function, Args, 5000). + rpc:multicall(Nodes, Module, Function, Args, rpc_timeout()). -spec eval_everywhere(module(), atom(), [any()]) -> ok. - eval_everywhere(Module, Function, Args) -> eval_everywhere(get_nodes(), Module, Function, Args), ok. -spec eval_everywhere([node()], module(), atom(), [any()]) -> ok. - eval_everywhere(Nodes, Module, Function, Args) -> rpc:eval_everywhere(Nodes, Module, Function, Args), ok. --spec join(node()) -> ok | {error, any()}. +%%%=================================================================== +%%% Backend dependent API +%%%=================================================================== +-spec get_nodes() -> [node()]. +get_nodes() -> + Mod = get_mod(), + Mod:get_nodes(). + +-spec get_known_nodes() -> [node()]. +get_known_nodes() -> + Mod = get_mod(), + Mod:get_known_nodes(). +-spec join(node()) -> ok | {error, any()}. join(Node) -> - case {node(), net_adm:ping(Node)} of - {Node, _} -> - {error, {not_master, Node}}; - {_, pong} -> - application:stop(ejabberd), - application:stop(mnesia), - mnesia:delete_schema([node()]), - application:start(mnesia), - mnesia:change_config(extra_db_nodes, [Node]), - mnesia:change_table_copy_type(schema, node(), disc_copies), - spawn(fun() -> - lists:foreach(fun(Table) -> - Type = call(Node, mnesia, table_info, [Table, storage_type]), - mnesia:add_table_copy(Table, node(), Type) - end, mnesia:system_info(tables)--[schema]) - end), - application:start(ejabberd); - _ -> - {error, {no_ping, Node}} - end. + Mod = get_mod(), + Mod:join(Node). -spec leave(node()) -> ok | {error, any()}. - leave(Node) -> - case {node(), net_adm:ping(Node)} of - {Node, _} -> - Cluster = get_nodes()--[Node], - leave(Cluster, Node); - {_, pong} -> - rpc:call(Node, ?MODULE, leave, [Node], 10000); - {_, pang} -> - case mnesia:del_table_copy(schema, Node) of - {atomic, ok} -> ok; - {aborted, Reason} -> {error, Reason} - end - end. -leave([], Node) -> - {error, {no_cluster, Node}}; -leave([Master|_], Node) -> - application:stop(ejabberd), - application:stop(mnesia), - call(Master, mnesia, del_table_copy, [schema, Node]), - spawn(fun() -> - mnesia:delete_schema([node()]), - erlang:halt(0) - end), - ok. + Mod = get_mod(), + Mod:leave(Node). -spec node_id() -> binary(). node_id() -> - integer_to_binary(erlang:phash2(node())). + Mod = get_mod(), + Mod:node_id(). -spec get_node_by_id(binary()) -> node(). -get_node_by_id(Hash) -> - try binary_to_integer(Hash) of - I -> match_node_id(I) - catch _:_ -> - node() +get_node_by_id(ID) -> + Mod = get_mod(), + Mod:get_node_by_id(ID). + +-spec send(dst(), term()) -> boolean(). +send(Dst, Msg) -> + IsLocal = case Dst of + {_, Node} -> Node == node(); + Pid when is_pid(Pid) -> node(Pid) == node(); + Name when is_atom(Name) -> true; + _ -> false + end, + if IsLocal -> + erlang:send(Dst, Msg), + true; + true -> + Mod = get_mod(), + Mod:send(Dst, Msg) end. +-spec wait_for_sync(timeout()) -> ok | {error, any()}. +wait_for_sync(Timeout) -> + Mod = get_mod(), + Mod:wait_for_sync(Timeout). + +-spec subscribe() -> ok. +subscribe() -> + subscribe(self()). + +-spec subscribe(dst()) -> ok. +subscribe(Proc) -> + Mod = get_mod(), + Mod:subscribe(Proc). + +%%%=================================================================== +%%% gen_server API +%%%=================================================================== +init([]) -> + Ticktime = ejabberd_config:get_option(net_ticktime, 60), + Nodes = ejabberd_config:get_option(cluster_nodes, []), + net_kernel:set_net_ticktime(Ticktime), + lists:foreach(fun(Node) -> + net_kernel:connect_node(Node) + end, Nodes), + Mod = get_mod(), + case Mod:init() of + ok -> + Mod:subscribe(?MODULE), + {ok, #state{}}; + {error, Reason} -> + {stop, Reason} + end. + +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({node_up, Node}, State) -> + ?INFO_MSG("Node ~s has joined", [Node]), + {noreply, State}; +handle_info({node_down, Node}, State) -> + ?INFO_MSG("Node ~s has left", [Node]), + {noreply, State}; +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + %%%=================================================================== %%% Internal functions %%%=================================================================== --spec match_node_id(integer()) -> node(). -match_node_id(I) -> - match_node_id(I, get_nodes()). - --spec match_node_id(integer(), [node()]) -> node(). -match_node_id(I, [Node|Nodes]) -> - case erlang:phash2(Node) of - I -> Node; - _ -> match_node_id(I, Nodes) - end; -match_node_id(_I, []) -> - node(). +get_mod() -> + Backend = ejabberd_config:get_option(cluster_backend, mnesia), + list_to_atom("ejabberd_cluster_" ++ atom_to_list(Backend)). + +rpc_timeout() -> + timer:seconds(ejabberd_config:get_option(rpc_timeout, 5)). + +opt_type(net_ticktime) -> + fun (P) when is_integer(P), P > 0 -> P end; +opt_type(cluster_nodes) -> + fun (Ns) -> true = lists:all(fun is_atom/1, Ns), Ns end; +opt_type(rpc_timeout) -> + fun (T) when is_integer(T), T > 0 -> T end; +opt_type(cluster_backend) -> + fun (T) -> ejabberd_config:v_db(?MODULE, T) end; +opt_type(_) -> + [rpc_timeout, cluster_backend, cluster_nodes, net_ticktime]. diff --git a/src/ejabberd_cluster_mnesia.erl b/src/ejabberd_cluster_mnesia.erl new file mode 100644 index 000000000..67fc60fde --- /dev/null +++ b/src/ejabberd_cluster_mnesia.erl @@ -0,0 +1,144 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_cluster_mnesia.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : Ejabberd clustering management via Mnesia +%%% Created : 7 Oct 2015 by Christophe Romain <christophe.romain@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2017 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. +%%% +%%%---------------------------------------------------------------------- + +-module(ejabberd_cluster_mnesia). +-behaviour(ejabberd_cluster). + +%% API +-export([init/0, get_nodes/0, join/1, leave/1, + get_known_nodes/0, node_id/0, get_node_by_id/1, + send/2, wait_for_sync/1, subscribe/1]). + +-include("ejabberd.hrl"). +-include("logger.hrl"). + +-spec init() -> ok. +init() -> + ok. + +-spec get_nodes() -> [node()]. + +get_nodes() -> + mnesia:system_info(running_db_nodes). + +-spec get_known_nodes() -> [node()]. + +get_known_nodes() -> + lists:usort(mnesia:system_info(db_nodes) + ++ mnesia:system_info(extra_db_nodes)). + +-spec join(node()) -> ok | {error, any()}. + +join(Node) -> + case {node(), net_adm:ping(Node)} of + {Node, _} -> + {error, {not_master, Node}}; + {_, pong} -> + application:stop(ejabberd), + application:stop(mnesia), + mnesia:delete_schema([node()]), + application:start(mnesia), + mnesia:change_config(extra_db_nodes, [Node]), + mnesia:change_table_copy_type(schema, node(), disc_copies), + spawn(fun() -> + lists:foreach(fun(Table) -> + Type = ejabberd_cluster:call( + Node, mnesia, table_info, [Table, storage_type]), + mnesia:add_table_copy(Table, node(), Type) + end, mnesia:system_info(tables)--[schema]) + end), + application:start(ejabberd); + _ -> + {error, {no_ping, Node}} + end. + +-spec leave(node()) -> ok | {error, any()}. + +leave(Node) -> + case {node(), net_adm:ping(Node)} of + {Node, _} -> + Cluster = get_nodes()--[Node], + leave(Cluster, Node); + {_, pong} -> + rpc:call(Node, ?MODULE, leave, [Node], 10000); + {_, pang} -> + case mnesia:del_table_copy(schema, Node) of + {atomic, ok} -> ok; + {aborted, Reason} -> {error, Reason} + end + end. +leave([], Node) -> + {error, {no_cluster, Node}}; +leave([Master|_], Node) -> + application:stop(ejabberd), + application:stop(mnesia), + ejabberd_cluster:call(Master, mnesia, del_table_copy, [schema, Node]), + spawn(fun() -> + mnesia:delete_schema([node()]), + erlang:halt(0) + end), + ok. + +-spec node_id() -> binary(). +node_id() -> + integer_to_binary(erlang:phash2(node())). + +-spec get_node_by_id(binary()) -> node(). +get_node_by_id(Hash) -> + try binary_to_integer(Hash) of + I -> match_node_id(I) + catch _:_ -> + node() + end. + +-spec send({atom(), node()}, term()) -> boolean(). +send(Dst, Msg) -> + erlang:send(Dst, Msg). + +-spec wait_for_sync(timeout()) -> ok. +wait_for_sync(Timeout) -> + ?INFO_MSG("Waiting for Mnesia synchronization to complete", []), + mnesia:wait_for_tables(mnesia:system_info(local_tables), Timeout), + ok. + +-spec subscribe(_) -> ok. +subscribe(_) -> + ok. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec match_node_id(integer()) -> node(). +match_node_id(I) -> + match_node_id(I, get_nodes()). + +-spec match_node_id(integer(), [node()]) -> node(). +match_node_id(I, [Node|Nodes]) -> + case erlang:phash2(Node) of + I -> Node; + _ -> match_node_id(I, Nodes) + end; +match_node_id(_I, []) -> + node(). diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index eccb0d621..265907299 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -221,7 +221,6 @@ get_command_format/1, get_command_format/2, get_command_format/3, - get_command_policy_and_scope/1, get_command_definition/1, get_command_definition/2, get_tags_commands/0, @@ -230,11 +229,6 @@ register_commands/1, unregister_commands/1, expose_commands/1, - execute_command/2, - execute_command/3, - execute_command/4, - execute_command/5, - execute_command/6, opt_type/1, get_commands_spec/0, get_commands_definition/0, @@ -361,6 +355,8 @@ expose_commands(Commands) -> Commands), case ejabberd_config:add_option(commands, [{add_commands, Names}]) of + ok -> + ok; {aborted, Reason} -> {error, Reason}; {atomic, Result} -> @@ -427,17 +423,6 @@ get_command_format(Name, Auth, Version) -> {Args, Result} end. --spec get_command_policy_and_scope(atom()) -> {ok, open|user|admin|restricted, [oauth_scope()]} | {error, command_not_found}. - -%% @doc return command policy. -get_command_policy_and_scope(Name) -> - case get_command_definition(Name) of - #ejabberd_commands{policy = Policy} = Cmd -> - {ok, Policy, cmd_scope(Cmd)}; - command_not_found -> - {error, command_not_found} - end. - %% The oauth scopes for a command are the command name itself, %% also might include either 'ejabberd:user' or 'ejabberd:admin' cmd_scope(#ejabberd_commands{policy = Policy, name = Name}) -> @@ -503,129 +488,6 @@ execute_command2(Name, Arguments, CallerInfo, Version) -> throw({error, access_rules_unauthorized}) end. -%% @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 | access_rules_unauthorized -execute_command(Name, Arguments) -> - execute_command(Name, Arguments, ?DEFAULT_VERSION). - --spec execute_command(atom(), - [any()], - integer() | - {binary(), binary(), binary(), boolean()} | - noauth | admin - ) -> any(). - -%% @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 -%% Arguments = [any()] -%% -%% @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 | access_rules_unauthorized -execute_command(AccessCommands1, Auth1, Name, Arguments, Version) -> - execute_command(AccessCommands1, Auth1, Name, Arguments, Version, #{}). - -execute_command(AccessCommands1, Auth1, Name, Arguments, Version, CallerInfo) -> - Auth = case is_admin(Name, Auth1, CallerInfo) of - true -> admin; - false -> Auth1 - end, - TokenJID = oauth_token_user(Auth1), - Command = get_command_definition(Name, Version), - AccessCommands = get_all_access_commands(AccessCommands1), - - case check_access_commands(AccessCommands, Auth, Name, Command, Arguments, CallerInfo) of - ok -> execute_check_policy(Auth, TokenJID, Command, Arguments) - end. - - -execute_check_policy( - _Auth, _JID, #ejabberd_commands{policy = open} = Command, Arguments) -> - do_execute_command(Command, Arguments); -execute_check_policy( - noauth, _JID, Command, Arguments) -> - do_execute_command(Command, Arguments); -execute_check_policy( - _Auth, _JID, #ejabberd_commands{policy = restricted} = Command, Arguments) -> - do_execute_command(Command, Arguments); -execute_check_policy( - _Auth, JID, #ejabberd_commands{policy = admin} = Command, Arguments) -> - execute_check_access(JID, Command, Arguments); -execute_check_policy( - admin, JID, #ejabberd_commands{policy = user} = Command, Arguments) -> - execute_check_access(JID, Command, Arguments); -execute_check_policy( - {User, Server, _, _}, JID, #ejabberd_commands{policy = user} = Command, Arguments) -> - execute_check_access(JID, Command, [User, Server | Arguments]). - -execute_check_access(_FromJID, #ejabberd_commands{access = []} = Command, Arguments) -> - do_execute_command(Command, Arguments); -execute_check_access(undefined, _Command, _Arguments) -> - throw({error, access_rules_unauthorized}); -execute_check_access(FromJID, #ejabberd_commands{access = AccessRefs} = Command, Arguments) -> - %% TODO Review: Do we have smarter / better way to check rule on other Host than global ? - Host = global, - Rules = lists:map( - fun({Mod, AccessName, Default}) -> - gen_mod:get_module_opt(Host, Mod, AccessName, Default); - (Default) -> - Default - end, AccessRefs), - case acl:any_rules_allowed(Host, Rules, FromJID) of - true -> - do_execute_command(Command, Arguments); - false -> - throw({error, access_rules_unauthorized}) - end. do_execute_command(Command, Arguments) -> Module = Command#ejabberd_commands.module, @@ -672,58 +534,6 @@ get_tags_commands(Version) -> %% Access verification %% ----------------------------- -%% @spec (AccessCommands, Auth, Method, Command, Arguments) -> ok -%% where -%% AccessCommands = [ {Access, CommandNames, Arguments} ] -%% Auth = {User::string(), Server::string(), Password::string()} | noauth -%% Method = atom() -%% Arguments = [any()] -%% @doc Check access is allowed to that command. -%% At least one AccessCommand must be satisfied. -%% It may throw {error, Error} where: -%% Error = account_unprivileged | invalid_account_data -check_access_commands([], _Auth, _Method, _Command, _Arguments, _CallerInfo) -> - ok; -check_access_commands(AccessCommands, Auth, Method, Command1, Arguments, CallerInfo) -> - Command = - case {Command1#ejabberd_commands.policy, Auth} of - {user, {_, _, _, _}} -> - Command1; - {user, _} -> - Command1#ejabberd_commands{ - args = [{user, binary}, {server, binary} | - Command1#ejabberd_commands.args]}; - _ -> - Command1 - end, - AccessCommandsAllowed = - lists:filter( - fun({Access, Commands, ArgumentRestrictions}) -> - case check_access(Command, Access, Auth, CallerInfo) of - true -> - check_access_command(Commands, Command, - ArgumentRestrictions, - Method, Arguments); - false -> - false - end; - ({Access, Commands}) -> - ArgumentRestrictions = [], - case check_access(Command, Access, Auth, CallerInfo) of - true -> - check_access_command(Commands, Command, - ArgumentRestrictions, - Method, Arguments); - false -> - false - end - end, - AccessCommands), - case AccessCommandsAllowed of - [] -> throw({error, account_unprivileged}); - L when is_list(L) -> ok - end. - -spec check_auth(ejabberd_commands(), noauth) -> noauth_provided; (ejabberd_commands(), {binary(), binary(), binary(), boolean()}) -> @@ -746,80 +556,6 @@ check_auth(_Command, {User, Server, Password, _}) when is_binary(Password) -> _ -> throw({error, invalid_account_data}) end. -check_access(Command, ?POLICY_ACCESS, _, _) - when Command#ejabberd_commands.policy == open -> - true; -check_access(_Command, _Access, admin, _) -> - true; -check_access(_Command, _Access, {_User, _Server, _, true}, _) -> - false; -check_access(Command, Access, Auth, CallerInfo) - when Access =/= ?POLICY_ACCESS; - Command#ejabberd_commands.policy == open; - Command#ejabberd_commands.policy == user -> - case check_auth(Command, Auth) of - {ok, User, Server} -> - check_access2(Access, CallerInfo#{usr => jid:split(jid:make(User, Server))}, Server); - no_auth_provided -> - case Command#ejabberd_commands.policy of - user -> - false; - _ -> - check_access2(Access, CallerInfo, global) - end; - _ -> - false - end; -check_access(_Command, _Access, _Auth, _CallerInfo) -> - false. - -check_access2(?POLICY_ACCESS, _CallerInfo, _Server) -> - true; -check_access2(Access, AccessInfo, Server) -> - %% Check this user has access permission - case acl:access_matches(Access, AccessInfo, Server) of - allow -> true; - deny -> false - end. - -check_access_command(Commands, Command, ArgumentRestrictions, - Method, Arguments) -> - case Commands==all orelse lists:member(Method, Commands) of - true -> check_access_arguments(Command, ArgumentRestrictions, - Arguments); - false -> false - end. - -check_access_arguments(Command, ArgumentRestrictions, Arguments) -> - ArgumentsTagged = tag_arguments(Command#ejabberd_commands.args, Arguments), - lists:all( - fun({ArgName, ArgAllowedValue}) -> - %% If the call uses the argument, check the value is acceptable - case lists:keysearch(ArgName, 1, ArgumentsTagged) of - {value, {ArgName, ArgValue}} -> ArgValue == ArgAllowedValue; - false -> true - end - end, ArgumentRestrictions). - -tag_arguments(ArgsDefs, Args) -> - lists:zipwith( - fun({ArgName, _ArgType}, ArgValue) -> - {ArgName, ArgValue} - end, - ArgsDefs, - Args). - - -%% Get commands for all version -get_all_access_commands(AccessCommands) -> - get_access_commands(AccessCommands, ?DEFAULT_VERSION). - -get_access_commands(undefined, Version) -> - Cmds = get_exposed_commands(Version), - [{?POLICY_ACCESS, Cmds, []}]; -get_access_commands(AccessCommands, _Version) -> - AccessCommands. - get_exposed_commands() -> get_exposed_commands(?DEFAULT_VERSION). get_exposed_commands(Version) -> @@ -854,13 +590,6 @@ expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds) when is_list(L [Command|Acc] end, [], L). -oauth_token_user(noauth) -> - undefined; -oauth_token_user(admin) -> - undefined; -oauth_token_user({User, Server, _, _}) -> - jid:make(User, Server). - is_admin(_Name, admin, _Extra) -> true; is_admin(_Name, {_User, _Server, _, false}, _Extra) -> diff --git a/src/ejabberd_commands_doc.erl b/src/ejabberd_commands_doc.erl index c3b7b4724..f90b70bce 100644 --- a/src/ejabberd_commands_doc.erl +++ b/src/ejabberd_commands_doc.erl @@ -87,9 +87,6 @@ md_tag(strong, V) -> md_tag(_, V) -> V. -unbinarize(binary) -> string; -unbinarize(Other) -> Other. - perl_gen({Name, integer}, Int, _Indent, HTMLOutput) -> [?ARG(Name), ?OP_L(" => "), ?NUM(Int)]; perl_gen({Name, string}, Str, _Indent, HTMLOutput) -> @@ -252,7 +249,7 @@ json_call(Name, ArgsDesc, Values, ResultDesc, Result, HTMLOutput) -> {200, json_gen(ResultDesc, Result, Indent, HTMLOutput)}; {{Name0, _}, _} -> {200, [Indent, ?OP_L("{"), ?STR_A(Name0), ?OP_L(": "), - json_gen(ResultDesc, Result, Indent, HTMLOutput), Indent, ?OP_L("}")]} + json_gen(ResultDesc, Result, Indent, HTMLOutput), ?OP_L("}")]} end, CodeStr = case Code of 200 -> <<" 200 OK">>; @@ -340,48 +337,62 @@ gen_calls(#ejabberd_commands{args_example=Values, args=ArgsDesc, end end. +format_type({list, {_, {tuple, Els}}}) -> + io_lib:format("[~s]", [format_type({tuple, Els})]); +format_type({list, El}) -> + io_lib:format("[~s]", [format_type(El)]); +format_type({tuple, Els}) -> + Args = [format_type(El) || El <- Els], + io_lib:format("{~s}", [string:join(Args, ", ")]); +format_type({Name, Type}) -> + io_lib:format("~s::~s", [Name, format_type(Type)]); +format_type(binary) -> + "string"; +format_type(atom) -> + "string"; +format_type(Type) -> + io_lib:format("~p", [Type]). + gen_param(Name, Type, undefined, HTMLOutput) -> - [?TAG(li, [?TAG_R(strong, atom_to_list(Name)), <<" :: ">>, - ?RAW(io_lib:format("~p", [unbinarize(Type)]))])]; + [?TAG(li, [?TAG_R(strong, atom_to_list(Name)), <<" :: ">>, ?RAW(format_type(Type))])]; gen_param(Name, Type, Desc, HTMLOutput) -> - [?TAG(dt, [?TAG_R(strong, atom_to_list(Name)), <<" :: ">>, - ?RAW(io_lib:format("~p", [unbinarize(Type)]))]), + [?TAG(dt, [?TAG_R(strong, atom_to_list(Name)), <<" :: ">>, ?RAW(format_type(Type))]), ?TAG(dd, ?RAW(Desc))]. gen_doc(#ejabberd_commands{name=Name, tags=_Tags, desc=Desc, longdesc=LongDesc, args=Args, args_desc=ArgsDesc, result=Result, result_desc=ResultDesc}=Cmd, HTMLOutput, Langs) -> - LDesc = case LongDesc of - "" -> Desc; - _ -> LongDesc - end, - ArgsText = case ArgsDesc of - none -> - [?TAG(ul, "args-list", [gen_param(AName, Type, undefined, HTMLOutput) - || {AName, Type} <- Args])]; - _ -> - [?TAG(dl, "args-list", [gen_param(AName, Type, ADesc, HTMLOutput) - || {{AName, Type}, ADesc} <- lists:zip(Args, ArgsDesc)])] - end, - ResultText = case Result of - {res,rescode} -> - [?TAG(dl, [gen_param(res, integer, - "Status code (0 on success, 1 otherwise)", - HTMLOutput)])]; - {res,restuple} -> - [?TAG(dl, [gen_param(res, string, - "Raw result string", - HTMLOutput)])]; - {RName, Type} -> - case ResultDesc of - none -> - [?TAG(ul, [gen_param(RName, Type, undefined, HTMLOutput)])]; - _ -> - [?TAG(dl, [gen_param(RName, Type, ResultDesc, HTMLOutput)])] - end - end, - try + LDesc = case LongDesc of + "" -> Desc; + _ -> LongDesc + end, + ArgsText = case ArgsDesc of + none -> + [?TAG(ul, "args-list", [gen_param(AName, Type, undefined, HTMLOutput) + || {AName, Type} <- Args])]; + _ -> + [?TAG(dl, "args-list", [gen_param(AName, Type, ADesc, HTMLOutput) + || {{AName, Type}, ADesc} <- lists:zip(Args, ArgsDesc)])] + end, + ResultText = case Result of + {res,rescode} -> + [?TAG(dl, [gen_param(res, integer, + "Status code (0 on success, 1 otherwise)", + HTMLOutput)])]; + {res,restuple} -> + [?TAG(dl, [gen_param(res, string, + "Raw result string", + HTMLOutput)])]; + {RName, Type} -> + case ResultDesc of + none -> + [?TAG(ul, [gen_param(RName, Type, undefined, HTMLOutput)])]; + _ -> + [?TAG(dl, [gen_param(RName, Type, ResultDesc, HTMLOutput)])] + end + end, + [?TAG(h1, [?TAG(strong, atom_to_list(Name)), <<" - ">>, ?RAW(Desc)]), ?TAG(p, ?RAW(LDesc)), ?TAG(h2, <<"Arguments:">>), ArgsText, diff --git a/src/ejabberd_http_ws.erl b/src/ejabberd_http_ws.erl index f4a73cc39..2c44d6552 100644 --- a/src/ejabberd_http_ws.erl +++ b/src/ejabberd_http_ws.erl @@ -241,6 +241,7 @@ handle_info(PingPong, StateName, StateData) when PingPong == ping orelse StateData2#state{pong_expected = false}}; handle_info({timeout, Timer, _}, _StateName, #state{timer = Timer} = StateData) -> + ?DEBUG("Closing websocket connection from hitting inactivity timeout", []), {stop, normal, StateData}; handle_info({timeout, Timer, _}, StateName, #state{ping_timer = Timer, ws = {_, WsPid}} = StateData) -> @@ -253,6 +254,7 @@ handle_info({timeout, Timer, _}, StateName, {next_state, StateName, StateData#state{ping_timer = PingTimer, pong_expected = true}}; true -> + ?DEBUG("Closing websocket connection from missing pongs", []), {stop, normal, StateData} end; handle_info(_, StateName, StateData) -> diff --git a/src/ejabberd_listener.erl b/src/ejabberd_listener.erl index 6dd8e706d..e9b4306e5 100644 --- a/src/ejabberd_listener.erl +++ b/src/ejabberd_listener.erl @@ -109,6 +109,7 @@ init_udp(PortIP, Module, Opts, SockOpts, Port, IPS) -> {ok, Socket} -> %% Inform my parent that this port was opened succesfully proc_lib:init_ack({ok, self()}), + application:ensure_started(ejabberd), start_module_sup(Port, Module), ?INFO_MSG("Start accepting UDP connections at ~s for ~p", [format_portip(PortIP), Module]), @@ -134,6 +135,7 @@ init_tcp(PortIP, Module, Opts, SockOpts, Port, IPS) -> ListenSocket = listen_tcp(PortIP, Module, SockOpts, Port, IPS), %% Inform my parent that this port was opened succesfully proc_lib:init_ack({ok, self()}), + application:ensure_started(ejabberd), start_module_sup(Port, Module), ?INFO_MSG("Start accepting TCP connections at ~s for ~p", [format_portip(PortIP), Module]), @@ -302,7 +304,7 @@ accept(ListenSocket, Module, Opts, Interval) -> ejabberd_config:may_hide_data(inet_parse:ntoa(PAddr)), PPort, inet_parse:ntoa(Addr), Port]); _ -> - ok + gen_tcp:close(Socket) end, accept(ListenSocket, Module, Opts, NewInterval); {error, Reason} -> diff --git a/src/ejabberd_logger.erl b/src/ejabberd_logger.erl index 1e48732c8..eee9d3b83 100644 --- a/src/ejabberd_logger.erl +++ b/src/ejabberd_logger.erl @@ -151,6 +151,9 @@ do_start() -> application:set_env(lager, crash_log_size, LogRotateSize), application:set_env(lager, crash_log_count, LogRotateCount), ejabberd:start_app(lager), + lists:foreach(fun(Handler) -> + lager:set_loghwm(Handler, LogRateLimit) + end, gen_event:which_handlers(lager_event)), ok. %% @spec () -> ok diff --git a/src/ejabberd_mnesia.erl b/src/ejabberd_mnesia.erl index 34691545a..16e385011 100644 --- a/src/ejabberd_mnesia.erl +++ b/src/ejabberd_mnesia.erl @@ -68,8 +68,6 @@ init([]) -> _ -> ok end, ejabberd:start_app(mnesia, permanent), - ?DEBUG("Waiting for Mnesia tables synchronization...", []), - mnesia:wait_for_tables(mnesia:system_info(local_tables), infinity), Schema = read_schema_file(), {ok, #state{schema = Schema}}; false -> diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index 026b30680..3e3fc3082 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -50,7 +50,7 @@ config_reloaded/0, opt_type/1]). --export([oauth_issue_token/3, oauth_list_tokens/0, oauth_revoke_token/1, oauth_list_scopes/0]). +-export([oauth_issue_token/3, oauth_list_tokens/0, oauth_revoke_token/1]). -include("xmpp.hrl"). @@ -96,14 +96,6 @@ get_commands_spec() -> policy = restricted, result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}} }, - #ejabberd_commands{name = oauth_list_scopes, tags = [oauth], - desc = "List scopes that can be granted, and commands", - longdesc = "List scopes that can be granted to tokens generated through the command line, together with the commands they allow", - module = ?MODULE, function = oauth_list_scopes, - args = [], - policy = restricted, - result = {scopes, {list, {scope, {tuple, [{scope, string}, {commands, string}]}}}} - }, #ejabberd_commands{name = oauth_revoke_token, tags = [oauth], desc = "Revoke authorization for a token (only Mnesia)", module = ?MODULE, function = oauth_revoke_token, @@ -143,9 +135,6 @@ oauth_revoke_token(Token) -> ok = mnesia:dirty_delete(oauth_token, list_to_binary(Token)), oauth_list_tokens(). -oauth_list_scopes() -> - [ {Scope, string:join([atom_to_list(Cmd) || Cmd <- Cmds], ",")} || {Scope, Cmds} <- dict:to_list(get_cmd_scopes())]. - config_reloaded() -> DBMod = get_db_backend(), case init_cache(DBMod) of @@ -240,17 +229,6 @@ verify_resowner_scope(_, _, _) -> {error, badscope}. -get_cmd_scopes() -> - ScopeMap = lists:foldl(fun(Cmd, Accum) -> - case ejabberd_commands:get_command_policy_and_scope(Cmd) of - {ok, Policy, Scopes} when Policy =/= restricted -> - lists:foldl(fun(Scope, Accum2) -> - dict:append(Scope, Cmd, Accum2) - end, Accum, Scopes); - _ -> Accum - end end, dict:new(), ejabberd_commands:get_exposed_commands()), - ScopeMap. - %% This is callback for oauth tokens generated through the command line. Only open and admin commands are %% made available. %verify_client_scope({client, ejabberd_ctl}, Scope, Ctx) -> @@ -755,7 +733,7 @@ css() -> text-decoration: underline; } - .container > .section { + .container > .section { background: #424A55; } diff --git a/src/ejabberd_router_mnesia.erl b/src/ejabberd_router_mnesia.erl index 76336d0b0..d84f7a609 100644 --- a/src/ejabberd_router_mnesia.erl +++ b/src/ejabberd_router_mnesia.erl @@ -149,7 +149,7 @@ init([]) -> lists:foreach( fun (Pid) -> erlang:monitor(process, Pid) end, mnesia:dirty_select(route, - [{{route, '_', '$1', '_'}, [], ['$1']}])), + [{#route{pid = '$1', _ = '_'}, [], ['$1']}])), {ok, #state{}}. handle_call(_Request, _From, State) -> diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl index 4b74b8c4a..a0e9411cf 100644 --- a/src/ejabberd_s2s.erl +++ b/src/ejabberd_s2s.erl @@ -546,25 +546,23 @@ parent_domains(Domain) -> get_commands_spec() -> [#ejabberd_commands{ - name = incoming_s2s_number, - tags = [stats, s2s], + name = incoming_s2s_number, tags = [stats, s2s], desc = "Number of incoming s2s connections on the node", - policy = admin, - module = ?MODULE, function = incoming_s2s_number, - args = [], result = {s2s_incoming, integer}}, + policy = admin, + module = ?MODULE, function = incoming_s2s_number, + args = [], result = {s2s_incoming, integer}}, #ejabberd_commands{ - name = outgoing_s2s_number, - tags = [stats, s2s], + name = outgoing_s2s_number, tags = [stats, s2s], desc = "Number of outgoing s2s connections on the node", - policy = admin, - module = ?MODULE, function = outgoing_s2s_number, - args = [], result = {s2s_outgoing, integer}}, - #ejabberd_commands{name = stop_all_connections, - tags = [s2s], - desc = "Stop all outgoing and incoming connections", - policy = admin, - module = ?MODULE, function = stop_all_connections, - args = [], result = {res, rescode}}]. + policy = admin, + module = ?MODULE, function = outgoing_s2s_number, + args = [], result = {s2s_outgoing, integer}}, + #ejabberd_commands{ + name = stop_all_connections, tags = [s2s], + desc = "Stop all outgoing and incoming connections", + policy = admin, + module = ?MODULE, function = stop_all_connections, + args = [], result = {res, rescode}}]. %% TODO Move those stats commands to ejabberd stats command ? incoming_s2s_number() -> diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl index 344febb5d..333edffa6 100644 --- a/src/ejabberd_sm.erl +++ b/src/ejabberd_sm.erl @@ -957,30 +957,36 @@ cache_nodes(Mod, LServer) -> %%% ejabberd commands get_commands_spec() -> - [#ejabberd_commands{name = connected_users, - tags = [session], + [#ejabberd_commands{name = connected_users, tags = [session], desc = "List all established sessions", policy = admin, module = ?MODULE, function = connected_users, args = [], + result_desc = "List of users sessions", + result_example = [<<"user1@example.com">>, <<"user2@example.com">>], result = {connected_users, {list, {sessions, string}}}}, - #ejabberd_commands{name = connected_users_number, - tags = [session, stats], + #ejabberd_commands{name = connected_users_number, tags = [session, stats], desc = "Get the number of established sessions", policy = admin, module = ?MODULE, function = connected_users_number, + result_example = 2, args = [], result = {num_sessions, integer}}, - #ejabberd_commands{name = user_resources, - tags = [session], + #ejabberd_commands{name = user_resources, tags = [session], desc = "List user's connected resources", policy = user, module = ?MODULE, function = user_resources, - args = [], + args = [{user, binary}, {host, binary}], + args_desc = ["User name", "Server name"], + args_example = [<<"user1">>, <<"example.com">>], + result_example = [<<"tka1">>, <<"Gajim">>, <<"mobile-app">>], result = {resources, {list, {resource, string}}}}, - #ejabberd_commands{name = kick_user, - tags = [session], + #ejabberd_commands{name = kick_user, tags = [session], desc = "Disconnect user's active sessions", module = ?MODULE, function = kick_user, args = [{user, binary}, {host, binary}], + args_desc = ["User name", "Server name"], + args_example = [<<"user1">>, <<"example.com">>], + result_desc = "Number of resources that were kicked", + result_example = 3, result = {num_resources, integer}}]. -spec connected_users() -> [binary()]. diff --git a/src/ejabberd_sup.erl b/src/ejabberd_sup.erl index 224ed16c1..35527ebd7 100644 --- a/src/ejabberd_sup.erl +++ b/src/ejabberd_sup.erl @@ -41,6 +41,12 @@ init([]) -> brutal_kill, worker, [ejabberd_hooks]}, + Cluster = {ejabberd_cluster, + {ejabberd_cluster, start_link, []}, + permanent, + 5000, + worker, + [ejabberd_cluster]}, SystemMonitor = {ejabberd_system_monitor, {ejabberd_system_monitor, start_link, []}, @@ -152,6 +158,7 @@ init([]) -> permanent, 5000, worker, [ejabberd_pkix]}, {ok, {{one_for_one, 10, 1}, [Hooks, + Cluster, CyrSASL, Translation, AccessPerms, diff --git a/src/ext_mod.erl b/src/ext_mod.erl index 86bd21983..ecdfa04c3 100644 --- a/src/ext_mod.erl +++ b/src/ext_mod.erl @@ -80,16 +80,18 @@ code_change(_OldVsn, State, _Extra) -> get_commands_spec() -> [#ejabberd_commands{name = modules_update_specs, tags = [admin,modules], - desc = "", - longdesc = "", + desc = "Update the module source code from Git", + longdesc = "A connection to Internet is required", module = ?MODULE, function = update, args = [], result = {res, rescode}}, #ejabberd_commands{name = modules_available, tags = [admin,modules], - desc = "", - longdesc = "", + desc = "List the contributed modules available to install", module = ?MODULE, function = available_command, + result_desc = "List of tuples with module name and description", + result_example = [{mod_cron, "Execute scheduled commands"}, + {mod_rest, "ReST frontend"}], args = [], result = {modules, {list, {module, {tuple, @@ -97,9 +99,11 @@ get_commands_spec() -> {summary, string}]}}}}}, #ejabberd_commands{name = modules_installed, tags = [admin,modules], - desc = "", - longdesc = "", + desc = "List the contributed modules already installed", module = ?MODULE, function = installed_command, + result_desc = "List of tuples with module name and description", + result_example = [{mod_cron, "Execute scheduled commands"}, + {mod_rest, "ReST frontend"}], args = [], result = {modules, {list, {module, {tuple, @@ -107,46 +111,64 @@ get_commands_spec() -> {summary, string}]}}}}}, #ejabberd_commands{name = module_install, tags = [admin,modules], - desc = "", - longdesc = "", + desc = "Compile, install and start an available contributed module", module = ?MODULE, function = install, + args_desc = ["Module name"], + args_example = [<<"mod_rest">>], args = [{module, binary}], result = {res, rescode}}, #ejabberd_commands{name = module_uninstall, tags = [admin,modules], - desc = "", - longdesc = "", + desc = "Uninstall a contributed module", module = ?MODULE, function = uninstall, + args_desc = ["Module name"], + args_example = [<<"mod_rest">>], args = [{module, binary}], result = {res, rescode}}, #ejabberd_commands{name = module_upgrade, tags = [admin,modules], - desc = "", - longdesc = "", + desc = "Upgrade the running code of an installed module", + longdesc = "In practice, this uninstalls and installs the module", module = ?MODULE, function = upgrade, + args_desc = ["Module name"], + args_example = [<<"mod_rest">>], args = [{module, binary}], result = {res, rescode}}, #ejabberd_commands{name = module_check, tags = [admin,modules], - desc = "", - longdesc = "", + desc = "Check the contributed module repository compliance", module = ?MODULE, function = check, + args_desc = ["Module name"], + args_example = [<<"mod_rest">>], args = [{module, binary}], result = {res, rescode}} ]. %% -- public modules functions update() -> - add_sources(?REPOS), + Contrib = maps:put(?REPOS, [], maps:new()), + Jungles = lists:foldl(fun({Package, Spec}, Acc) -> + Repo = proplists:get_value(url, Spec, ""), + Mods = maps:get(Repo, Acc, []), + maps:put(Repo, [Package|Mods], Acc) + end, Contrib, modules_spec(sources_dir(), "*/*")), + Repos = maps:fold(fun(Repo, _Mods, Acc) -> + Update = add_sources(Repo), + ?INFO_MSG("Update packages from repo ~s: ~p", [Repo, Update]), + case Update of + ok -> Acc; + Error -> [{repository, Repo, Error}|Acc] + end + end, [], Jungles), Res = lists:foldl(fun({Package, Spec}, Acc) -> - Path = proplists:get_value(url, Spec, ""), - Update = add_sources(Package, Path), + Repo = proplists:get_value(url, Spec, ""), + Update = add_sources(Package, Repo), ?INFO_MSG("Update package ~s: ~p", [Package, Update]), case Update of ok -> Acc; - Error -> [Error|Acc] + Error -> [{Package, Repo, Error}|Acc] end - end, [], modules_spec(sources_dir(), "*")), + end, Repos, modules_spec(sources_dir(), "*")), case Res of [] -> ok; [Error|_] -> Error @@ -538,7 +560,9 @@ compile_result(Results) -> compile_options() -> [verbose, report_errors, report_warnings] ++ [{i, filename:join(app_dir(App), "include")} - || App <- [fast_xml, xmpp, p1_utils, ejabberd]]. + || App <- [fast_xml, xmpp, p1_utils, ejabberd]] + ++ [{i, filename:join(mod_dir(Mod), "include")} + || Mod <- installed()]. app_dir(App) -> case code:lib_dir(App) of @@ -553,6 +577,10 @@ app_dir(App) -> Dir end. +mod_dir({Package, Spec}) -> + Default = filename:join(modules_dir(), Package), + proplists:get_value(path, Spec, Default). + compile_erlang_file(Dest, File) -> compile_erlang_file(Dest, File, compile_options()). diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl index e5d2892ff..013c342d8 100644 --- a/src/mod_admin_extra.erl +++ b/src/mod_admin_extra.erl @@ -303,6 +303,9 @@ get_commands_spec() -> desc = "List of users logged in host with their statuses", module = ?MODULE, function = status_list, args = [{host, binary}, {status, binary}], + args_example = [<<"myserver.com">>, <<"dnd">>], + args_desc = ["Server name", "Status type to check"], + result_example = [{<<"peter">>,<<"myserver.com">>,<<"tka">>,6,<<"Busy">>}], result = {users, {list, {userstatus, {tuple, [ {user, string}, @@ -316,6 +319,9 @@ get_commands_spec() -> desc = "List of logged users with this status", module = ?MODULE, function = status_list, args = [{status, binary}], + args_example = [<<"dnd">>], + args_desc = ["Status type to check"], + result_example = [{<<"peter">>,<<"myserver.com">>,<<"tka">>,6,<<"Busy">>}], result = {users, {list, {userstatus, {tuple, [ {user, string}, @@ -330,6 +336,8 @@ get_commands_spec() -> desc = "List all established sessions and their information", module = ?MODULE, function = connected_users_info, args = [], + result_example = [{"user1@myserver.com/tka", "c2s", "127.0.0.1", + 40092, 8, "ejabberd@localhost", 28}], result = {connected_users_info, {list, {sessions, {tuple, @@ -346,6 +354,9 @@ get_commands_spec() -> tags = [session], desc = "Get the list of established sessions in a vhost", module = ?MODULE, function = connected_users_vhost, + args_example = [<<"myexample.com">>], + args_desc = ["Server name"], + result_example = [<<"user1@myserver.com/tka">>, <<"user2@localhost/tka">>], args = [{host, binary}], result = {connected_users_vhost, {list, {sessions, string}}}}, #ejabberd_commands{name = user_sessions_info, @@ -353,6 +364,10 @@ get_commands_spec() -> desc = "Get information about all sessions of a user", module = ?MODULE, function = user_sessions_info, args = [{user, binary}, {host, binary}], + args_example = [<<"peter">>, <<"myserver.com">>], + args_desc = ["User name", "Server name"], + result_example = [{"c2s", "127.0.0.1", 42656,8, "ejabberd@localhost", + 231, <<"dnd">>, <<"tka">>, <<>>}], result = {sessions_info, {list, {session, {tuple, @@ -385,6 +400,9 @@ get_commands_spec() -> "defined by the user client.", module = ?MODULE, function = get_presence, args = [{user, binary}, {server, binary}], + args_example = [<<"peter">>, <<"myexample.com">>], + args_desc = ["User name", "Server name"], + result_example = {<<"user1@myserver.com/tka">>, <<"dnd">>, <<"Busy">>}, result = {presence, {tuple, @@ -398,24 +416,40 @@ get_commands_spec() -> {resource, binary}, {type, binary}, {show, binary}, {status, binary}, {priority, binary}], + args_example = [<<"user1">>,<<"myserver.com">>,<<"tka1">>, + <<"available">>,<<"away">>,<<"BB">>, <<"7">>], + args_desc = ["User name", "Server name", "Resource", + "Type: available, error, probe...", + "Show: away, chat, dnd, xa.", "Status text", + "Priority, provide this value as an integer"], result = {res, rescode}}, #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_example = [<<"user1">>,<<"myserver.com">>,<<"User 1">>], + args_desc = ["User name", "Server name", "Nickname"], result = {res, rescode}}, #ejabberd_commands{name = get_vcard, tags = [vcard], desc = "Get content from a vCard field", longdesc = Vcard1FieldsString ++ "\n" ++ Vcard2FieldsString ++ "\n\n" ++ VcardXEP, module = ?MODULE, function = get_vcard, args = [{user, binary}, {host, binary}, {name, binary}], + args_example = [<<"user1">>,<<"myserver.com">>,<<"NICKNAME">>], + args_desc = ["User name", "Server name", "Field name"], + result_example = "User 1", + result_desc = "Field content", result = {content, string}}, #ejabberd_commands{name = get_vcard2, tags = [vcard], - desc = "Get content from a vCard field", + desc = "Get content from a vCard subfield", longdesc = Vcard2FieldsString ++ "\n\n" ++ Vcard1FieldsString ++ "\n" ++ VcardXEP, module = ?MODULE, function = get_vcard, args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}], + args_example = [<<"user1">>,<<"myserver.com">>,<<"N">>, <<"FAMILY">>], + args_desc = ["User name", "Server name", "Field name", "Subfield name"], + result_example = "Schubert", + result_desc = "Field content", result = {content, string}}, #ejabberd_commands{name = get_vcard2_multi, tags = [vcard], desc = "Get multiple contents from a vCard field", @@ -429,12 +463,16 @@ get_commands_spec() -> longdesc = Vcard1FieldsString ++ "\n" ++ Vcard2FieldsString ++ "\n\n" ++ VcardXEP, module = ?MODULE, function = set_vcard, args = [{user, binary}, {host, binary}, {name, binary}, {content, binary}], + args_example = [<<"user1">>,<<"myserver.com">>, <<"URL">>, <<"www.example.com">>], + args_desc = ["User name", "Server name", "Field name", "Value"], result = {res, rescode}}, #ejabberd_commands{name = set_vcard2, tags = [vcard], desc = "Set content in a vCard subfield", longdesc = Vcard2FieldsString ++ "\n\n" ++ Vcard1FieldsString ++ "\n" ++ VcardXEP, module = ?MODULE, function = set_vcard, args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}, {content, binary}], + args_example = [<<"user1">>,<<"myserver.com">>,<<"TEL">>, <<"NUMBER">>, <<"123456">>], + args_desc = ["User name", "Server name", "Field name", "Subfield name", "Value"], result = {res, rescode}}, #ejabberd_commands{name = set_vcard2_multi, tags = [vcard], desc = "Set multiple contents in a vCard subfield", @@ -451,6 +489,10 @@ get_commands_spec() -> {user, binary}, {server, binary}, {nick, binary}, {group, binary}, {subs, binary}], + args_example = [<<"user1">>,<<"myserver.com">>,<<"user2">>, <<"myserver.com">>, + <<"User 2">>, <<"Friends">>, <<"both">>], + args_desc = ["User name", "Server name", "Contact user name", "Contact server name", + "Nickname", "Group", "Subscription"], result = {res, rescode}}, %%{"", "subs= none, from, to or both"}, %%{"", "example: add-roster peter localhost mike server.com MiKe Employees both"}, @@ -460,6 +502,8 @@ get_commands_spec() -> module = ?MODULE, function = delete_rosteritem, args = [{localuser, binary}, {localserver, binary}, {user, binary}, {server, binary}], + args_example = [<<"user1">>,<<"myserver.com">>,<<"user2">>, <<"myserver.com">>], + args_desc = ["User name", "Server name", "Contact user name", "Contact server name"], result = {res, rescode}}, #ejabberd_commands{name = process_rosteritems, tags = [roster], desc = "List/delete rosteritems that match filter (only Mnesia)", @@ -514,8 +558,14 @@ get_commands_spec() -> ]}}}}}, #ejabberd_commands{name = push_roster, tags = [roster], desc = "Push template roster from file to a user", + longdesc = "The text file must contain an erlang term: a list " + "of tuples with username, servername, group and nick. Example:\n" + "[{\"user1\", \"localhost\", \"Workers\", \"User 1\"},\n" + " {\"user2\", \"localhost\", \"Workers\", \"User 2\"}].", module = ?MODULE, function = push_roster, args = [{file, binary}, {user, binary}, {host, binary}], + args_example = [<<"/home/ejabberd/roster.txt">>, <<"user1">>, <<"localhost">>], + args_desc = ["File path", "User name", "Server name"], result = {res, rescode}}, #ejabberd_commands{name = push_roster_all, tags = [roster], desc = "Push template roster from file to all those users", @@ -525,11 +575,15 @@ get_commands_spec() -> " {\"user2\", \"localhost\", \"Workers\", \"User 2\"}].", module = ?MODULE, function = push_roster_all, args = [{file, binary}], + args_example = [<<"/home/ejabberd/roster.txt">>], + args_desc = ["File path"], result = {res, rescode}}, #ejabberd_commands{name = push_alltoall, tags = [roster], desc = "Add all the users to all the users of Host in Group", module = ?MODULE, function = push_alltoall, args = [{host, binary}, {group, binary}], + args_example = [<<"myserver.com">>,<<"Everybody">>], + args_desc = ["Server name", "Group name"], result = {res, rescode}}, #ejabberd_commands{name = get_last, tags = [last], @@ -538,6 +592,10 @@ get_commands_spec() -> "2017-02-23T22:25:28.063062Z ONLINE", module = ?MODULE, function = get_last, args = [{user, binary}, {host, binary}], + args_example = [<<"user1">>,<<"myserver.com">>], + args_desc = ["User name", "Server name"], + result_example = {<<"2017-06-30T14:32:16.060684Z">>, "ONLINE"}, + result_desc = "Last activity timestamp and status", result = {last_activity, {tuple, [{timestamp, string}, {status, string} @@ -548,17 +606,24 @@ get_commands_spec() -> "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}], + args_example = [<<"user1">>,<<"myserver.com">>, 1500045311, <<"GoSleeping">>], + args_desc = ["User name", "Server name", "Number of seconds since epoch", "Status message"], result = {res, rescode}}, #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_example = [<<"user1">>,<<"myserver.com">>,<<"storage">>, <<"storage:rosternotes">>], + args_desc = ["User name", "Server name", "Element name", "Namespace"], 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_example = [<<"user1">>,<<"myserver.com">>, + <<"<query xmlns='jabber:iq:private'> <storage xmlns='storage:rosternotes'/></query>">>], + args_desc = ["User name", "Server name", "XML storage element"], result = {res, rescode}}, #ejabberd_commands{name = srg_create, tags = [shared_roster_group], @@ -568,41 +633,63 @@ get_commands_spec() -> "put \\ \" around the argument and\nseparate the " "identifiers with \\ \\ n\n" "For example:\n" - " ejabberdctl srg_create group3 localhost " + " ejabberdctl srg_create group3 myserver.com " "name desc \\\"group1\\\\ngroup2\\\"", module = ?MODULE, function = srg_create, args = [{group, binary}, {host, binary}, {name, binary}, {description, binary}, {display, binary}], + args_example = [<<"group3">>, <<"myserver.com">>, <<"Group3">>, + <<"Third group">>, <<"group1\\\\ngroup2">>], + args_desc = ["Group identifier", "Group server name", "Group name", + "Group description", "Groups to display"], result = {res, rescode}}, #ejabberd_commands{name = srg_delete, tags = [shared_roster_group], desc = "Delete a Shared Roster Group", module = ?MODULE, function = srg_delete, args = [{group, binary}, {host, binary}], + args_example = [<<"group3">>, <<"myserver.com">>], + args_desc = ["Group identifier", "Group server name"], result = {res, rescode}}, #ejabberd_commands{name = srg_list, tags = [shared_roster_group], desc = "List the Shared Roster Groups in Host", module = ?MODULE, function = srg_list, args = [{host, binary}], + args_example = [<<"myserver.com">>], + args_desc = ["Server name"], + result_example = [<<"group1">>, <<"group2">>], + result_desc = "List of group identifiers", result = {groups, {list, {id, string}}}}, #ejabberd_commands{name = srg_get_info, tags = [shared_roster_group], desc = "Get info of a Shared Roster Group", module = ?MODULE, function = srg_get_info, args = [{group, binary}, {host, binary}], + args_example = [<<"group3">>, <<"myserver.com">>], + args_desc = ["Group identifier", "Group server name"], + result_example = [{<<"name">>, "Group 3"}, {<<"displayed_groups">>, "group1"}], + result_desc = "List of group informations, as key and value", 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, args = [{group, binary}, {host, binary}], + args_example = [<<"group3">>, <<"myserver.com">>], + args_desc = ["Group identifier", "Group server name"], + result_example = [<<"user1@localhost">>, <<"user2@localhost">>], + result_desc = "List of group identifiers", result = {members, {list, {member, string}}}}, #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_example = [<<"user1">>, <<"myserver.com">>, <<"group3">>, <<"myserver.com">>], + args_desc = ["Username", "User server name", "Group identifier", "Group server name"], result = {res, rescode}}, #ejabberd_commands{name = srg_user_del, tags = [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_example = [<<"user1">>, <<"myserver.com">>, <<"group3">>, <<"myserver.com">>], + args_desc = ["Username", "User server name", "Group identifier", "Group server name"], result = {res, rescode}}, #ejabberd_commands{name = get_offline_count, @@ -611,27 +698,42 @@ get_commands_spec() -> policy = user, module = mod_offline, function = count_offline_messages, args = [], + result_example = 5, + result_desc = "Number", result = {value, integer}}, #ejabberd_commands{name = send_message, tags = [stanza], 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}], + args_example = [<<"headline">>, <<"admin@localhost">>, <<"user1@localhost">>, + <<"Restart">>, <<"In 5 minutes">>], + args_desc = ["Message type: normal, chat, headline", "Sender JID", + "Receiver JID", "Subject, or empty string", "Body"], result = {res, rescode}}, #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_example = [<<"admin">>, <<"myserver.com">>, <<"bot">>, + <<"<message to='user1@localhost'><ext attr='value'/></message>">>], + args_desc = ["Username", "Server name", "Resource", "Stanza"], 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}], + args_example = [<<"admin@localhost">>, <<"user1@localhost">>, + <<"<message><ext attr='value'/></message>">>], + args_desc = ["Sender JID", "Destination JID", "Stanza"], result = {res, rescode}}, #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_example = [<<"user1">>, <<"myserver.com">>, + <<"<query xmlns='jabber:iq:privacy'>...">>], + args_desc = ["Username", "Server name", "Query XML element"], result = {res, rescode}}, #ejabberd_commands{name = stats, tags = [stats], @@ -639,12 +741,20 @@ get_commands_spec() -> policy = admin, module = ?MODULE, function = stats, args = [{name, binary}], + args_example = [<<"registeredusers">>], + args_desc = ["Statistic name"], + result_example = 6, + result_desc = "Integer statistic value", result = {stat, integer}}, #ejabberd_commands{name = stats_host, tags = [stats], desc = "Get statistical value for this host: registeredusers onlineusers", policy = admin, module = ?MODULE, function = stats, args = [{name, binary}, {host, binary}], + args_example = [<<"registeredusers">>, <<"example.com">>], + args_desc = ["Statistic name", "Server JID"], + result_example = 6, + result_desc = "Integer statistic value", result = {stat, integer}} ]. @@ -952,7 +1062,7 @@ connected_users_info() -> connected_users_vhost(Host) -> USRs = ejabberd_sm:get_vh_session_list(Host), - [ [U, $@, S, $/, R] || {U, S, R} <- USRs]. + [ jid:encode(jid:make(USR)) || USR <- USRs]. %% Code copied from ejabberd_sm.erl and customized dirty_get_sessions_list2() -> @@ -1002,20 +1112,16 @@ set_presence(User, Host, Resource, Type, Show, Status, Priority0) -> Priority = if is_integer(Priority0) -> Priority0; true -> binary_to_integer(Priority0) end, - case ejabberd_sm:get_session_pid(User, Host, Resource) of - none -> - error; - Pid -> - From = jid:make(User, Host, Resource), - To = jid:make(User, Host), - Presence = #presence{from = From, to = To, - type = misc:binary_to_atom(Type), - show = misc:binary_to_atom(Show), - status = xmpp:mk_text(Status), - priority = Priority}, - Pid ! {route, Presence}, - ok - end. + Pres = #presence{ + from = jid:make(User, Host, Resource), + to = jid:make(User, Host), + type = misc:binary_to_atom(Type), + status = xmpp:mk_text(Status), + show = misc:binary_to_atom(Show), + priority = Priority, + sub_els = []}, + Ref = ejabberd_sm:get_session_pid(User, Host, Resource), + ejabberd_c2s:set_presence(Ref, Pres). user_sessions_info(User, Host) -> CurrentSec = calendar:datetime_to_gregorian_seconds({date(), time()}), diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index 7be05dbf1..ef881d14b 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -538,9 +538,17 @@ json_error(HTTPCode, JSONCode, Message) -> log(Call, Args, {Addr, Port}) -> AddrS = misc:ip_to_list({Addr, Port}), - ?INFO_MSG("API call ~s ~p from ~s:~p", [Call, Args, AddrS, Port]); + ?INFO_MSG("API call ~s ~p from ~s:~p", [Call, hide_sensitive_args(Args), AddrS, Port]); log(Call, Args, IP) -> - ?INFO_MSG("API call ~s ~p (~p)", [Call, Args, IP]). + ?INFO_MSG("API call ~s ~p (~p)", [Call, hide_sensitive_args(Args), IP]). + +hide_sensitive_args(Args=[_H|_T]) -> + lists:map( fun({<<"password">>, Password}) -> {<<"password">>, ejabberd_config:may_hide_data(Password)}; + ({<<"newpass">>,NewPassword}) -> {<<"newpass">>, ejabberd_config:may_hide_data(NewPassword)}; + (E) -> E end, + Args); +hide_sensitive_args(NonListArgs) -> + NonListArgs. permission_addon() -> Access = gen_mod:get_module_opt(global, ?MODULE, admin_ip_access, none), diff --git a/src/mod_http_fileserver.erl b/src/mod_http_fileserver.erl index c2144042e..4e3cfd08b 100644 --- a/src/mod_http_fileserver.erl +++ b/src/mod_http_fileserver.erl @@ -59,8 +59,13 @@ -define(HTTP_ERR_FILE_NOT_FOUND, {-1, 404, [], <<"Not found">>}). +-define(REQUEST_AUTH_HEADERS, + [{<<"WWW-Authenticate">>, <<"Basic realm=\"ejabberd\"">>}]). + -define(HTTP_ERR_FORBIDDEN, {-1, 403, [], <<"Forbidden">>}). +-define(HTTP_ERR_REQUEST_AUTH, + {-1, 401, ?REQUEST_AUTH_HEADERS, <<"Unauthorized">>}). -define(DEFAULT_CONTENT_TYPE, <<"application/octet-stream">>). @@ -317,12 +322,17 @@ serve(LocalPath, Auth, DocRoot, DirectoryIndices, CustomHeaders, DefaultContentT false end, case CanProceed of + false -> + ?HTTP_ERR_REQUEST_AUTH; true -> FileName = filename:join(filename:split(DocRoot) ++ LocalPath), case file:read_file_info(FileName) of - {error, enoent} -> ?HTTP_ERR_FILE_NOT_FOUND; - {error, enotdir} -> ?HTTP_ERR_FILE_NOT_FOUND; - {error, eacces} -> ?HTTP_ERR_FORBIDDEN; + {error, enoent} -> + ?HTTP_ERR_FILE_NOT_FOUND; + {error, enotdir} -> + ?HTTP_ERR_FILE_NOT_FOUND; + {error, eacces} -> + ?HTTP_ERR_FORBIDDEN; {ok, #file_info{type = directory}} -> serve_index(FileName, DirectoryIndices, CustomHeaders, diff --git a/src/mod_mam.erl b/src/mod_mam.erl index eb2082fe2..d4c18d41b 100644 --- a/src/mod_mam.erl +++ b/src/mod_mam.erl @@ -1008,6 +1008,9 @@ get_commands_spec() -> longdesc = "Valid message TYPEs: " "\"chat\", \"groupchat\", \"all\".", module = ?MODULE, function = delete_old_messages, + args_desc = ["Type of messages to delete (chat, groupchat, all)", + "Days to keep messages"], + args_example = [<<"all">>, 31], args = [{type, binary}, {days, integer}], result = {res, rescode}}]. diff --git a/src/mod_muc.erl b/src/mod_muc.erl index 69fc2d0dc..8b6d7b8b9 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -166,7 +166,7 @@ restore_room(ServerHost, Host, Name) -> forget_room(ServerHost, Host, Name) -> LServer = jid:nameprep(ServerHost), - ejabberd_hooks:run(remove_room, LServer, [LServer, Name, Host]), + ejabberd_hooks:run(destroy_room, LServer, [LServer, Name, Host]), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:forget_room(LServer, Host, Name). @@ -256,6 +256,7 @@ handle_call({create, Room, From, Nick, Opts}, _From, Nick, NewOpts, QueueType), RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RMod:register_online_room(ServerHost, Room, Host, Pid), + ejabberd_hooks:run(create_room, ServerHost, [ServerHost, Room, Host]), {reply, ok, State}. handle_cast({reload, ServerHost, NewOpts, OldOpts}, #state{host = OldHost}) -> diff --git a/src/mod_muc_riak.erl b/src/mod_muc_riak.erl index 8dd1eddf8..42e644fdd 100644 --- a/src/mod_muc_riak.erl +++ b/src/mod_muc_riak.erl @@ -60,7 +60,7 @@ restore_room(_LServer, Host, Name) -> forget_room(_LServer, Host, Name) -> {atomic, ejabberd_riak:delete(muc_room, {Name, Host})}. -can_use_nick(LServer, Host, JID, Nick) -> +can_use_nick(_LServer, Host, JID, Nick) -> {LUser, LServer, _} = jid:tolower(JID), LUS = {LUser, LServer}, case ejabberd_riak:get_by_index(muc_registered, diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index ec1cffd6a..31dbbbfa7 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -4004,6 +4004,7 @@ tab_add_online_user(JID, StateData) -> Room = StateData#state.room, Host = StateData#state.host, ServerHost = StateData#state.server_host, + ejabberd_hooks:run(join_room, ServerHost, [ServerHost, Room, Host, JID]), mod_muc:register_online_user(ServerHost, jid:tolower(JID), Room, Host). -spec tab_remove_online_user(jid(), state()) -> any(). @@ -4011,6 +4012,7 @@ tab_remove_online_user(JID, StateData) -> Room = StateData#state.room, Host = StateData#state.host, ServerHost = StateData#state.server_host, + ejabberd_hooks:run(leave_room, ServerHost, [ServerHost, Room, Host, JID]), mod_muc:unregister_online_user(ServerHost, jid:tolower(JID), Room, Host). -spec tab_count_user(jid(), state()) -> non_neg_integer(). diff --git a/src/mod_privacy.erl b/src/mod_privacy.erl index eca229813..85384610d 100644 --- a/src/mod_privacy.erl +++ b/src/mod_privacy.erl @@ -410,9 +410,11 @@ decode_item(#privacy_item{order = Order, match_presence_out = MatchPresenceOut} end. --spec c2s_copy_session(ejabberd_c2s:state(), c2s_state()) -> c2s_state(). +-spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state(). c2s_copy_session(State, #{privacy_active_list := List}) -> - State#{privacy_active_list => List}. + State#{privacy_active_list => List}; +c2s_copy_session(State, _) -> + State. -spec user_send_packet({stanza(), c2s_state()}) -> {stanza(), c2s_state()}. user_send_packet({#iq{type = Type, diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl index 1a620cb6b..263b33ab9 100644 --- a/src/mod_pubsub.erl +++ b/src/mod_pubsub.erl @@ -546,14 +546,7 @@ disco_identity(Host, Node, From) -> case get_allowed_items_call(Host, Nidx, From, Type, Options, Owners) of {result, _} -> - {result, [#identity{category = <<"pubsub">>, - type = <<"pep">>}, - #identity{category = <<"pubsub">>, - type = <<"leaf">>, - name = case get_option(Options, title) of - false -> <<>>; - [Title] -> Title - end}]}; + {result, [#identity{category = <<"pubsub">>, type = <<"pep">>}]}; _ -> {result, []} end @@ -586,8 +579,7 @@ disco_features(Host, Node, From) -> Type, Options, Owners) of {result, _} -> {result, - [?NS_PUBSUB | - [feature(F) || F <- plugin_features(Host, <<"pep">>)]]}; + [?NS_PUBSUB | [feature(F) || F <- plugin_features(Host, <<"pep">>)]]}; _ -> {result, []} end @@ -620,7 +612,7 @@ disco_items(Host, <<>>, From) -> jid = jid:make(Host), name = case get_option(Options, title) of false -> <<>>; - [Title] -> Title + Title -> Title end} | Acc]; _ -> Acc @@ -679,16 +671,14 @@ caps_update(#jid{luser = U, lserver = S, lresource = R}, #jid{lserver = Host} = presence(Host, {presence, U, S, [R], JID}). -spec presence_probe(jid(), jid(), pid()) -> ok. -presence_probe(#jid{luser = U, lserver = S, lresource = R} = JID, JID, Pid) -> - presence(S, {presence, JID, Pid}), - presence(S, {presence, U, S, [R], JID}); presence_probe(#jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}, _Pid) -> %% ignore presence_probe from my other ressources %% to not get duplicated last items ok; -presence_probe(#jid{luser = U, lserver = S, lresource = R}, #jid{lserver = S} = JID, _Pid) -> - presence(S, {presence, U, S, [R], JID}); -presence_probe(_Host, _JID, _Pid) -> +presence_probe(#jid{lserver = S} = From, #jid{lserver = S} = To, Pid) -> + presence(S, {presence, From, Pid}), + presence(S, {presence, From#jid.luser, S, [From#jid.lresource], To}); +presence_probe(_From, _To, _Pid) -> %% ignore presence_probe from remote contacts, %% those are handled via caps_add ok. @@ -1723,7 +1713,7 @@ delete_node(Host, Node, Owner) -> %%<li>The node does not support subscriptions.</li> %%<li>The node does not exist.</li> %%</ul> --spec subscribe_node(host(), binary(), jid(), binary(), [{binary(), [binary()]}]) -> +-spec subscribe_node(host(), binary(), jid(), jid(), [{binary(), [binary()]}]) -> {result, pubsub()} | {error, stanza_error()}. subscribe_node(Host, Node, From, JID, Configuration) -> SubModule = subscription_plugin(Host), @@ -2152,9 +2142,14 @@ get_allowed_items_call(Host, Nidx, From, Type, Options, Owners, RSM) -> node_call(Host, Type, get_items, [Nidx, From, AccessModel, PS, RG, undefined, RSM]). get_last_items(Host, Type, Nidx, LJID, Count) -> - case node_action(Host, Type, get_last_items, [Nidx, LJID, Count]) of - {result, Items} -> Items; - _ -> [] + case get_cached_item(Host, Nidx) of + undefined -> + case node_action(Host, Type, get_last_items, [Nidx, LJID, Count]) of + {result, Items} -> Items; + _ -> [] + end; + LastItem -> + [LastItem] end. %% @doc <p>Resend the items of a node to the user.</p> diff --git a/src/node_pep.erl b/src/node_pep.erl index 6dd7a68c4..cc0dd41fb 100644 --- a/src/node_pep.erl +++ b/src/node_pep.erl @@ -119,7 +119,7 @@ create_node(Nidx, Owner) -> delete_node(Nodes) -> {result, {_, _, Result}} = node_flat:delete_node(Nodes), - {result, {[], Result}}. + {result, {default, Result}}. subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> diff --git a/src/node_pep_sql.erl b/src/node_pep_sql.erl index 68a2ce946..b84c945bd 100644 --- a/src/node_pep_sql.erl +++ b/src/node_pep_sql.erl @@ -73,7 +73,7 @@ create_node(Nidx, Owner) -> delete_node(Nodes) -> {result, {_, _, Result}} = node_flat_sql:delete_node(Nodes), - {result, {[], Result}}. + {result, {default, Result}}. subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> diff --git a/test/ejabberd_SUITE_data/extauth.py b/test/ejabberd_SUITE_data/extauth.py index fa2c9efd0..263d6464e 100755 --- a/test/ejabberd_SUITE_data/extauth.py +++ b/test/ejabberd_SUITE_data/extauth.py @@ -3,23 +3,27 @@ import struct def read(): (pkt_size,) = struct.unpack('>H', sys.stdin.read(2)) - pkt = sys.stdin.read(pkt_size).split(':') - cmd = pkt[0] - args_num = len(pkt) - 1 - if cmd == 'auth' and args_num >= 3: - if pkt[1] == "wrong": + pkt = sys.stdin.read(pkt_size) + cmd = pkt.split(':')[0] + if cmd == 'auth': + u, s, p = pkt.split(':', 3)[1:] + if u == "wrong": write(False) else: write(True) - elif cmd == 'isuser' and args_num == 2: + elif cmd == 'isuser': + u, s = pkt.split(':', 2)[1:] + elif cmd == 'setpass': + u, s, p = pkt.split(':', 3)[1:] write(True) - elif cmd == 'setpass' and args_num >= 3: + elif cmd == 'tryregister': + u, s, p = pkt.split(':', 3)[1:] write(True) - elif cmd == 'tryregister' and args_num >= 3: + elif cmd == 'removeuser': + u, s = pkt.split(':', 2)[1:] write(True) - elif cmd == 'removeuser' and args_num == 2: - write(True) - elif cmd == 'removeuser3' and args_num >= 3: + elif cmd == 'removeuser3': + u, s, p = pkt.split(':', 3)[1:] write(True) else: write(False) diff --git a/test/ejabberd_admin_test.exs b/test/ejabberd_admin_test.exs index effcbc579..aa3c2ee41 100644 --- a/test/ejabberd_admin_test.exs +++ b/test/ejabberd_admin_test.exs @@ -25,12 +25,15 @@ defmodule EjabberdAdminTest do setup_all do :mnesia.start + :ejabberd_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_hooks.start_link + {:ok, _} = :acl.start_link {:ok, _} = :ejabberd_access_permissions.start_link() - :ejabberd_commands.init - :ejabberd_admin.start + :ejabberd_commands.start_link + :ejabberd_admin.start_link :ok end @@ -41,40 +44,44 @@ defmodule EjabberdAdminTest do test "Logvel can be set and retrieved" do :ejabberd_logger.start() - assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [1]) + assert :lager == call_command(:set_loglevel, [1]) assert {1, :critical, 'Critical'} == - :ejabberd_commands.execute_command(:get_loglevel, []) + call_command(:get_loglevel, []) - assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [2]) + assert :lager == call_command(:set_loglevel, [2]) assert {2, :error, 'Error'} == - :ejabberd_commands.execute_command(:get_loglevel, []) + call_command(:get_loglevel, []) - assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [3]) + assert :lager == call_command(:set_loglevel, [3]) assert {3, :warning, 'Warning'} == - :ejabberd_commands.execute_command(:get_loglevel, []) + call_command(:get_loglevel, []) assert {:wrong_loglevel, 6} == - catch_throw :ejabberd_commands.execute_command(:set_loglevel, [6]) + catch_throw call_command(:set_loglevel, [6]) assert {3, :warning, 'Warning'} == - :ejabberd_commands.execute_command(:get_loglevel, []) + call_command(:get_loglevel, []) - assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [4]) + assert :lager == call_command(:set_loglevel, [4]) assert {4, :info, 'Info'} == - :ejabberd_commands.execute_command(:get_loglevel, []) + call_command(:get_loglevel, []) - assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [5]) + assert :lager == call_command(:set_loglevel, [5]) assert {5, :debug, 'Debug'} == - :ejabberd_commands.execute_command(:get_loglevel, []) + call_command(:get_loglevel, []) - assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [0]) + assert :lager == call_command(:set_loglevel, [0]) assert {0, :no_log, 'No log'} == - :ejabberd_commands.execute_command(:get_loglevel, []) + call_command(:get_loglevel, []) end + defp call_command(name, args) do + :ejabberd_commands.execute_command2(name, args, %{:caller_module => :ejabberd_ctl}) + end + test "command status works with ejabberd stopped" do assert :ejabberd_not_running == - elem(:ejabberd_commands.execute_command(:status, []), 0) + elem(call_command(:status, []), 0) end end diff --git a/test/ejabberd_commands_mock_test.exs b/test/ejabberd_commands_mock_test.exs deleted file mode 100644 index d7ac7ae12..000000000 --- a/test/ejabberd_commands_mock_test.exs +++ /dev/null @@ -1,481 +0,0 @@ -# ---------------------------------------------------------------------- -# -# ejabberd, Copyright (C) 2002-2017 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. -# -# ---------------------------------------------------------------------- - -## TODO Fix next test error: add admin user ACL - -defmodule EjabberdCommandsMockTest do - use ExUnit.Case, async: false - - require EjabberdOauthMock - - @author "jsautret@process-one.net" - - # mocked callback module - @module :test_module - # Admin user - @admin "admin" - @adminpass "adminpass" - # Non admin user - @user "user" - @userpass "userpass" - # XMPP domain - @domain "domain" - - require Record - Record.defrecord :ejabberd_commands, Record.extract(:ejabberd_commands, from_lib: "ejabberd/include/ejabberd_commands.hrl") - - setup_all do - :ok = :ejabberd.start_app(:lager) - try do - :stringprep.start - rescue - _ -> :ok - end - :mnesia.start - :ejabberd_mnesia.start - :jid.start - :ejabberd_hooks.start_link - :ok = :ejabberd_config.start(["domain1", "domain2"], []) - {:ok, _} = :ejabberd_access_permissions.start_link() - {:ok, _} = :acl.start_link - :ejabberd_oauth.start_link - :ejabberd_commands.start_link - EjabberdOauthMock.init - on_exit fn -> :meck.unload end - end - - setup do - :meck.unload - :meck.new(@module, [:non_strict]) - :mnesia.clear_table(:ejabberd_commands) - :ejabberd_commands.register_commands(:ejabberd_commands.get_commands_spec()) - :ok - 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 - mock_commands_config - - # 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 - mock_commands_config - - 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 {:error, :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 [:user, :admin] - - # 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 [:admin] - - # 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 - - test "Commands can perform extra check on access" do - mock_commands_config [:admin, :open] - - command_name = :test - function = :test_command - command = ejabberd_commands(name: command_name, - args: [{:user, :binary}, {:host, :binary}], - access: [:basic_rule_1], - module: @module, - function: function, - policy: :open) - :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] - -# :acl.add(:global, :basic_acl_1, {:user, @user, @host}) -# :acl.add_access(:global, :basic_rule_1, [{:allow, [{:acl, :basic_acl_1}]}]) - - assert {@user, @domain} == - :ejabberd_commands.execute_command(:undefined, - {@user, @domain, - @userpass, false}, - command_name, - [@user, @domain]) - assert {@user, @domain} == - :ejabberd_commands.execute_command(:undefined, - {@admin, @domain, - @adminpass, false}, - command_name, - [@user, @domain]) - - end - - ########################################################## - # Utils - - # Mock a config where only @admin user is allowed to call commands - # as admin - def mock_commands_config(commands \\ []) 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 - (:commands, _) -> [{:add_commands, commands}] - (_, default) -> default - end) - :meck.expect(:ejabberd_config, :get_myhosts, - fn() -> [@domain] end) - :meck.expect(:ejabberd_config, :default_db, - fn(_) -> :mnesia end) - :meck.expect(:ejabberd_config, :default_db, - fn(_, _) -> :mnesia end) - - :meck.new :acl - :meck.expect(:acl, :access_matches, - fn(:commands_admin_access, info, _scope) -> - case info do - %{usr: {@admin, @domain, _}} -> :allow - _ -> :deny - end; - (:all, _, _scope) -> - :allow - end) - end - -end diff --git a/test/ejabberd_commands_test.exs b/test/ejabberd_commands_test.exs deleted file mode 100644 index 40e860c27..000000000 --- a/test/ejabberd_commands_test.exs +++ /dev/null @@ -1,108 +0,0 @@ -# ---------------------------------------------------------------------- -# -# ejabberd, Copyright (C) 2002-2017 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 EjabberdCommandsTest do - @author "mremond@process-one.net" - - use ExUnit.Case, async: true - - require Record - Record.defrecord :ejabberd_commands, Record.extract(:ejabberd_commands, from_lib: "ejabberd/include/ejabberd_commands.hrl") - - setup_all do - :mnesia.start - :ejabberd_mnesia.start - :stringprep.start - :ok = :ejabberd_config.start(["localhost"], []) - {:ok, _} = :ejabberd_access_permissions.start_link() - - :ejabberd_commands.start_link - :ok - end - - test "Check that we can register a command" do - :ok = :ejabberd_commands.register_commands([user_test_command]) - commands = :ejabberd_commands.list_commands - assert Enum.member?(commands, {:test_user, [], "Test user"}) - end - - test "get_exposed_commands/0 returns registered commands" do - commands = [open_test_command] - :ok = :ejabberd_commands.register_commands(commands) - :ok = :ejabberd_commands.expose_commands(commands) - exposed_commands = :ejabberd_commands.get_exposed_commands - assert Enum.member?(exposed_commands, :test_open) - end - - test "Check that admin commands are rejected with noauth credentials" do - :ok = :ejabberd_commands.register_commands([admin_test_command]) - - assert catch_throw(:ejabberd_commands.execute_command(:undefined, :noauth, :test_admin, [])) == {:error, :account_unprivileged} - - # Command executed from ejabberdctl passes anyway with access commands trick - # TODO: We should refactor to have explicit call when bypassing auth check for command-line - :ok = :ejabberd_commands.execute_command([], :noauth, :test_admin, []) - end - - # TODO Test that we can add command to list of expose commands - # This can be done with: - # ejabberd_config:add_local_option(commands, [[{add_commands, [open_cmd]}]]). - -# test "Check that a user can use a user command" do -# [Command] = ets:lookup(ejabberd_commands, test_user), -# AccessCommands = ejabberd_commands:get_access_commands(undefined), -# ejabberd_commands:check_access_commands(AccessCommands, {<<"test">>,<<"localhost">>, {oauth,<<"MyToken">>}, false}, test_user, Command, []). -# end - - defp user_test_command do - 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 - - defp open_test_command do - ejabberd_commands(name: :test_open, tags: [:test], - desc: "Test open", - policy: :open, - module: __MODULE__, - function: :test_open, - args: [], - result: {:res, :rescode}) - end - - defp admin_test_command do - ejabberd_commands(name: :test_admin, tags: [:roster], - desc: "Test admin", - policy: :restricted, - module: __MODULE__, - function: :test_admin, - args: [], - result: {:res, :rescode}) - end - - def test_admin, do: :ok -end diff --git a/test/ejabberd_cyrsasl_test.exs b/test/ejabberd_cyrsasl_test.exs index f601c78ef..bdef92cd4 100644 --- a/test/ejabberd_cyrsasl_test.exs +++ b/test/ejabberd_cyrsasl_test.exs @@ -30,6 +30,7 @@ defmodule EjabberdCyrsaslTest do :ejabberd_mnesia.start :ok = start_module(:stringprep) start_module(:jid) + :ejabberd_hooks.start_link :ok = :ejabberd_config.start(["domain1"], []) {:ok, _} = :cyrsasl.start_link cyrstate = :cyrsasl.server_new("domain1", "domain1", "domain1", :ok, &get_password/1, diff --git a/test/mod_admin_extra_test.exs b/test/mod_admin_extra_test.exs index eefe2e9bd..23ed38c61 100644 --- a/test/mod_admin_extra_test.exs +++ b/test/mod_admin_extra_test.exs @@ -41,16 +41,20 @@ defmodule EjabberdModAdminExtraTest do :jid.start :stringprep.start :mnesia.start + :ejabberd_mnesia.start :p1_sha.load_nif + :ejabberd_hooks.start_link rescue _ -> :ok end - {:ok, _} = :ejabberd_access_permissions.start_link() - :ejabberd_commands.init - :ok = :ejabberd_config.start([@domain], []) + :acl.start_link + :ejabberd_access_permissions.start_link() + :ejabberd_commands.start_link + :ok = :ejabberd_config.start([@domain], []) + :gen_mod.start_link :mod_admin_extra.start(@domain, []) :sel_application.start_app(:moka) - {:ok, _pid} = :ejabberd_hooks.start_link + :ejabberd_hooks.start_link :ok end @@ -66,9 +70,9 @@ defmodule EjabberdModAdminExtraTest do 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 call_command(:check_account, [@user, @domain]) + refute call_command(:check_account, [@user, "bad_domain"]) + refute call_command(:check_account, ["bad_user", @domain]) assert :meck.validate :ejabberd_auth end @@ -77,13 +81,13 @@ defmodule EjabberdModAdminExtraTest do EjabberdAuthMock.create_user @user, @domain, @password - assert :ejabberd_commands.execute_command(:check_password, + assert call_command(:check_password, [@user, @domain, @password]) - refute :ejabberd_commands.execute_command(:check_password, + refute call_command(:check_password, [@user, @domain, "bad_password"]) - refute :ejabberd_commands.execute_command(:check_password, + refute call_command(:check_password, [@user, "bad_domain", @password]) - refute :ejabberd_commands.execute_command(:check_password, + refute call_command(:check_password, ["bad_user", @domain, @password]) assert :meck.validate :ejabberd_auth @@ -95,21 +99,21 @@ defmodule EjabberdModAdminExtraTest do EjabberdAuthMock.create_user @user, @domain, @password hash = "5F4DCC3B5AA765D61D8327DEB882CF99" # echo -n password|md5 - assert :ejabberd_commands.execute_command(:check_password_hash, + assert call_command(:check_password_hash, [@user, @domain, hash, "md5"]) - refute :ejabberd_commands.execute_command(:check_password_hash, + refute call_command(:check_password_hash, [@user, @domain, "bad_hash", "md5"]) - refute :ejabberd_commands.execute_command(:check_password_hash, + refute call_command(:check_password_hash, [@user, "bad_domain", hash, "md5"]) - refute :ejabberd_commands.execute_command(:check_password_hash, + refute call_command(:check_password_hash, ["bad_user", @domain, hash, "md5"]) hash = "5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8" # echo -n password|shasum - assert :ejabberd_commands.execute_command(:check_password_hash, + assert call_command(:check_password_hash, [@user, @domain, hash, "sha"]) assert :unkown_hash_method == - catch_throw :ejabberd_commands.execute_command(:check_password_hash, + catch_throw call_command(:check_password_hash, [@user, @domain, hash, "bad_method"]) assert :meck.validate :ejabberd_auth @@ -119,14 +123,14 @@ defmodule EjabberdModAdminExtraTest do test "set_password works" do EjabberdAuthMock.create_user @user, @domain, @password - assert :ejabberd_commands.execute_command(:change_password, + assert call_command(:change_password, [@user, @domain, "new_password"]) - refute :ejabberd_commands.execute_command(:check_password, + refute call_command(:check_password, [@user, @domain, @password]) - assert :ejabberd_commands.execute_command(:check_password, + assert call_command(:check_password, [@user, @domain, "new_password"]) assert {:not_found, 'unknown_user'} == - catch_throw :ejabberd_commands.execute_command(:change_password, + catch_throw call_command(:change_password, ["bad_user", @domain, @password]) assert :meck.validate :ejabberd_auth @@ -135,23 +139,23 @@ defmodule EjabberdModAdminExtraTest do ###################### Sessions test "num_resources works" do - assert 0 == :ejabberd_commands.execute_command(:num_resources, + assert 0 == call_command(:num_resources, [@user, @domain]) EjabberdSmMock.connect_resource @user, @domain, @resource - assert 1 == :ejabberd_commands.execute_command(:num_resources, + assert 1 == call_command(:num_resources, [@user, @domain]) EjabberdSmMock.connect_resource @user, @domain, @resource<>"2" - assert 2 == :ejabberd_commands.execute_command(:num_resources, + assert 2 == call_command(:num_resources, [@user, @domain]) EjabberdSmMock.connect_resource @user<>"1", @domain, @resource - assert 2 == :ejabberd_commands.execute_command(:num_resources, + assert 2 == call_command(:num_resources, [@user, @domain]) EjabberdSmMock.disconnect_resource @user, @domain, @resource - assert 1 == :ejabberd_commands.execute_command(:num_resources, + assert 1 == call_command(:num_resources, [@user, @domain]) assert :meck.validate :ejabberd_sm @@ -163,14 +167,14 @@ defmodule EjabberdModAdminExtraTest do EjabberdSmMock.connect_resource @user, @domain, @resource<>"1" assert :bad_argument == - elem(catch_throw(:ejabberd_commands.execute_command(:resource_num, + elem(catch_throw(call_command(:resource_num, [@user, @domain, 0])), 0) assert @resource<>"1" == - :ejabberd_commands.execute_command(:resource_num, [@user, @domain, 1]) + call_command(:resource_num, [@user, @domain, 1]) assert @resource<>"3" == - :ejabberd_commands.execute_command(:resource_num, [@user, @domain, 3]) + call_command(:resource_num, [@user, @domain, 3]) assert :bad_argument == - elem(catch_throw(:ejabberd_commands.execute_command(:resource_num, + elem(catch_throw(call_command(:resource_num, [@user, @domain, 4])), 0) assert :meck.validate :ejabberd_sm end @@ -184,7 +188,7 @@ defmodule EjabberdModAdminExtraTest do assert 1 == length EjabberdSmMock.get_session @user, @domain, @resource<>"2" assert :ok == - :ejabberd_commands.execute_command(:kick_session, + call_command(:kick_session, [@user, @domain, @resource<>"2", "kick"]) @@ -199,18 +203,18 @@ defmodule EjabberdModAdminExtraTest do test "get_last works" do assert {_, 'NOT FOUND'} = - :ejabberd_commands.execute_command(:get_last, [@user, @domain]) + call_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]) + call_command(:get_last, [@user, @domain]) EjabberdSmMock.disconnect_resource @user, @domain, @resource<>"1" assert {_, 'ONLINE'} = - :ejabberd_commands.execute_command(:get_last, [@user, @domain]) + call_command(:get_last, [@user, @domain]) now = {megasecs, secs, _microsecs} = :os.timestamp timestamp = megasecs * 1000000 + secs @@ -221,7 +225,7 @@ defmodule EjabberdModAdminExtraTest do "~w-~.2.0w-~.2.0wT~.2.0w:~.2.0w:~.2.0wZ", [year, month, day, hour, minute, second])) assert {result, ""} == - :ejabberd_commands.execute_command(:get_last, [@user, @domain]) + call_command(:get_last, [@user, @domain]) assert :meck.validate :mod_last end @@ -238,7 +242,7 @@ defmodule EjabberdModAdminExtraTest do assert [] == ModRosterMock.get_roster(@user, @domain) assert :ok == - :ejabberd_commands.execute_command(:add_rosteritem, [@user, @domain, + call_command(:add_rosteritem, [@user, @domain, @user<>"1", @domain, "nick1", "group1", @@ -261,7 +265,7 @@ defmodule EjabberdModAdminExtraTest do {:item, {@user<>"1", @domain, ""}, :both}]) assert :ok == - :ejabberd_commands.execute_command(:add_rosteritem, [@user, @domain, + call_command(:add_rosteritem, [@user, @domain, @user<>"2", @domain, "nick2", "group2", @@ -278,7 +282,7 @@ defmodule EjabberdModAdminExtraTest do {:item, {@user<>"2", @domain, ""}, :both}]) - :ejabberd_commands.execute_command(:delete_rosteritem, [@user, @domain, + call_command(:delete_rosteritem, [@user, @domain, @user<>"1", @domain]) result = ModRosterMock.get_roster(@user, @domain) assert 1 == length result @@ -296,7 +300,7 @@ defmodule EjabberdModAdminExtraTest do [jid, {:item, {@user<>"1", @domain, ""}, :none}]) - :ejabberd_commands.execute_command(:delete_rosteritem, [@user, @domain, + call_command(:delete_rosteritem, [@user, @domain, @user<>"2", @domain]) # Check that the item roster user2 was pushed with subscription @@ -321,39 +325,47 @@ defmodule EjabberdModAdminExtraTest do test "get_roster works" do assert [] == ModRosterMock.get_roster(@user, @domain) - assert [] == :ejabberd_commands.execute_command(:get_roster, [@user, @domain], + assert [] == call_command(:get_roster, [@user, @domain], :admin) assert :ok == - :ejabberd_commands.execute_command(:add_rosteritem, [@user, @domain, + call_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) + call_command(:get_roster, [@user, @domain], :admin) assert :ok == - :ejabberd_commands.execute_command(:add_rosteritem, [@user, @domain, + call_command(:add_rosteritem, [@user, @domain, @user<>"2", @domain, "nick2", "group2", "none"]) - result = :ejabberd_commands.execute_command(:get_roster, [@user, @domain], :admin) + result = call_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 + defp call_command(name, args) do + :ejabberd_commands.execute_command2(name, args, %{:caller_module => :ejabberd_ctl}) + end + + defp call_command(name, args, mode) do + call_command(name, args) + 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, +# assert 0 == call_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, +# call_command(:kick_user, [@user, @domain]) +# assert 0 == call_command(:num_resources, # [@user, @domain]) # assert :meck.validate :ejabberd_sm # end diff --git a/test/mod_http_api_mock_test.exs b/test/mod_http_api_mock_test.exs index 4dde78939..ceda2bb0f 100644 --- a/test/mod_http_api_mock_test.exs +++ b/test/mod_http_api_mock_test.exs @@ -45,10 +45,11 @@ defmodule ModHttpApiMockTest do :jid.start :mnesia.start :ejabberd_mnesia.start - :stringprep.start + :stringprep.start + :ejabberd_hooks.start_link :ejabberd_config.start([@domain], []) {:ok, _} = :ejabberd_access_permissions.start_link() - :ejabberd_commands.init + :ejabberd_commands.start_link rescue _ -> :ok end @@ -73,18 +74,12 @@ defmodule ModHttpApiMockTest do fn (@acommand, %{usr: {@user, @domain, _}}, @version) -> {[], {:res, :rescode}} end) - :meck.expect(:ejabberd_commands, :get_command_policy_and_scope, - fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8)]} end) - :meck.expect(:ejabberd_commands, :get_exposed_commands, + :meck.expect(:ejabberd_commands, :get_exposed_commands, fn () -> [@acommand] end) :meck.expect(:ejabberd_commands, :execute_command2, fn (@acommand, [], %{usr: {@user, @domain, _}}, @version) -> :ok end) - :meck.expect(:ejabberd_commands, :execute_command, - fn (:undefined, {@user, @domain, @userpass, false}, @acommand, [], @version, _) -> - :ok - end) :ejabberd_config.add_local_option(:commands, [[{:add_commands, [@acommand]}]]) @@ -130,9 +125,7 @@ defmodule ModHttpApiMockTest do fn (@acommand, %{usr: {@user, @domain, _}}, @version) -> {[], {:res, :rescode}} end) - :meck.expect(:ejabberd_commands, :get_command_policy_and_scope, - fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8), "ejabberd:user"]} end) - :meck.expect(:ejabberd_commands, :get_exposed_commands, + :meck.expect(:ejabberd_commands, :get_exposed_commands, fn () -> [@acommand] end) :meck.expect(:ejabberd_commands, :execute_command2, fn (@acommand, [], %{usr: {@user, @domain, _}, oauth_scope: ["ejabberd:user"]}, @version) -> @@ -142,11 +135,6 @@ defmodule ModHttpApiMockTest do (@acommand, [], %{usr: {@user, @domain, _}, oauth_scope: _}, @version) -> throw({:error, :access_rules_unauthorized}) end) - :meck.expect(:ejabberd_commands, :execute_command, - fn (:undefined, {@user, @domain, {:oauth, _token}, false}, - @acommand, [], @version, _) -> - :ok - end) # Correct OAuth call using specific scope @@ -231,15 +219,8 @@ defmodule ModHttpApiMockTest do fn (@acommand, {@user, @domain, {:oauth, _token}, false}, @version) -> {[], {:res, :rescode}} end) - :meck.expect(:ejabberd_commands, :get_command_policy_and_scope, - fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8), "ejabberd:user"]} end) - :meck.expect(:ejabberd_commands, :get_exposed_commands, + :meck.expect(:ejabberd_commands, :get_exposed_commands, fn () -> [@acommand] end) - :meck.expect(:ejabberd_commands, :execute_command, - fn (:undefined, {@user, @domain, {:oauth, _token}, false}, - @acommand, [], @version, _) -> - :ok - end) #Mock acl to allow oauth authorizations :meck.expect(:acl, :match_rule, fn(_Server, _Access, _Jid) -> :allow end) diff --git a/test/mod_http_api_test.exs b/test/mod_http_api_test.exs index c39af79dc..29405a3ec 100644 --- a/test/mod_http_api_test.exs +++ b/test/mod_http_api_test.exs @@ -31,9 +31,11 @@ defmodule ModHttpApiTest do :ok = :mnesia.start :ejabberd_mnesia.start :stringprep.start + :ejabberd_hooks.start_link :ok = :ejabberd_config.start(["localhost"], []) + :acl.start_link {:ok, _} = :ejabberd_access_permissions.start_link() - :ok = :ejabberd_commands.init + {:ok, _} = :ejabberd_commands.start_link :ok = :ejabberd_commands.register_commands(cmds) on_exit fn -> :meck.unload @@ -42,7 +44,7 @@ defmodule ModHttpApiTest do test "We can expose several commands to API at a time" do setup_mocks() - :ejabberd_commands.expose_commands([:open_cmd, :user_cmd]) + assert :ok == :ejabberd_commands.expose_commands([:open_cmd, :user_cmd]) commands = :ejabberd_commands.get_exposed_commands() assert Enum.member?(commands, :open_cmd) assert Enum.member?(commands, :user_cmd) @@ -58,14 +60,14 @@ defmodule ModHttpApiTest do # This related to the commands config file option test "Attempting to access a command that is not exposed as HTTP API returns 403" do setup_mocks() - :ejabberd_commands.expose_commands([]) + assert :ok == :ejabberd_commands.expose_commands([]) request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]") {403, _, _} = :mod_http_api.process(["open_cmd"], request) end test "Call to user, admin or restricted commands without authentication are rejected" do setup_mocks() - :ejabberd_commands.expose_commands([:user_cmd, :admin_cmd, :restricted]) + assert :ok == :ejabberd_commands.expose_commands([:user_cmd, :admin_cmd, :restricted]) request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]") {403, _, _} = :mod_http_api.process(["user_cmd"], request) {403, _, _} = :mod_http_api.process(["admin_cmd"], request) diff --git a/test/mod_roster_mock.exs b/test/mod_roster_mock.exs index 58e759729..02b62183e 100644 --- a/test/mod_roster_mock.exs +++ b/test/mod_roster_mock.exs @@ -23,6 +23,7 @@ defmodule ModRosterMock do require Record Record.defrecord :roster, Record.extract(:roster, from_lib: "ejabberd/include/mod_roster.hrl") + Record.defrecord :roster_version, Record.extract(:roster_version, from_lib: "ejabberd/include/mod_roster.hrl") @agent __MODULE__ @@ -37,6 +38,13 @@ defmodule ModRosterMock do mock_with_moka module + :ejabberd_mnesia.create(:mod_roster_mnesia, :roster, + [ram_copies: [node()], + attributes: Keyword.keys(roster(roster())), + index: [:us]]) + :ejabberd_mnesia.create(:mod_roster_mnesia, :roster_version, + [ram_copies: [node()], + attributes: Keyword.keys(roster_version(roster_version()))]) #:mod_roster.stop(domain) :mod_roster.start(domain, []) end @@ -92,6 +100,11 @@ defmodule ModRosterMock do :moka.load(roster_mock0) roster_mock = :moka.start(:mod_roster_mnesia) + :moka.replace(roster_mock, :init, + fn (_host, _opts) -> + :ok + end) + :moka.replace(roster_mock, :gen_mod, :db_type, fn (_host, _opts) -> {:none} diff --git a/tools/update-deps-releases.pl b/tools/update-deps-releases.pl index aa8f178e6..3536ed101 100755 --- a/tools/update-deps-releases.pl +++ b/tools/update-deps-releases.pl @@ -33,10 +33,13 @@ sub get_deps { return \%deps; } my (%info_updates, %top_deps_updates, %sub_deps_updates, @operations); +my $epoch = 1; sub top_deps { state %deps; - if (not %deps) { + state $my_epoch = $epoch; + if (not %deps or $my_epoch != $epoch) { + $my_epoch = $epoch; my $config = slurp "rebar.config"; croak "Unable to extract floating_deps" unless $config =~ /\{floating_deps, \[(.*?)\]/s; @@ -49,7 +52,9 @@ sub top_deps { } sub update_deps_repos { + my ($force) = @_; my $deps = top_deps(); + $epoch++; mkdir(".deps-update") unless -d ".deps-update"; for my $dep (keys %{$deps}) { my $dd = ".deps-update/$dep"; @@ -58,7 +63,7 @@ sub update_deps_repos { my $repo = $deps->{$dep}->{repo}; $repo =~ s!^https?://github.com/!git\@github.com:!; system("git", "-C", ".deps-update", "clone", $repo); - } elsif (time() - stat($dd)->mtime > 24 * 60 * 60) { + } elsif (time() - stat($dd)->mtime > 24 * 60 * 60 or $force) { say "Updating $dep..."; system("git", "-C", $dd, "pull"); touch($dd) @@ -68,7 +73,9 @@ sub update_deps_repos { sub sub_deps { state %sub_deps; - if (not %sub_deps) { + state $my_epoch = $epoch; + if (not %sub_deps or $my_epoch != $epoch) { + $my_epoch = $epoch; my $deps = top_deps(); for my $dep (keys %{$deps}) { my $rc = ".deps-update/$dep/rebar.config"; @@ -94,7 +101,9 @@ sub rev_deps_helper { sub rev_deps { state %rev_deps; - if (not %rev_deps) { + state $deps_epoch = $epoch; + if (not %rev_deps or $deps_epoch != $epoch) { + $deps_epoch = $epoch; my $sub_deps = sub_deps(); for my $dep (keys %$sub_deps) { $rev_deps{$_}->{direct}->{$dep} = 1 for keys %{$sub_deps->{$dep}}; @@ -113,13 +122,46 @@ sub update_changelog { my $reason = join "\n", map {"* $_"} @reasons; my $content = slurp($cl); if (not $content =~ /^# Version $version/) { - $content = "# Version $version\n\n$reason\n\n$content" + $content = "# Version $version\n\n$reason\n\n$content"; } else { $content =~ s/(# Version $version\n\n)/$1$reason\n/; } write_file($cl, $content); } +sub edit_changelog { + my ($dep, $version) = @_; + my $cl = ".deps-update/$dep/CHANGELOG.md"; + + return if not -f $cl; + + my $top_deps = top_deps(); + my $git_info = deps_git_info(); + + say color("red"), "$dep", color("reset"), " ($top_deps->{$dep}->{commit}):"; + say " $_" for @{$git_info->{$dep}->{new_commits}}; + say ""; + + my $content = slurp($cl); + my $old_content = $content; + + if (not $content =~ /^# Version $version/) { + $content = "# Version $version\n\n* \n\n$content"; + } else { + $content =~ s/(# Version $version\n\n)/$1* \n/; + } + write_file($cl, $content); + + system("$ENV{EDITOR} $cl"); + + my $new_content = slurp($cl); + if ($new_content eq $content) { + write_file($cl, $old_content); + } else { + system("git", "-C", ".deps-update/$dep", "commit", "-a", "-m", "Update changelog"); + } +} + sub update_app_src { my ($dep, $version) = @_; my $app = ".deps-update/$dep/src/$dep.app.src"; @@ -177,7 +219,9 @@ sub cmp_ver { sub deps_git_info { state %info; - if (not %info) { + state $my_epoch = $epoch; + if (not %info or $my_epoch != $epoch) { + $my_epoch = $epoch; my $deps = top_deps(); for my $dep (keys %{$deps}) { my $dir = ".deps-update/$dep"; @@ -281,6 +325,7 @@ sub git_push { update_deps_repos(); +MAIN: while (1) { my $top_deps = top_deps(); my $git_info = deps_git_info(); @@ -308,6 +353,7 @@ while (1) { my $cmd = show_commands($old_deps ? (U => "Update dependency") : (), $changed_deps ? (T => "Tag new release") : (), @operations ? (A => "Apply changes") : (), + R => "Refresh repositiories", E => "Exit"); last if $cmd eq "E"; @@ -333,6 +379,9 @@ while (1) { } } + if ($cmd eq "R") { + update_deps_repos(1); + } if ($cmd eq "T") { while (1) { my @deps_to_tag; @@ -356,63 +405,85 @@ while (1) { } } - if ($cmd eq "A") { - $top_deps = top_deps(); - $git_info = deps_git_info(); - my $sub_deps = sub_deps(); + my $changelog_updated = 0; - for my $dep (keys %$top_deps) { - for my $sdep (keys %{$sub_deps->{$dep}}) { - next if not defined $top_deps->{$sdep} or - $sub_deps->{$dep}->{$sdep}->{commit} eq $top_deps->{$sdep}->{commit}; - say "$dep $sdep ",$sub_deps->{$dep}->{$sdep}->{commit}," <=> $sdep ",$top_deps->{$sdep}->{commit}; - schedule_operation("update", $dep, $git_info->{$dep}->{new_tag}, - "Updating $sdep to version $top_deps->{$sdep}->{commit}.", [$sdep, $top_deps->{$sdep}->{commit}]); + if ($cmd eq "A") { + APPLY: { + $top_deps = top_deps(); + $git_info = deps_git_info(); + my $sub_deps = sub_deps(); + + for my $dep (keys %$top_deps) { + for my $sdep (keys %{$sub_deps->{$dep}}) { + next if not defined $top_deps->{$sdep} or + $sub_deps->{$dep}->{$sdep}->{commit} eq $top_deps->{$sdep}->{commit}; + say "$dep $sdep ", $sub_deps->{$dep}->{$sdep}->{commit}, " <=> $sdep ", + $top_deps->{$sdep}->{commit}; + schedule_operation("update", $dep, $git_info->{$dep}->{new_tag}, + "Updating $sdep to version $top_deps->{$sdep}->{commit}.", + [ $sdep, $top_deps->{$sdep}->{commit} ]); + } } - } - - %info_updates = (); - %top_deps_updates = (); - %sub_deps_updates = (); - $top_deps = top_deps(); - $git_info = deps_git_info(); - $sub_deps = sub_deps(); + %info_updates = (); + %top_deps_updates = (); + %sub_deps_updates = (); - print color("bold blue"), "List of operations:\n", color("reset"); - for my $op (@operations) { - print color("red"), $op->{dep}, color("reset"), " ($top_deps->{$op->{dep}}->{commit} -> $op->{version})"; - if (@{$op->{operations}}) { - say ":"; - say " $_->[0] -> $_->[1]" for @{$op->{operations}}; - } else { - say ""; - } - } + $top_deps = top_deps(); + $git_info = deps_git_info(); + $sub_deps = sub_deps(); - say ""; - my $cmd = show_commands(A => "Apply", E => "Exit"); - if ($cmd eq "A") { - my %top_changes; + print color("bold blue"), "List of operations:\n", color("reset"); for my $op (@operations) { - update_changelog($op->{dep}, $op->{version}, @{$op->{reasons}}) - if @{$op->{reasons}}; - update_deps_versions(".deps-update/$op->{dep}/rebar.config", unpairs(@{$op->{operations}})) - if @{$op->{operations}}; - if ($git_info->{$op->{dep}}->{last_tag} ne $op->{version}) { - update_app_src($op->{dep}, $op->{version}); - git_tag($op->{dep}, $op->{version}, "Release $op->{version}"); + print color("red"), $op->{dep}, color("reset"), + " ($top_deps->{$op->{dep}}->{commit} -> $op->{version})"; + if (@{$op->{operations}}) { + say ":"; + say " $_->[0] -> $_->[1]" for @{$op->{operations}}; } + else { + say ""; + } + } - $top_changes{$op->{dep}} = $op->{version}; + say ""; + my %to_tag; + if (not $changelog_updated) { + for my $op (@operations) { + if ($git_info->{$op->{dep}}->{last_tag} ne $op->{version}) { + $to_tag{$op->{dep}} = $op->{version}; + } + } } - update_deps_versions("rebar.config", %top_changes); - for my $op (@operations) { - if ($git_info->{$op->{dep}}->{last_tag} ne $op->{version}) { - git_push($op->{dep}); + my $cmd = show_commands(A => "Apply", (%to_tag ? (U => "Update Changelogs") : ()), E => "Exit"); + if ($cmd eq "U") { + for my $dep (keys %to_tag) { + edit_changelog($dep, $to_tag{$dep}); + } + redo APPLY; + } + elsif ($cmd eq "A") { + my %top_changes; + for my $op (@operations) { + update_changelog($op->{dep}, $op->{version}, @{$op->{reasons}}) + if @{$op->{reasons}}; + update_deps_versions(".deps-update/$op->{dep}/rebar.config", unpairs(@{$op->{operations}})) + if @{$op->{operations}}; + if ($git_info->{$op->{dep}}->{last_tag} ne $op->{version}) { + update_app_src($op->{dep}, $op->{version}); + git_tag($op->{dep}, $op->{version}, "Release $op->{version}"); + } + + $top_changes{$op->{dep}} = $op->{version}; + } + update_deps_versions("rebar.config", %top_changes); + for my $op (@operations) { + if ($git_info->{$op->{dep}}->{last_tag} ne $op->{version}) { + git_push($op->{dep}); + } } + last MAIN; } - last; } } } |