diff options
author | Konstantinos Kallas <konstantinos.kallas@hotmail.com> | 2017-07-26 09:54:23 +0300 |
---|---|---|
committer | Konstantinos Kallas <konstantinos.kallas@hotmail.com> | 2017-07-26 09:54:23 +0300 |
commit | 61d1411ab3321b437f6741703405860d18c82b93 (patch) | |
tree | 44481834335fb872eee63e205e81417fa26249d5 | |
parent | Add an erl_opt so that rsa can be used when the otp version is enough (diff) | |
parent | Update oauth2 dependency (diff) |
Sync fork with upstream
46 files changed, 1157 insertions, 1401 deletions
diff --git a/ejabberd.yml.example b/ejabberd.yml.example index 7a75c5533..85754e1bb 100644 --- a/ejabberd.yml.example +++ b/ejabberd.yml.example @@ -158,12 +158,12 @@ listen: ip: "::" module: ejabberd_http request_handlers: - "/websocket": ejabberd_http_ws + "/ws": ejabberd_http_ws + "/bosh": mod_bosh "/api": mod_http_api "/.well-known": acme_challenge ## "/pub/archive": mod_http_fileserver web_admin: true - http_bind: true ## register: true captcha: true ## @@ -266,12 +266,12 @@ listen: ## Outgoing S2S options ## ## Preferred address families (which to try first) and connect timeout -## in milliseconds. +## in seconds. ## ## outgoing_s2s_families: ## - ipv4 ## - ipv6 -## outgoing_s2s_timeout: 10000 +## outgoing_s2s_timeout: 190 ###. ============== ###' AUTHENTICATION diff --git a/ejabberdctl.template b/ejabberdctl.template index 26719cc26..edf9bea0d 100755 --- a/ejabberdctl.template +++ b/ejabberdctl.template @@ -10,26 +10,33 @@ FIREWALL_WINDOW="" ERLANG_NODE=ejabberd@localhost # define default environment variables -ERL={{erl}} -IEX={{bindir}}/iex -EPMD={{epmd}} -INSTALLUSER={{installuser}} +ERL="{{erl}}" +IEX="{{bindir}}/iex" +EPMD="{{epmd}}" +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,19 +110,19 @@ 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 } exec_erl() { NODE=$1; shift - exec_cmd $ERL ${S:--}name $NODE $ERLANG_OPTS "$@" + exec_cmd "$ERL" ${S:--}name $NODE $ERLANG_OPTS "$@" } exec_iex() { NODE=$1; shift - exec_cmd $IEX ${S:---}name $NODE --erl "$ERLANG_OPTS" "$@" + exec_cmd "$IEX" ${S:---}name $NODE --erl "$ERLANG_OPTS" "$@" } # usage @@ -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) @@ -287,7 +295,7 @@ case $1 in ping) PEER=${2:-$ERLANG_NODE} [ "$PEER" = "${PEER%.*}" ] && PS="-s" - exec_cmd $ERL ${PS:--}name $(uid ping $(hostname $PS)) $ERLANG_OPTS \ + exec_cmd "$ERL" ${PS:--}name $(uid ping $(hostname $PS)) $ERLANG_OPTS \ -noinput -hidden -eval 'io:format("~p~n",[net_adm:ping('"$PEER"')])' \ -s erlang halt -output text ;; diff --git a/rebar.config b/rebar.config index 2e0220633..d24236cee 100644 --- a/rebar.config +++ b/rebar.config @@ -18,20 +18,21 @@ %%% %%%---------------------------------------------------------------------- -{deps, [{lager, ".*", {git, "https://github.com/erlang-lager/lager", {tag, "3.4.2"}}}, +{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.8"}}}, - {fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.0.12"}}}, + {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.11"}}}, + {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"}}}, {jose, ".*", {git, "git://github.com/potatosalad/erlang-jose.git", {branch, "master"}}}, - {if_var_true, stun, {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.11"}}}}, - {if_var_true, sip, {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.12"}}}}, + {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", @@ -39,7 +40,7 @@ {if_var_true, sqlite, {sqlite3, ".*", {git, "https://github.com/processone/erlang-sqlite3", {tag, "1.1.5"}}}}, {if_var_true, pam, {epam, ".*", {git, "https://github.com/processone/epam", - {tag, "1.0.2"}}}}, + {tag, "1.0.3"}}}}, {if_var_true, zlib, {ezlib, ".*", {git, "https://github.com/processone/ezlib", {tag, "1.0.2"}}}}, {if_var_true, riak, {riakc, ".*", {git, "https://github.com/basho/riak-erlang-client", @@ -71,6 +72,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 8f374a440..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]). @@ -67,7 +67,10 @@ start(SockData, Opts) -> case proplists:get_value(supervisor, Opts, true) of true -> - supervisor:start_child(ejabberd_c2s_sup, [SockData, Opts]); + case supervisor:start_child(ejabberd_c2s_sup, [SockData, Opts]) of + {ok, undefined} -> ignore; + Res -> Res + end; _ -> xmpp_stream_in:start(?MODULE, [SockData, Opts], ejabberd_config:fsm_limit_opts(Opts)) @@ -94,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). @@ -522,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 fc921c066..f90b70bce 100644 --- a/src/ejabberd_commands_doc.erl +++ b/src/ejabberd_commands_doc.erl @@ -69,9 +69,9 @@ list_join_with([El|Tail], M) -> end, [El], Tail)). md_tag(dt, V) -> - [<<"\n">>, V, <<"\n">>]; + [<<"- ">>, V]; md_tag(dd, V) -> - [<<"\n: ">>, V, <<"\n">>]; + [<<" : ">>, V, <<"\n">>]; md_tag(li, V) -> [<<"- ">>, V, <<"\n">>]; md_tag(pre, V) -> @@ -87,14 +87,6 @@ md_tag(strong, V) -> md_tag(_, V) -> V. - -%% rescode_to_int(ok) -> -%% 0; -%% rescode_to_int(true) -> -%% 0; -%% rescode_to_int(_) -> -%% 1. - perl_gen({Name, integer}, Int, _Indent, HTMLOutput) -> [?ARG(Name), ?OP_L(" => "), ?NUM(Int)]; perl_gen({Name, string}, Str, _Indent, HTMLOutput) -> @@ -257,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">>; @@ -324,55 +316,94 @@ gen_calls(#ejabberd_commands{args_example=Values, args=ArgsDesc, case lists:member(<<"xmlrpc">>, Langs) of true -> ?TAG(li, ?TAG(pre, XML)); _ -> [] end, case lists:member(<<"json">>, Langs) of true -> ?TAG(li, ?TAG(pre, JSON)); _ -> [] end])]; true -> - [<<"\n">>, case lists:member(<<"java">>, Langs) of true -> <<"* Java\n">>; _ -> [] end, - case lists:member(<<"perl">>, Langs) of true -> <<"* Perl\n">>; _ -> [] end, - case lists:member(<<"xmlrpc">>, Langs) of true -> <<"* XmlRPC\n">>; _ -> [] end, - case lists:member(<<"json">>, Langs) of true -> <<"* JSON\n">>; _ -> [] end, - <<"{: .code-samples-labels}\n">>, - case lists:member(<<"java">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, Java), <<"~~~\n">>]; _ -> [] end, - case lists:member(<<"perl">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, Perl), <<"~~~\n">>]; _ -> [] end, - case lists:member(<<"xmlrpc">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, XML), <<"~~~\n">>]; _ -> [] end, - case lists:member(<<"json">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, JSON), <<"~~~\n">>]; _ -> [] end, - <<"{: .code-samples-tabs}\n\n">>] + case Langs of + Val when length(Val) == 0 orelse length(Val) == 1 -> + [case lists:member(<<"java">>, Langs) of true -> [<<"\n">>, ?TAG(pre, Java), <<"~~~\n">>]; _ -> [] end, + case lists:member(<<"perl">>, Langs) of true -> [<<"\n">>, ?TAG(pre, Perl), <<"~~~\n">>]; _ -> [] end, + case lists:member(<<"xmlrpc">>, Langs) of true -> [<<"\n">>, ?TAG(pre, XML), <<"~~~\n">>]; _ -> [] end, + case lists:member(<<"json">>, Langs) of true -> [<<"\n">>, ?TAG(pre, JSON), <<"~~~\n">>]; _ -> [] end, + <<"\n\n">>]; + _ -> + [<<"\n">>, case lists:member(<<"java">>, Langs) of true -> <<"* Java\n">>; _ -> [] end, + case lists:member(<<"perl">>, Langs) of true -> <<"* Perl\n">>; _ -> [] end, + case lists:member(<<"xmlrpc">>, Langs) of true -> <<"* XmlRPC\n">>; _ -> [] end, + case lists:member(<<"json">>, Langs) of true -> <<"* JSON\n">>; _ -> [] end, + <<"{: .code-samples-labels}\n">>, + case lists:member(<<"java">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, Java), <<"~~~\n">>]; _ -> [] end, + case lists:member(<<"perl">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, Perl), <<"~~~\n">>]; _ -> [] end, + case lists:member(<<"xmlrpc">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, XML), <<"~~~\n">>]; _ -> [] end, + case lists:member(<<"json">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, JSON), <<"~~~\n">>]; _ -> [] end, + <<"{: .code-samples-tabs}\n\n">>] + 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(format_type(Type))])]; +gen_param(Name, Type, Desc, HTMLOutput) -> + [?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", lists:map(fun({AName, Type}) -> - [?TAG(li, [?TAG_R(strong, atom_to_list(AName)), <<" :: ">>, - ?RAW(io_lib:format("~p", [Type]))])] - end, Args))]; - _ -> - [?TAG(dl, "args-list", lists:map(fun({{AName, Type}, ADesc}) -> - [?TAG(dt, [?TAG_R(strong, atom_to_list(AName)), <<" :: ">>, - ?RAW(io_lib:format("~p", [Type]))]), - ?TAG(dd, ?RAW(ADesc))] - end, lists:zip(Args, ArgsDesc)))] - end, - ResultText = case ResultDesc of - none -> - [?RAW(io_lib:format("~p", [Result]))]; - _ -> - [?TAG(dl, [ - ?TAG(dt, io_lib:format("~p", [Result])), - ?TAG_R(dd, ResultDesc)])] - end, - - [?TAG(h1, [?TAG(strong, atom_to_list(Name)), <<" - ">>, ?RAW(Desc)]), - ?TAG(p, ?RAW(LDesc)), - ?TAG(h2, <<"Arguments:">>), - ArgsText, - ?TAG(h2, <<"Result:">>), - ResultText, - ?TAG(h2, <<"Examples:">>), - gen_calls(Cmd, HTMLOutput, Langs)]. + 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, + ?TAG(h2, <<"Result:">>), ResultText, + ?TAG(h2, <<"Examples:">>), gen_calls(Cmd, HTMLOutput, Langs)] + catch + _:Ex -> + throw(iolist_to_binary(io_lib:format( + <<"Error when generating documentation for command '~p': ~p">>, + [Name, Ex]))) + end. find_commands_definitions() -> case code:lib_dir(ejabberd, ebin) of diff --git a/src/ejabberd_http_ws.erl b/src/ejabberd_http_ws.erl index 18ba071dd..f4a73cc39 100644 --- a/src/ejabberd_http_ws.erl +++ b/src/ejabberd_http_ws.erl @@ -81,8 +81,14 @@ start_link(WS) -> gen_fsm:start_link(?MODULE, [WS], ?FSMOPTS). send_xml({http_ws, FsmRef, _IP}, Packet) -> - gen_fsm:sync_send_all_state_event(FsmRef, - {send_xml, Packet}). + case catch gen_fsm:sync_send_all_state_event(FsmRef, + {send_xml, Packet}, + 15000) + of + {'EXIT', {timeout, _}} -> {error, timeout}; + {'EXIT', _} -> {error, einval}; + Res -> Res + end. setopts({http_ws, FsmRef, _IP}, Opts) -> case lists:member({active, once}, Opts) of 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_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_s2s_in.erl b/src/ejabberd_s2s_in.erl index 76a844f87..48a650a4e 100644 --- a/src/ejabberd_s2s_in.erl +++ b/src/ejabberd_s2s_in.erl @@ -57,7 +57,10 @@ start(SockData, Opts) -> case proplists:get_value(supervisor, Opts, true) of true -> - supervisor:start_child(ejabberd_s2s_in_sup, [SockData, Opts]); + case supervisor:start_child(ejabberd_s2s_in_sup, [SockData, Opts]) of + {ok, undefined} -> ignore; + Res -> Res + end; _ -> xmpp_stream_in:start(?MODULE, [SockData, Opts], ejabberd_config:fsm_limit_opts(Opts)) diff --git a/src/ejabberd_s2s_out.erl b/src/ejabberd_s2s_out.erl index bcacd8e77..e8cad9792 100644 --- a/src/ejabberd_s2s_out.erl +++ b/src/ejabberd_s2s_out.erl @@ -54,10 +54,13 @@ %%%=================================================================== start(From, To, Opts) -> case proplists:get_value(supervisor, Opts, true) of - true -> - supervisor:start_child(ejabberd_s2s_out_sup, - [From, To, Opts]); - _ -> + true -> + case supervisor:start_child(ejabberd_s2s_out_sup, + [From, To, Opts]) of + {ok, undefined} -> ignore; + Res -> Res + end; + _ -> xmpp_stream_out:start(?MODULE, [ejabberd_socket, From, To, Opts], ejabberd_config:fsm_limit_opts([])) end. 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_socket.erl b/src/ejabberd_socket.erl index e1712be2f..9953a76ae 100644 --- a/src/ejabberd_socket.erl +++ b/src/ejabberd_socket.erl @@ -87,7 +87,7 @@ %% API %%==================================================================== -spec start(atom(), sockmod(), socket(), [proplists:property()]) - -> {ok, pid() | independent} | {error, inet:posix() | any()}. + -> {ok, pid() | independent} | {error, inet:posix() | any()} | ignore. start(Module, SockMod, Socket, Opts) -> case Module:socket_type() of independent -> {ok, independent}; 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 c5ce6874f..80296f7b7 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, integer}}, + 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,32 +111,37 @@ 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, integer}}, + 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, integer}}, + 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, integer}}, + 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, integer}} + result = {res, rescode}} ]. %% -- public modules functions diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl index df2206d8d..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()}), @@ -1401,11 +1507,11 @@ srg_get_members(Group, Host) -> || {MUser, MServer} <- Members]. srg_user_add(User, Host, Group, GroupHost) -> - {atomic, _} = mod_shared_roster:add_user_to_group(GroupHost, {User, Host}, Group), + mod_shared_roster:add_user_to_group(GroupHost, {User, Host}, Group), ok. srg_user_del(User, Host, Group, GroupHost) -> - {atomic, _} = mod_shared_roster:remove_user_from_group(GroupHost, {User, Host}, Group), + mod_shared_roster:remove_user_from_group(GroupHost, {User, Host}, Group), ok. diff --git a/src/mod_block_strangers.erl b/src/mod_block_strangers.erl index 636d5077b..49d79e043 100644 --- a/src/mod_block_strangers.erl +++ b/src/mod_block_strangers.erl @@ -58,9 +58,12 @@ filter_packet({#message{} = Msg, State} = Acc) -> LFrom = jid:tolower(From), LBFrom = jid:remove_resource(LFrom), #{pres_a := PresA, jid := JID, lserver := LServer} = State, + AllowLocalUsers = + gen_mod:get_module_opt(LServer, ?MODULE, allow_local_users, true), case (Msg#message.body == [] andalso Msg#message.subject == []) - orelse ejabberd_router:is_my_route(From#jid.lserver) + orelse (AllowLocalUsers andalso + ejabberd_router:is_my_route(From#jid.lserver)) orelse (?SETS):is_element(LFrom, PresA) orelse (?SETS):is_element(LBFrom, PresA) orelse sets_bare_member(LBFrom, PresA) of @@ -128,4 +131,6 @@ mod_opt_type(drop) -> fun (B) when is_boolean(B) -> B end; mod_opt_type(log) -> fun (B) when is_boolean(B) -> B end; -mod_opt_type(_) -> [drop, log]. +mod_opt_type(allow_local_users) -> + fun (B) when is_boolean(B) -> B end; +mod_opt_type(_) -> [drop, log, allow_local_users]. diff --git a/src/mod_client_state.erl b/src/mod_client_state.erl index f2fcb37ee..efe6a260f 100644 --- a/src/mod_client_state.erl +++ b/src/mod_client_state.erl @@ -199,8 +199,8 @@ c2s_authenticated_packet(C2SState, _) -> C2SState. -spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state(). -c2s_copy_session(C2SState, #{csi_state := State, csi_queue := Q}) -> - C2SState#{csi_state => State, csi_queue => Q}; +c2s_copy_session(C2SState, #{csi_queue := Q}) -> + C2SState#{csi_queue => Q}; c2s_copy_session(C2SState, _) -> C2SState. 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_mam_sql.erl b/src/mod_mam_sql.erl index fae14955f..7e02b5791 100644 --- a/src/mod_mam_sql.erl +++ b/src/mod_mam_sql.erl @@ -198,15 +198,14 @@ export(_Server) -> [] end}, {archive_msg, - fun(Host, #archive_msg{us ={_LUser, LServer}, + fun(Host, #archive_msg{us ={LUser, LServer}, id = _ID, timestamp = TS, peer = Peer, - bare_peer = {PUser, PServer, <<>>}, type = Type, nick = Nick, packet = Pkt}) when LServer == Host -> TStmp = now_to_usec(TS), SUser = case Type of - chat -> PUser; - groupchat -> jid:encode({PUser, PServer, <<>>}) + chat -> LUser; + groupchat -> jid:encode({LUser, LServer, <<>>}) end, BarePeer = jid:encode(jid:tolower(jid:remove_resource(Peer))), LPeer = jid:encode(jid:tolower(Peer)), diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index 8726856b5..2d1e66ba5 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -208,8 +208,11 @@ get_commands_spec() -> module = ?MODULE, function = send_direct_invitation, args_desc = ["Room name", "MUC service", "Password, or none", "Reason text, or none", "Users JIDs separated with : characters"], - args_example = ["room1", "muc.example.com", none, none, "user2@localhost:user3@example.com"], - args = [{name, binary}, {service, binary}, {password, binary}, {reason, binary}, {users, binary}], + args_example = [<<"room1">>, <<"muc.example.com">>, + <<>>, <<"Check this out!">>, + "user2@localhost:user3@example.com"], + args = [{name, binary}, {service, binary}, {password, binary}, + {reason, binary}, {users, binary}], result = {res, rescode}}, #ejabberd_commands{name = change_room_option, tags = [muc_room], @@ -277,7 +280,7 @@ get_commands_spec() -> args_desc = ["Room name", "MUC service"], args_example = ["room1", "muc.example.com"], result_desc = "The list of affiliations with username, domain, affiliation and reason", - result_example = [{"user1", "example.com", "member"}], + result_example = [{"user1", "example.com", member, "member"}], args = [{name, binary}, {service, binary}], result = {affiliations, {list, {affiliation, {tuple, @@ -1009,7 +1012,7 @@ set_room_affiliation(Name, Service, JID, AffiliationString) -> case mod_muc:find_online_room(Name, Service) of {ok, Pid} -> %% Get the PID for the online room so we can get the state of the room - {ok, StateData} = gen_fsm:sync_send_all_state_event(Pid, {process_item_change, {jid:decode(JID), affiliation, Affiliation, <<"">>}, <<"">>}), + {ok, StateData} = gen_fsm:sync_send_all_state_event(Pid, {process_item_change, {jid:decode(JID), affiliation, Affiliation, <<"">>}, undefined}), mod_muc:store_room(StateData#state.server_host, StateData#state.host, StateData#state.room, make_opts(StateData)), ok; error -> diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 149e6c221..ec1cffd6a 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -2608,7 +2608,7 @@ process_item_change(UJID) -> -type admin_action() :: {jid(), affiliation | role, affiliation() | role(), binary()}. --spec process_item_change(admin_action(), state(), jid()) -> state() | {error, stanza_error()}. +-spec process_item_change(admin_action(), state(), undefined | jid()) -> state() | {error, stanza_error()}. process_item_change(Item, SD, UJID) -> try case Item of {JID, affiliation, owner, _} when JID#jid.luser == <<"">> -> @@ -2658,8 +2658,15 @@ process_item_change(Item, SD, UJID) -> SD1 end catch E:R -> - ?ERROR_MSG("failed to set item ~p from ~s: ~p", - [Item, jid:encode(UJID), + FromSuffix = case UJID of + #jid{} -> + JidString = jid:encode(UJID), + <<" from ", JidString/binary>>; + undefined -> + <<"">> + end, + ?ERROR_MSG("failed to set item ~p~s: ~p", + [Item, FromSuffix, {E, {R, erlang:get_stacktrace()}}]), {error, xmpp:err_internal_server_error()} end. diff --git a/src/mod_multicast.erl b/src/mod_multicast.erl index d2627c252..4d3e481a7 100644 --- a/src/mod_multicast.erl +++ b/src/mod_multicast.erl @@ -998,9 +998,8 @@ build_service_limit_record(LimitOpts) -> build_limit_record(LimitOptsR, remote)}. get_from_limitopts(LimitOpts, SenderT) -> - [{StanzaT, Number} - || {SenderT2, StanzaT, Number} <- LimitOpts, - SenderT =:= SenderT2]. + {SenderT, Result} = lists:keyfind(SenderT, 1, LimitOpts), + Result. build_remote_limit_record(LimitOpts, SenderT) -> build_limit_record(LimitOpts, SenderT). @@ -1118,6 +1117,13 @@ depends(_Host, _Opts) -> mod_opt_type(access) -> fun acl:access_rules_validator/1; mod_opt_type(host) -> fun iolist_to_binary/1; -mod_opt_type(limits) -> - fun (A) when is_list(A) -> A end; -mod_opt_type(_) -> [access, host, limits]. +mod_opt_type({limits, Type}) when (Type == local) or (Type == remote) -> + fun(L) -> + lists:map( + fun ({message, infinite}) -> infinite; + ({presence, infinite}) -> infinite; + ({message, I}) when is_integer(I) -> I; + ({presence, I}) when is_integer(I) -> I + end, L) + end; +mod_opt_type(_) -> [access, host, {limits, local}, {limits, remote}]. diff --git a/src/mod_offline.erl b/src/mod_offline.erl index 2c2c6185a..0be61f71f 100644 --- a/src/mod_offline.erl +++ b/src/mod_offline.erl @@ -671,7 +671,7 @@ user_queue(User, Server, Query, Lang) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Res = user_queue_parse_query(LUser, LServer, Query), HdrsAll = Mod:read_message_headers(LUser, LServer), - Hdrs = get_messages_subset(US, Server, HdrsAll), + Hdrs = get_messages_subset(User, Server, HdrsAll), FMsgs = format_user_queue(Hdrs), [?XC(<<"h1">>, (str:format(?T(<<"~s's Offline Messages Queue">>), diff --git a/src/mod_offline_sql.erl b/src/mod_offline_sql.erl index bcf5b7aad..f43f4c929 100644 --- a/src/mod_offline_sql.erl +++ b/src/mod_offline_sql.erl @@ -181,6 +181,13 @@ count_messages(LUser, LServer) -> export(_Server) -> [{offline_msg, + fun(Host, #offline_msg{us = {LUser, LServer}}) + when LServer == Host -> + [?SQL("delete from spool where username=%(LUser)s;")]; + (_Host, _R) -> + [] + end}, + {offline_msg, fun(Host, #offline_msg{us = {LUser, LServer}, timestamp = TimeStamp, from = From, to = To, packet = El}) @@ -192,8 +199,7 @@ export(_Server) -> Packet1, jid:make(LServer), TimeStamp, <<"Offline Storage">>), XML = fxml:element_to_binary(xmpp:encode(Packet2)), - [?SQL("delete from spool where username=%(LUser)s;"), - ?SQL("insert into spool(username, xml) values (" + [?SQL("insert into spool(username, xml) values (" "%(LUser)s, %(XML)s);")] catch _:{xmpp_codec, Why} -> ?ERROR_MSG("failed to decode packet ~p of user ~s@~s: ~s", 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 91d2f928a..1a620cb6b 100644 --- a/src/mod_pubsub.erl +++ b/src/mod_pubsub.erl @@ -3009,60 +3009,61 @@ c2s_handle_info(C2SState, _) -> subscribed_nodes_by_jid(NotifyType, SubsByDepth) -> NodesToDeliver = fun (Depth, Node, Subs, Acc) -> - NodeName = case Node#pubsub_node.nodeid of - {_, N} -> N; - Other -> Other - end, - NodeOptions = Node#pubsub_node.options, - lists:foldl(fun({LJID, SubID, SubOptions}, {JIDs, Recipients}) -> - case is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) of - true -> - case state_can_deliver(LJID, SubOptions) of - [] -> {JIDs, Recipients}; - JIDsToDeliver -> - lists:foldl( - fun(JIDToDeliver, {JIDsAcc, RecipientsAcc}) -> - case lists:member(JIDToDeliver, JIDs) of - %% check if the JIDs co-accumulator contains the Subscription Jid, - false -> - %% - if not, - %% - add the Jid to JIDs list co-accumulator ; - %% - create a tuple of the Jid, Nidx, and SubID (as list), - %% and add the tuple to the Recipients list co-accumulator - {[JIDToDeliver | JIDsAcc], - [{JIDToDeliver, NodeName, [SubID]} - | RecipientsAcc]}; - true -> - %% - if the JIDs co-accumulator contains the Jid - %% get the tuple containing the Jid from the Recipient list co-accumulator - {_, {JIDToDeliver, NodeName1, SubIDs}} = - lists:keysearch(JIDToDeliver, 1, RecipientsAcc), - %% delete the tuple from the Recipients list - % v1 : Recipients1 = lists:keydelete(LJID, 1, Recipients), - % v2 : Recipients1 = lists:keyreplace(LJID, 1, Recipients, {LJID, Nidx1, [SubID | SubIDs]}), - %% add the SubID to the SubIDs list in the tuple, - %% and add the tuple back to the Recipients list co-accumulator - % v1.1 : {JIDs, lists:append(Recipients1, [{LJID, Nidx1, lists:append(SubIDs, [SubID])}])} - % v1.2 : {JIDs, [{LJID, Nidx1, [SubID | SubIDs]} | Recipients1]} - % v2: {JIDs, Recipients1} - {JIDsAcc, - lists:keyreplace(JIDToDeliver, 1, - RecipientsAcc, - {JIDToDeliver, NodeName1, - [SubID | SubIDs]})} - end - end, {JIDs, Recipients}, JIDsToDeliver) - end; - false -> - {JIDs, Recipients} - end - end, Acc, Subs) - end, + NodeName = case Node#pubsub_node.nodeid of + {_, N} -> N; + Other -> Other + end, + NodeOptions = Node#pubsub_node.options, + lists:foldl(fun({LJID, SubID, SubOptions}, {JIDs, Recipients}) -> + case is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) of + true -> + case state_can_deliver(LJID, SubOptions) of + [] -> {JIDs, Recipients}; + [LJID] -> {JIDs, [{LJID, NodeName, [SubID]} | Recipients]}; + JIDsToDeliver -> + lists:foldl( + fun(JIDToDeliver, {JIDsAcc, RecipientsAcc}) -> + case lists:member(JIDToDeliver, JIDs) of + %% check if the JIDs co-accumulator contains the Subscription Jid, + false -> + %% - if not, + %% - add the Jid to JIDs list co-accumulator ; + %% - create a tuple of the Jid, Nidx, and SubID (as list), + %% and add the tuple to the Recipients list co-accumulator + {[JIDToDeliver | JIDsAcc], + [{JIDToDeliver, NodeName, [SubID]} + | RecipientsAcc]}; + true -> + %% - if the JIDs co-accumulator contains the Jid + %% get the tuple containing the Jid from the Recipient list co-accumulator + {_, {JIDToDeliver, NodeName1, SubIDs}} = + lists:keysearch(JIDToDeliver, 1, RecipientsAcc), + %% delete the tuple from the Recipients list + % v1 : Recipients1 = lists:keydelete(LJID, 1, Recipients), + % v2 : Recipients1 = lists:keyreplace(LJID, 1, Recipients, {LJID, Nidx1, [SubID | SubIDs]}), + %% add the SubID to the SubIDs list in the tuple, + %% and add the tuple back to the Recipients list co-accumulator + % v1.1 : {JIDs, lists:append(Recipients1, [{LJID, Nidx1, lists:append(SubIDs, [SubID])}])} + % v1.2 : {JIDs, [{LJID, Nidx1, [SubID | SubIDs]} | Recipients1]} + % v2: {JIDs, Recipients1} + {JIDsAcc, + lists:keyreplace(JIDToDeliver, 1, + RecipientsAcc, + {JIDToDeliver, NodeName1, + [SubID | SubIDs]})} + end + end, {JIDs, Recipients}, JIDsToDeliver) + end; + false -> + {JIDs, Recipients} + end + end, Acc, Subs) + end, DepthsToDeliver = fun({Depth, SubsByNode}, Acc1) -> - lists:foldl(fun({Node, Subs}, Acc2) -> - NodesToDeliver(Depth, Node, Subs, Acc2) - end, Acc1, SubsByNode) - end, + lists:foldl(fun({Node, Subs}, Acc2) -> + NodesToDeliver(Depth, Node, Subs, Acc2) + end, Acc1, SubsByNode) + end, {_, JIDSubs} = lists:foldl(DepthsToDeliver, {[], []}, SubsByDepth), JIDSubs. diff --git a/src/mod_stream_mgmt.erl b/src/mod_stream_mgmt.erl index 7b546465e..127eea3e8 100644 --- a/src/mod_stream_mgmt.erl +++ b/src/mod_stream_mgmt.erl @@ -441,6 +441,7 @@ update_num_stanzas_in(#{mgmt_state := MgmtState, update_num_stanzas_in(State, _El) -> State. +-spec send_rack(state()) -> state(). send_rack(#{mgmt_ack_timer := _} = State) -> State; send_rack(#{mgmt_xmlns := Xmlns, @@ -450,6 +451,7 @@ send_rack(#{mgmt_xmlns := Xmlns, State1 = State#{mgmt_ack_timer => TRef, mgmt_stanzas_req => NumStanzasOut}, send(State1, #sm_r{xmlns = Xmlns}). +-spec resend_rack(state()) -> state(). resend_rack(#{mgmt_ack_timer := _, mgmt_queue := Queue, mgmt_stanzas_out := NumStanzasOut, diff --git a/src/prosody2ejabberd.erl b/src/prosody2ejabberd.erl index 312a177be..c60f2c24b 100644 --- a/src/prosody2ejabberd.erl +++ b/src/prosody2ejabberd.erl @@ -50,7 +50,7 @@ from_dir(ProsodyDir) -> convert_dir(Path, Host, SubDir) end, ["vcard", "accounts", "roster", "private", "config", "offline", - "privacy"]) + "privacy", "pubsub"]) end, HostDirs); {error, Why} = Err -> ?ERROR_MSG("failed to list ~s: ~s", @@ -115,7 +115,7 @@ maybe_get_scram_auth(Data) -> #scram{ storedkey = misc:hex_to_base64(proplists:get_value(<<"stored_key">>, Data, <<"">>)), serverkey = misc:hex_to_base64(proplists:get_value(<<"server_key">>, Data, <<"">>)), - salt = misc:hex_to_base64(proplists:get_value(<<"salt">>, Data, <<"">>)), + salt = base64:encode(proplists:get_value(<<"salt">>, Data, <<"">>)), iterationcount = round(IC) }; _ -> <<"">> @@ -212,6 +212,46 @@ convert_data(Host, "privacy", User, [Data]) -> end end, Lists)}, mod_privacy:set_list(Priv); +convert_data(PubSub, "pubsub", NodeId, [Data]) -> + Host = url_decode(PubSub), + Node = url_decode(NodeId), + Type = node_type(Host, Node), + NodeData = convert_node_config(Host, Data), + DefaultConfig = mod_pubsub:config(Host, default_node_config, []), + Owner = proplists:get_value(owner, NodeData), + Options = lists:foldl( + fun({_Opt, undefined}, Acc) -> + Acc; + ({Opt, Val}, Acc) -> + lists:keystore(Opt, 1, Acc, {Opt, Val}) + end, DefaultConfig, proplists:get_value(options, NodeData)), + case mod_pubsub:tree_action(Host, create_node, [Host, Node, Type, Owner, Options, []]) of + {ok, Nidx} -> + case mod_pubsub:node_action(Host, Type, create_node, [Nidx, Owner]) of + {result, _} -> + Access = open, % always allow subscriptions proplists:get_value(access_model, Options), + Publish = open, % always allow publications proplists:get_value(publish_model, Options), + MaxItems = proplists:get_value(max_items, Options), + Affiliations = proplists:get_value(affiliations, NodeData), + Subscriptions = proplists:get_value(subscriptions, NodeData), + Items = proplists:get_value(items, NodeData), + [mod_pubsub:node_action(Host, Type, set_affiliation, + [Nidx, Entity, Aff]) + || {Entity, Aff} <- Affiliations, Entity =/= Owner], + [mod_pubsub:node_action(Host, Type, subscribe_node, + [Nidx, jid:make(Entity), Entity, Access, never, [], [], []]) + || Entity <- Subscriptions], + [mod_pubsub:node_action(Host, Type, publish_item, + [Nidx, Publisher, Publish, MaxItems, ItemId, Payload, []]) + || {ItemId, Publisher, Payload} <- Items]; + Error -> + Error + end; + Error -> + ?ERROR_MSG("failed to import pubsub node ~s on host ~s:~n~p", + [Node, Host, NodeData]), + Error + end; convert_data(_Host, _Type, _User, _Data) -> ok. @@ -333,6 +373,88 @@ convert_privacy_item({_, Item}) -> match_presence_in = MatchPresIn, match_presence_out = MatchPresOut}. +url_decode(Encoded) -> + url_decode(Encoded, <<>>). +url_decode(<<$%, Hi, Lo, Tail/binary>>, Acc) -> + Hex = list_to_integer([Hi, Lo], 16), + url_decode(Tail, <<Acc/binary, Hex>>); +url_decode(<<H, Tail/binary>>, Acc) -> + url_decode(Tail, <<Acc/binary, H>>); +url_decode(<<>>, Acc) -> + Acc. + +node_type(_Host, <<"urn:", _Tail/binary>>) -> <<"pep">>; +node_type(_Host, <<"http:", _Tail/binary>>) -> <<"pep">>; +node_type(_Host, <<"https:", _Tail/binary>>) -> <<"pep">>; +node_type(Host, _) -> hd(mod_pubsub:plugins(Host)). + +max_items(Config, Default) -> + case round(proplists:get_value(<<"max_items">>, Config, Default)) of + I when I =< 0 -> Default; + I -> I + end. + +convert_node_affiliations(Data) -> + lists:flatmap( + fun({J, Aff}) -> + try jid:decode(J) of + JID -> + [{JID, misc:binary_to_atom(Aff)}] + catch _:{bad_jid, _} -> + [] + end + end, proplists:get_value(<<"affiliations">>, Data, [])). + +convert_node_subscriptions(Data) -> + lists:flatmap( + fun({J, true}) -> + try jid:decode(J) of + JID -> + [jid:tolower(JID)] + catch _:{bad_jid, _} -> + [] + end; + (_) -> + [] + end, proplists:get_value(<<"subscribers">>, Data, [])). + +convert_node_items(Host, Data) -> + Authors = proplists:get_value(<<"data_author">>, Data, []), + lists:flatmap( + fun({ItemId, Item}) -> + try catch jid:decode(proplists:get_value(ItemId, Authors, Host)) of + JID -> + [El] = deserialize(Item), + [{ItemId, JID, El#xmlel.children}] + catch _:{bad_jid, _} -> + [] + end + end, proplists:get_value(<<"data">>, Data, [])). + +convert_node_config(Host, Data) -> + Config = proplists:get_value(<<"config">>, Data, []), + [{affiliations, convert_node_affiliations(Data)}, + {subscriptions, convert_node_subscriptions(Data)}, + {owner, jid:decode(proplists:get_value(<<"creator">>, Config, Host))}, + {items, convert_node_items(Host, Data)}, + {options, [ + {deliver_notifications, + proplists:get_value(<<"deliver_notifications">>, Config, true)}, + {deliver_payloads, + proplists:get_value(<<"deliver_payloads">>, Config, true)}, + {persist_items, + proplists:get_value(<<"persist_items">>, Config, true)}, + {max_items, + max_items(Config, 10)}, + {access_model, + misc:binary_to_atom(proplists:get_value(<<"access_model">>, Config, <<"open">>))}, + {publish_model, + misc:binary_to_atom(proplists:get_value(<<"publish_model">>, Config, <<"publishers">>))}, + {title, + proplists:get_value(<<"title">>, Config, <<"">>)} + ]} + ]. + el_to_offline_msg(LUser, LServer, #xmlel{attrs = Attrs} = El) -> try TS = xmpp_util:decode_timestamp( 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/elixir_SUITE.erl b/test/elixir_SUITE.erl index f4612fa6d..5cc0c95a4 100644 --- a/test/elixir_SUITE.erl +++ b/test/elixir_SUITE.erl @@ -99,7 +99,7 @@ run_elixir_test(Func) -> 'Elixir.Code':load_file(list_to_binary(filename:join(test_dir(), atom_to_list(Func)))), %% I did not use map syntax, so that this file can still be build under R16 - 'Elixir.ExUnit.Server':cases_loaded(), + catch 'Elixir.ExUnit.Server':cases_loaded(), ResultMap = 'Elixir.ExUnit':run(), case maps:find(failures, ResultMap) of {ok, 0} -> diff --git a/test/ldap_srv.erl b/test/ldap_srv.erl index d5dc8dbe2..f601827c4 100644 --- a/test/ldap_srv.erl +++ b/test/ldap_srv.erl @@ -45,7 +45,7 @@ -define(ERROR_MSG(Fmt, Args), error_logger:error_msg(Fmt, Args)). -define(TCP_SEND_TIMEOUT, 32000). --define(SERVER, ?MODULE). +-define(SERVER, ?MODULE). -record(state, {listener = make_ref() :: reference()}). @@ -122,7 +122,7 @@ accept(ListenSocket, Tree) -> process(Socket, Tree) -> case gen_tcp:recv(Socket, 0) of {ok, B} -> - case asn1rt:decode('ELDAPv3', 'LDAPMessage', B) of + case 'ELDAPv3':decode('LDAPMessage', B) of {ok, Msg} -> Replies = process_msg(Msg, Tree), Id = Msg#'LDAPMessage'.messageID, @@ -131,8 +131,8 @@ process(Socket, Tree) -> Reply = #'LDAPMessage'{messageID = Id, protocolOp = ReplyOp}, %%?DEBUG("sent:~n~p", [Reply]), - {ok, Bytes} = asn1rt:encode( - 'ELDAPv3', 'LDAPMessage', Reply), + {ok, Bytes} = 'ELDAPv3':encode( + 'LDAPMessage', Reply), gen_tcp:send(Socket, Bytes) end, Replies), process(Socket, Tree); 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 586ae0fe4..3536ed101 100755 --- a/tools/update-deps-releases.pl +++ b/tools/update-deps-releases.pl @@ -22,17 +22,24 @@ sub get_deps { return { } unless $config =~ /\{\s*deps\s*,\s*\[(.*?)\]/s; my $sdeps = $1; - while ($sdeps =~ /\{\s*(\w+)\s*,\s*".*?"\s*,\s*\{\s*git\s*,\s*"(.*?)"\s*,\s*(?:{\s*tag\s*,\s*"(.*?)"|"(.*?)" )/sg) { + while ($sdeps =~ /\{\s* (\w+) \s*,\s* ".*?" \s*,\s* \{\s*git \s*,\s* "(.*?)" \s*,\s* + (?: + (?:{\s*tag \s*,\s* "(.*?)") | + "(.*?)" | + ( \{ (?: (?-1) | [^{}]+ )+ \} ) )/sgx) { next unless not %fdeps or exists $fdeps{$1}; $deps{$1} = { repo => $2, commit => $3 || $4 }; } 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; @@ -45,18 +52,20 @@ 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"; if (not -d $dd) { say "Downloading $dep..."; my $repo = $deps->{$dep}->{repo}; - $repo =~ s/^https?/git/; + $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, "fetch"); + system("git", "-C", $dd, "pull"); touch($dd) } } @@ -64,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"; @@ -90,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}}; @@ -109,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"; @@ -130,7 +176,7 @@ sub update_deps_versions { my $config = slurp $config_path; for (keys %deps) { - $config =~ s/(\{\s*$_\s*,\s*".*?"\s*,\s*\{\s*git\s*,\s*".*?"\s*,\s*)(?:{\s*tag\s*,\s*"(.*?)"\s*}|"(.*?)" )/$1\{tag, "$deps{$_}"}/s; + $config =~ s/(\{\s*$_\s*,\s*".*?"\s*,\s*\{\s*git\s*,\s*".*?"\s*,\s*)(?:{\s*tag\s*,\s*"(.*?)"\s*}|"(.*?)")/$1\{tag, "$deps{$_}"}/s; } write_file($config_path, $config); @@ -173,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"; @@ -218,10 +266,19 @@ sub schedule_operation { my $idx = first { $operations[$_]->{dep} eq $dep } 0..$#operations; if (defined $idx) { - push @{$operations[$idx]->{reasons}}, $reason; - push @{$operations[$idx]->{operations}}, $op; + my $mop = $operations[$idx]; + if (defined $op) { + my $oidx = first { $mop->{operations}->[$_]->[0] eq $op->[0] } 0..$#{$mop->{operations}}; + if (defined $oidx) { + $mop->{reasons}->[$oidx] = $reason; + $mop->{operations}->[$oidx] = $op; + } else { + push @{$mop->{reasons}}, $reason; + push @{$mop->{operations}}, $op; + } + } return if $type eq "update"; - $operations[$idx]->{type} = $type; + $mop->{type} = $type; $info_updates{$dep}->{new_commits} = []; return; } @@ -268,6 +325,7 @@ sub git_push { update_deps_repos(); +MAIN: while (1) { my $top_deps = top_deps(); my $git_info = deps_git_info(); @@ -295,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"; @@ -320,6 +379,9 @@ while (1) { } } + if ($cmd eq "R") { + update_deps_repos(1); + } if ($cmd eq "T") { while (1) { my @deps_to_tag; @@ -343,47 +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 $sub_deps->{$dep}->{$sdep}->{commit} eq $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(); - - 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}"); - } + %info_updates = (); + %top_deps_updates = (); + %sub_deps_updates = (); - $top_changes{$op->{dep}} = $op->{version}; - } - update_deps_versions("rebar.config", %top_changes); + $top_deps = top_deps(); + $git_info = deps_git_info(); + $sub_deps = sub_deps(); + + 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 ""; + } + } - for my $op (@operations) { - if ($git_info->{$op->{dep}}->{last_tag} ne $op->{version}) { - git_push($op->{dep}); + 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}; + } + } + } + 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; } } |