diff --git a/.travis.yml b/.travis.yml index 2b1c92190..6e1533a61 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ otp_release: - 17.5 - 18.3 - 19.2 + - 20.0 services: - riak diff --git a/Dockerfile b/Dockerfile index 6de6f5783..e5f3d78f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM debian:jessie-slim MAINTAINER Rafael Römhild -ENV EJABBERD_BRANCH=17.04 \ +ENV EJABBERD_BRANCH=17.08 \ EJABBERD_USER=ejabberd \ EJABBERD_HTTPS=true \ EJABBERD_STARTTLS=true \ diff --git a/README b/README index 67e062267..87b684f14 100644 --- a/README +++ b/README @@ -116,11 +116,14 @@ To compile ejabberd you need: needed on systems with GNU Libc. - ImageMagick's Convert program. Optional. For CAPTCHA challenges. +If your system splits packages in libraries and development headers, you must +install the development packages also. ### 1. Compile and install on *nix systems To compile ejabberd, execute the following commands. The first one is only -necessary if your source tree didn't come with a `configure` script. +necessary if your source tree didn't come with a `configure` script (In this +case you need autoconf installed). ./autogen.sh ./configure diff --git a/configure.ac b/configure.ac index 884db5d4e..edf54722c 100644 --- a/configure.ac +++ b/configure.ac @@ -3,8 +3,8 @@ AC_PREREQ(2.53) AC_INIT(ejabberd, m4_esyscmd([echo `git describe --tags 2>/dev/null || echo 0.0` | sed 's/-g.*//;s/-/./' | tr -d '\012']), [ejabberd@process-one.net], [ejabberd]) -REQUIRE_ERLANG_MIN="6.1 (Erlang/OTP 17.1)" -REQUIRE_ERLANG_MAX="9.0.0 (No Max)" +REQUIRE_ERLANG_MIN="6.4 (Erlang/OTP 17.5)" +REQUIRE_ERLANG_MAX="100.0.0 (No Max)" AC_CONFIG_MACRO_DIR([m4]) diff --git a/ejabberd.yml.example b/ejabberd.yml.example index ee3dda24c..72eb0b4b1 100644 --- a/ejabberd.yml.example +++ b/ejabberd.yml.example @@ -738,6 +738,8 @@ modules: - "flat" - "hometree" - "pep" # pep requires mod_caps + mod_push: {} + mod_push_keepalive: {} ## mod_register: ## ## Protect In-Band account registrations with CAPTCHA. diff --git a/ejabberdctl.template b/ejabberdctl.template index edf9bea0d..9d47334a2 100755 --- a/ejabberdctl.template +++ b/ejabberdctl.template @@ -69,9 +69,7 @@ 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) diff --git a/mix.exs b/mix.exs index 025552f45..24b74f87c 100644 --- a/mix.exs +++ b/mix.exs @@ -3,7 +3,7 @@ defmodule Ejabberd.Mixfile do def project do [app: :ejabberd, - version: "17.6.0", + version: "17.8.0", description: description(), elixir: "~> 1.4", elixirc_paths: ["lib"], diff --git a/mix.lock b/mix.lock index 40eda1930..4b67809e5 100644 --- a/mix.lock +++ b/mix.lock @@ -1,18 +1,22 @@ -%{"cache_tab": {:hex, :cache_tab, "1.0.8", "eac8923f0f20c35e630317790c4d4c2629c5bc792753fa48eb5391bd39c80245", [:rebar3], [{:p1_utils, "1.0.9", [hex: :p1_utils, optional: false]}]}, - "distillery": {:hex, :distillery, "1.4.0", "d633cd322c8efa0428082b00b7f902daf8caa166d45f9022bbc19a896d2e1e56", [:mix], []}, - "earmark": {:hex, :earmark, "1.2.2", "f718159d6b65068e8daeef709ccddae5f7fdc770707d82e7d126f584cd925b74", [:mix], []}, - "esip": {:hex, :esip, "1.0.12", "e0505afe74bb362b0ea486e2a64b3c1934b1eb541a7b3e990b23045e4bdc07d4", [:rebar3], [{:fast_tls, "1.0.12", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.9", [hex: :p1_utils, optional: false]}, {:stun, "1.0.11", [hex: :stun, optional: false]}]}, - "ex_doc": {:hex, :ex_doc, "0.16.1", "b4b8a23602b4ce0e9a5a960a81260d1f7b29635b9652c67e95b0c2f7ccee5e81", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]}, +%{"cache_tab": {:hex, :cache_tab, "1.0.10", "dd6aba8951ba15cab4ad483d997f8eefdb0cb00225971d0629c730d107a2bed6", [:rebar3], [{:p1_utils, "1.0.9", [hex: :p1_utils, optional: false]}]}, + "distillery": {:hex, :distillery, "1.4.1", "546d851bf27ae8fe0727e10e4fc4e146ad836eecee138263a60431e688044ed3", [:mix], []}, + "earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [:mix], []}, + "eredis": {:hex, :eredis, "1.0.8", "ab4fda1c4ba7fbe6c19c26c249dc13da916d762502c4b4fa2df401a8d51c5364", [:rebar], []}, + "esip": {:hex, :esip, "1.0.15", "82c8b0178618c10b1ac9690841d94025c982d63f8cd6c8f8bf920cf33e301658", [:rebar3], [{:fast_tls, "1.0.15", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.9", [hex: :p1_utils, optional: false]}, {:stun, "1.0.14", [hex: :stun, optional: false]}]}, + "ex_doc": {:hex, :ex_doc, "0.16.2", "3b3e210ebcd85a7c76b4e73f85c5640c011d2a0b2f06dcdf5acdb2ae904e5084", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]}, "ezlib": {:hex, :ezlib, "1.0.2", "22004ecf553a7d831404394d5642712e2aede90522e22bd6ccc089ca410ee098", [:rebar3], []}, - "fast_tls": {:hex, :fast_tls, "1.0.12", "861b591f23103142782c5b72de8898673a37acd78646c50dbda978e1e1c5b463", [:rebar3], [{:p1_utils, "1.0.9", [hex: :p1_utils, optional: false]}]}, + "fast_tls": {:hex, :fast_tls, "1.0.15", "96546e6a8b8384fbbcddf435c4c42cf2c0a3dc1858c3c9c2e62a74ae1ddd526a", [:rebar3], [{:p1_utils, "1.0.9", [hex: :p1_utils, optional: false]}]}, "fast_xml": {:hex, :fast_xml, "1.1.23", "1e7b311d3353806ee832d7630fef57713987cea40a7020669cf057d537de4721", [:rebar3], [{:p1_utils, "1.0.9", [hex: :p1_utils, optional: false]}]}, "fast_yaml": {:hex, :fast_yaml, "1.0.10", "ce5d52b77cb21968c8b73aa29b39f56a4ffd7e1e11f853d5597e7277858f155e", [:rebar3], [{:p1_utils, "1.0.9", [hex: :p1_utils, optional: false]}]}, "goldrush": {:hex, :goldrush, "0.1.9", "f06e5d5f1277da5c413e84d5a2924174182fb108dabb39d5ec548b27424cd106", [:rebar3], []}, "iconv": {:hex, :iconv, "1.0.5", "ae871aa11c854695db37e48fd5e5583b02e106126fbdf21bb53448f5a47c092b", [:rebar3], [{:p1_utils, "1.0.9", [hex: :p1_utils, optional: false]}]}, "jiffy": {:hex, :jiffy, "0.14.11", "919a87d491c5a6b5e3bbc27fafedc3a0761ca0b4c405394f121f582fd4e3f0e5", [:rebar3], []}, "lager": {:hex, :lager, "3.4.2", "150b9a17b23ae6d3265cc10dc360747621cf217b7a22b8cddf03b2909dbf7aa5", [:rebar3], [{:goldrush, "0.1.9", [hex: :goldrush, optional: false]}]}, + "p1_mysql": {:hex, :p1_mysql, "1.0.3", "e2cc26f2e8d17c3885a9c2fee3ff64fcac5915896f50ab6f6aa9b0da1eed341c", [:rebar3], []}, "p1_oauth2": {:hex, :p1_oauth2, "0.6.1", "4e021250cc198c538b097393671a41e7cebf463c248980320e038fe0316eb56b", [:rebar3], []}, + "p1_pgsql": {:hex, :p1_pgsql, "1.1.3", "ce94c83e9605c88d5f541b8f4b49edff3dc2bbacd1b6409c4cad0fbf7bef2ac4", [:rebar3], []}, "p1_utils": {:hex, :p1_utils, "1.0.9", "c33c230efbeb4dcc02911161e3cb1a93231a92df15e3fc97de655a9271a26d9f", [:rebar3], []}, + "sqlite3": {:hex, :sqlite3, "1.1.5", "794738b6d07b6d36ec6d42492cb9d629bad9cf3761617b8b8d728e765db19840", [:rebar3], []}, "stringprep": {:hex, :stringprep, "1.0.9", "9182ba39931cd1db528b8883cad0d63530abe2bf21835d26cec2f9af8bc00be0", [:rebar3], [{:p1_utils, "1.0.9", [hex: :p1_utils, optional: false]}]}, - "stun": {:hex, :stun, "1.0.11", "386cb3e3543e17a6351028a43e047c2172225d035c826a72fcb67672da9874e5", [:rebar3], [{:fast_tls, "1.0.12", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.9", [hex: :p1_utils, optional: false]}]}, - "xmpp": {:hex, :xmpp, "1.1.11", "8c49964d0d48b81080d2c5700fcf6cc19950ae9dc60a71bd3ff3d4620336d052", [:rebar3], [{:fast_xml, "1.1.23", [hex: :fast_xml, optional: false]}, {:p1_utils, "1.0.9", [hex: :p1_utils, optional: false]}, {:stringprep, "1.0.9", [hex: :stringprep, optional: false]}]}} + "stun": {:hex, :stun, "1.0.14", "6dc2080c25a72f7087301dc7333c1ea7d27ea4d88efaa379fc2b5924f3b17006", [:rebar3], [{:fast_tls, "1.0.15", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.9", [hex: :p1_utils, optional: false]}]}, + "xmpp": {:hex, :xmpp, "1.1.14", "e186f5208e7a448a4af784a8d2cb87cefe99dd49b24623e25d38115b23a50e12", [:rebar3], [{:fast_xml, "1.1.23", [hex: :fast_xml, optional: false]}, {:p1_utils, "1.0.9", [hex: :p1_utils, optional: false]}, {:stringprep, "1.0.9", [hex: :stringprep, optional: false]}]}} diff --git a/rebar.config b/rebar.config index d24236cee..0383fb084 100644 --- a/rebar.config +++ b/rebar.config @@ -77,7 +77,7 @@ ezlib, iconv]}}. -{erl_first_files, ["src/ejabberd_config.erl", "src/gen_mod.erl", "src/mod_muc_room.erl"]}. +{erl_first_files, ["src/ejabberd_config.erl", "src/gen_mod.erl", "src/mod_muc_room.erl", "src/mod_push.erl"]}. {erl_opts, [nowarn_deprecated_function, {i, "include"}, @@ -93,6 +93,7 @@ {if_var_true, elixir, {d, 'ELIXIR_ENABLED'}}, {if_var_true, erlang_deprecated_types, {d, 'ERL_DEPRECATED_TYPES'}}, {if_have_fun, {crypto, strong_rand_bytes, 1}, {d, 'STRONG_RAND_BYTES'}}, + {if_have_fun, {rand, uniform, 1}, {d, 'RAND_UNIFORM'}}, {if_have_fun, {gb_sets, iterator_from, 2}, {d, 'GB_SETS_ITERATOR_FROM'}}, {if_have_fun, {public_key, short_name_hash, 1}, {d, 'SHORT_NAME_HASH'}}, %% {if_have_fun, {public_key, generate_key, 1}, {d, 'GENERATE_RSA_KEY'}}, diff --git a/src/ejabberd_auth.erl b/src/ejabberd_auth.erl index 251e36ff7..b34925ff0 100644 --- a/src/ejabberd_auth.erl +++ b/src/ejabberd_auth.erl @@ -261,12 +261,12 @@ try_register(User, Server, Password) -> ok; (Mod, _) -> db_try_register( - User, Server, Password, Mod) + LUser, LServer, Password, Mod) end, {error, not_allowed}, auth_modules(LServer)) of ok -> ejabberd_hooks:run( - register_user, Server, [User, Server]); + register_user, LServer, [LUser, LServer]); {error, _} = Err -> Err end; diff --git a/src/ejabberd_auth_mnesia.erl b/src/ejabberd_auth_mnesia.erl index 9c2152578..690152674 100644 --- a/src/ejabberd_auth_mnesia.erl +++ b/src/ejabberd_auth_mnesia.erl @@ -208,6 +208,8 @@ remove_user(User, Server) -> {error, db_failure} end. +need_transform(#reg_users_counter{}) -> + false; need_transform(#passwd{us = {U, S}, password = Pass}) -> if is_binary(Pass) -> case store_type(S) of diff --git a/src/ejabberd_bosh.erl b/src/ejabberd_bosh.erl index 0755067e7..1df6681ff 100644 --- a/src/ejabberd_bosh.erl +++ b/src/ejabberd_bosh.erl @@ -27,9 +27,7 @@ -protocol({xep, 124, '1.11'}). -protocol({xep, 206, '1.4'}). --define(GEN_FSM, p1_fsm). - --behaviour(?GEN_FSM). +-behaviour(p1_fsm). %% API -export([start/2, start/3, start_link/3]). @@ -137,18 +135,18 @@ start(#body{attrs = Attrs} = Body, IP, SID) -> end. start(StateName, State) -> - (?GEN_FSM):start_link(?MODULE, [StateName, State], + p1_fsm:start_link(?MODULE, [StateName, State], ?FSMOPTS). start_link(Body, IP, SID) -> - (?GEN_FSM):start_link(?MODULE, [Body, IP, SID], + p1_fsm:start_link(?MODULE, [Body, IP, SID], ?FSMOPTS). send({http_bind, FsmRef, IP}, Packet) -> send_xml({http_bind, FsmRef, IP}, Packet). send_xml({http_bind, FsmRef, _IP}, Packet) -> - case catch (?GEN_FSM):sync_send_all_state_event(FsmRef, + case catch p1_fsm:sync_send_all_state_event(FsmRef, {send_xml, Packet}, ?SEND_TIMEOUT) of @@ -160,12 +158,12 @@ send_xml({http_bind, FsmRef, _IP}, Packet) -> setopts({http_bind, FsmRef, _IP}, Opts) -> case lists:member({active, once}, Opts) of true -> - (?GEN_FSM):send_all_state_event(FsmRef, + p1_fsm:send_all_state_event(FsmRef, {activate, self()}); _ -> case lists:member({active, false}, Opts) of true -> - case catch (?GEN_FSM):sync_send_all_state_event(FsmRef, + case catch p1_fsm:sync_send_all_state_event(FsmRef, deactivate_socket) of {'EXIT', _} -> {error, einval}; @@ -181,7 +179,7 @@ custom_receiver({http_bind, FsmRef, _IP}) -> {receiver, ?MODULE, FsmRef}. become_controller(FsmRef, C2SPid) -> - (?GEN_FSM):send_all_state_event(FsmRef, + p1_fsm:send_all_state_event(FsmRef, {become_controller, C2SPid}). change_controller({http_bind, FsmRef, _IP}, C2SPid) -> @@ -190,14 +188,14 @@ change_controller({http_bind, FsmRef, _IP}, C2SPid) -> reset_stream({http_bind, _FsmRef, _IP}) -> ok. change_shaper({http_bind, FsmRef, _IP}, Shaper) -> - (?GEN_FSM):send_all_state_event(FsmRef, + p1_fsm:send_all_state_event(FsmRef, {change_shaper, Shaper}). monitor({http_bind, FsmRef, _IP}) -> erlang:monitor(process, FsmRef). close({http_bind, FsmRef, _IP}) -> - catch (?GEN_FSM):sync_send_all_state_event(FsmRef, + catch p1_fsm:sync_send_all_state_event(FsmRef, close). sockname(_Socket) -> {ok, {{0, 0, 0, 0}, 0}}. @@ -269,7 +267,7 @@ process_request(Data, IP, Type) -> end. process_request(Pid, Req, _IP, Type) -> - case catch (?GEN_FSM):sync_send_event(Pid, Req, + case catch p1_fsm:sync_send_event(Pid, Req, infinity) of #body{} = Resp -> bosh_response(Resp, Type); @@ -571,7 +569,7 @@ handle_sync_event({send_xml, El}, _From, StateName, of {{value, {TRef, From, Body}}, Q} -> cancel_timer(TRef), - (?GEN_FSM):send_event(self(), {Body, From}), + p1_fsm:send_event(self(), {Body, From}), State1#state{shaped_receivers = Q}; _ -> State1 end, @@ -598,7 +596,7 @@ handle_info({timeout, TRef, shaper_timeout}, StateName, State) -> case p1_queue:out(State#state.shaped_receivers) of {{value, {TRef, From, Req}}, Q} -> - (?GEN_FSM):send_event(self(), {Req, From}), + p1_fsm:send_event(self(), {Req, From}), {next_state, StateName, State#state{shaped_receivers = Q}}; {{value, _}, _} -> @@ -630,7 +628,7 @@ terminate(_Reason, _StateName, State) -> mod_bosh:close_session(State#state.sid), case State#state.c2s_pid of C2SPid when is_pid(C2SPid) -> - (?GEN_FSM):send_event(C2SPid, closed); + p1_fsm:send_event(C2SPid, closed); _ -> ok end, bounce_receivers(State, closed), @@ -644,7 +642,7 @@ print_state(State) -> State. route_els(#state{el_ibuf = Buf, c2s_pid = C2SPid} = State) -> NewBuf = p1_queue:dropwhile( fun(El) -> - ?GEN_FSM:send_event(C2SPid, El), + p1_fsm:send_event(C2SPid, El), true end, Buf), State#state{el_ibuf = NewBuf}. @@ -653,7 +651,7 @@ route_els(State, Els) -> case State#state.c2s_pid of C2SPid when is_pid(C2SPid) -> lists:foreach(fun (El) -> - (?GEN_FSM):send_event(C2SPid, El) + p1_fsm:send_event(C2SPid, El) end, Els), State; @@ -676,7 +674,7 @@ reply(State, Body, RID, From) -> case catch gb_trees:take_smallest(Receivers) of {NextRID, {From1, Req}, Receivers1} when NextRID == RID + 1 -> - (?GEN_FSM):send_event(self(), {Req, From1}), + p1_fsm:send_event(self(), {Req, From1}), State2#state{receivers = Receivers1}; _ -> State2#state{receivers = Receivers} end. @@ -715,7 +713,7 @@ do_reply(State, From, Body, RID) -> ?DEBUG("send reply:~n** RequestID: ~p~n** Reply: " "~p~n** To: ~p~n** State: ~p", [RID, Body, From, State]), - (?GEN_FSM):reply(From, Body), + p1_fsm:reply(From, Body), Responses = gb_trees:delete_any(RID, State#state.responses), Responses1 = case gb_trees:size(Responses) of @@ -1053,7 +1051,7 @@ buf_out(Buf, I, Els) -> end. cancel_timer(TRef) when is_reference(TRef) -> - (?GEN_FSM):cancel_timer(TRef); + p1_fsm:cancel_timer(TRef); cancel_timer(_) -> false. restart_timer(TRef, Timeout, Msg) -> diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 159cb4054..fe60f344e 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -47,7 +47,7 @@ process_terminated/2, process_info/2]). %% API -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, + open_session/1, call/3, cast/2, send/2, close/1, close/2, stop/1, reply/2, copy_state/2, set_timeout/2, route/2, host_up/1, host_down/1]). @@ -90,6 +90,10 @@ socket_type() -> call(Ref, Msg, Timeout) -> xmpp_stream_in:call(Ref, Msg, Timeout). +-spec cast(pid(), term()) -> ok. +cast(Ref, Msg) -> + xmpp_stream_in:cast(Ref, Msg). + reply(Ref, Reply) -> xmpp_stream_in:reply(Ref, Reply). @@ -293,14 +297,19 @@ process_terminated(State, _Reason) -> %%%=================================================================== %%% xmpp_stream_in callbacks %%%=================================================================== -tls_options(#{lserver := LServer, tls_options := DefaultOpts}) -> - TLSOpts1 = case ejabberd_config:get_option( - {c2s_certfile, LServer}, - ejabberd_config:get_option( - {domain_certfile, LServer})) of - undefined -> DefaultOpts; - CertFile -> lists:keystore(certfile, 1, DefaultOpts, - {certfile, CertFile}) +tls_options(#{lserver := LServer, tls_options := DefaultOpts, + stream_encrypted := Encrypted}) -> + TLSOpts1 = case {Encrypted, proplists:get_value(certfile, DefaultOpts)} of + {true, CertFile} when CertFile /= undefined -> DefaultOpts; + {_, _} -> + case ejabberd_config:get_option( + {domain_certfile, LServer}, + ejabberd_config:get_option( + {c2s_certfile, LServer})) of + undefined -> DefaultOpts; + CertFile -> lists:keystore(certfile, 1, DefaultOpts, + {certfile, CertFile}) + end end, TLSOpts2 = case ejabberd_config:get_option( {c2s_ciphers, LServer}) of diff --git a/src/ejabberd_captcha.erl b/src/ejabberd_captcha.erl index 48e4ac1e6..76af5278f 100644 --- a/src/ejabberd_captcha.erl +++ b/src/ejabberd_captcha.erl @@ -388,29 +388,24 @@ get_transfer_protocol(PortString) -> get_port_listeners(PortNumber) -> AllListeners = ejabberd_config:get_option(listen, []), - lists:filter(fun (Listener) when is_list(Listener) -> - case proplists:get_value(port, Listener) of - PortNumber -> true; - _ -> false - end; - (_) -> false - end, - AllListeners). + lists:filter( + fun({{Port, _IP, _Transport}, _Module, _Opts}) -> + Port == PortNumber + end, AllListeners). get_captcha_transfer_protocol([]) -> throw(<<"The port number mentioned in captcha_host " "is not a ejabberd_http listener with " "'captcha' option. Change the port number " "or specify http:// in that option.">>); -get_captcha_transfer_protocol([Listener | Listeners]) when is_list(Listener) -> - case proplists:get_value(module, Listener) == ejabberd_http andalso - proplists:get_bool(captcha, Listener) of +get_captcha_transfer_protocol([{_, ejabberd_http, Opts} | Listeners]) -> + case proplists:get_bool(captcha, Opts) of true -> - case proplists:get_bool(tls, Listener) of - true -> https; - false -> http - end; - false -> get_captcha_transfer_protocol(Listeners) + case proplists:get_bool(tls, Opts) of + true -> https; + false -> http + end; + false -> get_captcha_transfer_protocol(Listeners) end; get_captcha_transfer_protocol([_ | Listeners]) -> get_captcha_transfer_protocol(Listeners). diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl index 1e5d495ce..5d3bc8680 100644 --- a/src/ejabberd_config.erl +++ b/src/ejabberd_config.erl @@ -28,7 +28,7 @@ -export([start/0, load_file/1, reload_file/0, read_file/1, get_option/1, get_option/2, add_option/2, has_option/1, - get_vh_by_auth_method/1, is_file_readable/1, + get_vh_by_auth_method/1, get_version/0, get_myhosts/0, get_mylang/0, get_lang/1, get_ejabberd_config_path/0, is_using_elixir_config/0, prepare_opt_val/4, transform_options/1, collect_options/1, @@ -46,11 +46,12 @@ get_global_option/2, get_local_option/2, get_global_option/3, get_local_option/3, get_option/3]). +-export([is_file_readable/1]). -deprecated([{add_global_option, 2}, {add_local_option, 2}, {get_global_option, 2}, {get_local_option, 2}, {get_global_option, 3}, {get_local_option, 3}, - {get_option, 3}]). + {get_option, 3}, {is_file_readable, 1}]). -include("ejabberd.hrl"). -include("logger.hrl"). diff --git a/src/ejabberd_http_ws.erl b/src/ejabberd_http_ws.erl index f4a73cc39..f9f7b07e9 100644 --- a/src/ejabberd_http_ws.erl +++ b/src/ejabberd_http_ws.erl @@ -28,7 +28,7 @@ -author('ecestari@process-one.net'). --behaviour(gen_fsm). +-behaviour(p1_fsm). -export([start/1, start_link/1, init/1, handle_event/3, handle_sync_event/4, code_change/4, handle_info/3, @@ -75,13 +75,13 @@ -export_type([ws_socket/0]). start(WS) -> - gen_fsm:start(?MODULE, [WS], ?FSMOPTS). + p1_fsm:start(?MODULE, [WS], ?FSMOPTS). start_link(WS) -> - gen_fsm:start_link(?MODULE, [WS], ?FSMOPTS). + p1_fsm:start_link(?MODULE, [WS], ?FSMOPTS). send_xml({http_ws, FsmRef, _IP}, Packet) -> - case catch gen_fsm:sync_send_all_state_event(FsmRef, + case catch p1_fsm:sync_send_all_state_event(FsmRef, {send_xml, Packet}, 15000) of @@ -93,7 +93,7 @@ send_xml({http_ws, FsmRef, _IP}, Packet) -> setopts({http_ws, FsmRef, _IP}, Opts) -> case lists:member({active, once}, Opts) of true -> - gen_fsm:send_all_state_event(FsmRef, + p1_fsm:send_all_state_event(FsmRef, {activate, self()}); _ -> ok end. @@ -105,11 +105,11 @@ peername({http_ws, _FsmRef, IP}) -> {ok, IP}. controlling_process(_Socket, _Pid) -> ok. become_controller(FsmRef, C2SPid) -> - gen_fsm:send_all_state_event(FsmRef, + p1_fsm:send_all_state_event(FsmRef, {become_controller, C2SPid}). close({http_ws, FsmRef, _IP}) -> - catch gen_fsm:sync_send_all_state_event(FsmRef, close). + catch p1_fsm:sync_send_all_state_event(FsmRef, close). socket_handoff(LocalPath, Request, Socket, SockMod, Buf, Opts) -> ejabberd_websocket:socket_handoff(LocalPath, Request, Socket, SockMod, @@ -241,6 +241,7 @@ handle_info(PingPong, StateName, StateData) when PingPong == ping orelse StateData2#state{pong_expected = false}}; handle_info({timeout, Timer, _}, _StateName, #state{timer = Timer} = StateData) -> + ?DEBUG("Closing websocket connection from hitting inactivity timeout", []), {stop, normal, StateData}; handle_info({timeout, Timer, _}, StateName, #state{ping_timer = Timer, ws = {_, WsPid}} = StateData) -> @@ -253,6 +254,7 @@ handle_info({timeout, Timer, _}, StateName, {next_state, StateName, StateData#state{ping_timer = PingTimer, pong_expected = true}}; true -> + ?DEBUG("Closing websocket connection from missing pongs", []), {stop, normal, StateData} end; handle_info(_, StateName, StateData) -> diff --git a/src/ejabberd_logger.erl b/src/ejabberd_logger.erl index 1e48732c8..eee9d3b83 100644 --- a/src/ejabberd_logger.erl +++ b/src/ejabberd_logger.erl @@ -151,6 +151,9 @@ do_start() -> application:set_env(lager, crash_log_size, LogRotateSize), application:set_env(lager, crash_log_count, LogRotateCount), ejabberd:start_app(lager), + lists:foreach(fun(Handler) -> + lager:set_loghwm(Handler, LogRateLimit) + end, gen_event:which_handlers(lager_event)), ok. %% @spec () -> ok diff --git a/src/ejabberd_receiver.erl b/src/ejabberd_receiver.erl index 44c29680c..52077ac3c 100644 --- a/src/ejabberd_receiver.erl +++ b/src/ejabberd_receiver.erl @@ -234,7 +234,7 @@ terminate(_Reason, State) -> close_stream(XMLStreamState), if C2SPid /= undefined -> - gen_fsm:send_event(C2SPid, closed); + p1_fsm:send_event(C2SPid, closed); true -> ok end, catch (State#state.sock_mod):close(State#state.socket), @@ -272,7 +272,7 @@ process_data([Element | Els], element(1, Element) == xmlstreamend -> if C2SPid == undefined -> State; true -> - catch gen_fsm:send_event(C2SPid, + catch p1_fsm:send_event(C2SPid, element_wrapper(Element)), process_data(Els, State) end; diff --git a/src/ejabberd_regexp.erl b/src/ejabberd_regexp.erl index b4ef7ac16..10f51aa47 100644 --- a/src/ejabberd_regexp.erl +++ b/src/ejabberd_regexp.erl @@ -25,7 +25,7 @@ -module(ejabberd_regexp). --compile([export_all]). +-export([exec/2, run/2, split/2, replace/3, greplace/3, sh_to_awk/1]). exec({ReM, ReF, ReA}, {RgM, RgF, RgA}) -> try apply(ReM, ReF, ReA) catch diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl index a0e9411cf..cb4e5e5ec 100644 --- a/src/ejabberd_s2s.erl +++ b/src/ejabberd_s2s.erl @@ -39,7 +39,7 @@ remove_connection/2, start_connection/2, start_connection/3, dirty_get_connections/0, allow_host/2, incoming_s2s_number/0, outgoing_s2s_number/0, - stop_all_connections/0, + stop_s2s_connections/0, clean_temporarily_blocked_table/0, list_temporarily_blocked_hosts/0, external_host_overloaded/1, is_temporarly_blocked/1, @@ -199,9 +199,9 @@ dirty_get_connections() -> -spec tls_options(binary(), [proplists:property()]) -> [proplists:property()]. tls_options(LServer, DefaultOpts) -> TLSOpts1 = case ejabberd_config:get_option( - {s2s_certfile, LServer}, + {domain_certfile, LServer}, ejabberd_config:get_option( - {domain_certfile, LServer})) of + {s2s_certfile, LServer})) of undefined -> DefaultOpts; CertFile -> lists:keystore(certfile, 1, DefaultOpts, {certfile, CertFile}) @@ -558,10 +558,10 @@ get_commands_spec() -> 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", + name = stop_s2s_connections, tags = [s2s], + desc = "Stop all s2s outgoing and incoming connections", policy = admin, - module = ?MODULE, function = stop_all_connections, + module = ?MODULE, function = stop_s2s_connections, args = [], result = {res, rescode}}]. %% TODO Move those stats commands to ejabberd stats command ? @@ -578,8 +578,8 @@ supervisor_count(Supervisor) -> length(Result) end. --spec stop_all_connections() -> ok. -stop_all_connections() -> +-spec stop_s2s_connections() -> ok. +stop_s2s_connections() -> lists:foreach( fun({_Id, Pid, _Type, _Module}) -> supervisor:terminate_child(ejabberd_s2s_in_sup, Pid) @@ -682,7 +682,7 @@ complete_s2s_info([Connection | T], Type, Result) -> -spec get_s2s_state(pid()) -> [{status, open | closed | error} | {s2s_pid, pid()}]. get_s2s_state(S2sPid) -> - Infos = case gen_fsm:sync_send_all_state_event(S2sPid, + Infos = case p1_fsm:sync_send_all_state_event(S2sPid, get_state_infos) of {state_infos, Is} -> [{status, open} | Is]; diff --git a/src/ejabberd_s2s_out.erl b/src/ejabberd_s2s_out.erl index e8cad9792..fea5d8162 100644 --- a/src/ejabberd_s2s_out.erl +++ b/src/ejabberd_s2s_out.erl @@ -374,7 +374,7 @@ mk_bounce_error(_Lang, _State) -> -spec get_delay() -> non_neg_integer(). get_delay() -> MaxDelay = ejabberd_config:get_option(s2s_max_retry_delay, 300), - crypto:rand_uniform(1, MaxDelay). + randoms:uniform(MaxDelay). -spec set_idle_timeout(state()) -> state(). set_idle_timeout(#{on_route := send, server := LServer} = State) -> diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl index 333edffa6..96dbb4e83 100644 --- a/src/ejabberd_sm.erl +++ b/src/ejabberd_sm.erl @@ -66,6 +66,8 @@ user_resources/2, kick_user/2, get_session_pid/3, + get_session_sid/3, + get_session_sids/2, get_user_info/2, get_user_info/3, get_user_ip/3, @@ -292,15 +294,32 @@ close_session_unset_presence(SID, User, Server, -spec get_session_pid(binary(), binary(), binary()) -> none | pid(). get_session_pid(User, Server, Resource) -> + case get_session_sid(User, Server, Resource) of + {_, PID} -> PID; + none -> none + end. + +-spec get_session_sid(binary(), binary(), binary()) -> none | sid(). + +get_session_sid(User, Server, Resource) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LResource = jid:resourceprep(Resource), Mod = get_sm_backend(LServer), case online(get_sessions(Mod, LUser, LServer, LResource)) of - [#session{sid = {_, Pid}}] -> Pid; + [#session{sid = SID}] -> SID; _ -> none end. +-spec get_session_sids(binary(), binary()) -> [sid()]. + +get_session_sids(User, Server) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + Mod = get_sm_backend(LServer), + Sessions = online(get_sessions(Mod, LUser, LServer)), + [SID || #session{sid = SID} <- Sessions]. + -spec set_offline_info(sid(), binary(), binary(), binary(), info()) -> ok. set_offline_info(SID, User, Server, Resource, Info) -> @@ -471,9 +490,13 @@ host_up(Host) -> -spec host_down(binary()) -> ok. host_down(Host) -> Mod = get_sm_backend(Host), + Err = case ejabberd_cluster:get_nodes() of + [Node] when Node == node() -> xmpp:serr_system_shutdown(); + _ -> xmpp:serr_reset() + end, lists:foreach( fun(#session{sid = {_, Pid}}) when node(Pid) == node() -> - ejabberd_c2s:send(Pid, xmpp:serr_system_shutdown()), + ejabberd_c2s:send(Pid, Err), ejabberd_c2s:stop(Pid); (_) -> ok @@ -972,7 +995,7 @@ get_commands_spec() -> args = [], result = {num_sessions, integer}}, #ejabberd_commands{name = user_resources, tags = [session], desc = "List user's connected resources", - policy = user, + policy = admin, module = ?MODULE, function = user_resources, args = [{user, binary}, {host, binary}], args_desc = ["User name", "Server name"], diff --git a/src/ejabberd_sql.erl b/src/ejabberd_sql.erl index 35d970291..cae41da6c 100644 --- a/src/ejabberd_sql.erl +++ b/src/ejabberd_sql.erl @@ -29,9 +29,7 @@ -author('alexey@process-one.net'). --define(GEN_FSM, p1_fsm). - --behaviour(?GEN_FSM). +-behaviour(p1_fsm). %% External exports -export([start/1, start_link/2, @@ -113,11 +111,11 @@ %%% API %%%---------------------------------------------------------------------- start(Host) -> - (?GEN_FSM):start(ejabberd_sql, [Host], + p1_fsm:start(ejabberd_sql, [Host], fsm_limit_opts() ++ (?FSMOPTS)). start_link(Host, StartInterval) -> - (?GEN_FSM):start_link(ejabberd_sql, + p1_fsm:start_link(ejabberd_sql, [Host, StartInterval], fsm_limit_opts() ++ (?FSMOPTS)). @@ -160,7 +158,7 @@ sql_call(Host, Msg) -> case ejabberd_sql_sup:get_random_pid(Host) of none -> {error, <<"Unknown Host">>}; Pid -> - (?GEN_FSM):sync_send_event(Pid,{sql_cmd, Msg, + p1_fsm:sync_send_event(Pid,{sql_cmd, Msg, p1_time_compat:monotonic_time(milli_seconds)}, query_timeout(Host)) end; @@ -168,7 +166,7 @@ sql_call(Host, Msg) -> end. keep_alive(Host, PID) -> - (?GEN_FSM):sync_send_event(PID, + p1_fsm:sync_send_event(PID, {sql_cmd, {sql_query, ?KEEPALIVE_QUERY}, p1_time_compat:monotonic_time(milli_seconds)}, query_timeout(Host)). @@ -280,7 +278,7 @@ init([Host, StartInterval]) -> keep_alive, [Host, self()]) end, [DBType | _] = db_opts(Host), - (?GEN_FSM):send_event(self(), connect), + p1_fsm:send_event(self(), connect), ejabberd_sql_sup:add_pid(Host, self()), QueueType = case ejabberd_config:get_option({sql_queue_type, Host}) of undefined -> @@ -313,7 +311,7 @@ connecting(connect, #state{host = Host} = State) -> PendingRequests = p1_queue:dropwhile( fun(Req) -> - ?GEN_FSM:send_event(self(), Req), + p1_fsm:send_event(self(), Req), true end, State#state.pending_requests), State1 = State#state{db_ref = Ref, @@ -325,7 +323,7 @@ connecting(connect, #state{host = Host} = State) -> "Retry after: ~p seconds", [State#state.db_type, Reason, State#state.start_interval div 1000]), - (?GEN_FSM):send_event_after(State#state.start_interval, + p1_fsm:send_event_after(State#state.start_interval, connect), {next_state, connecting, State} end; @@ -337,7 +335,7 @@ connecting(Event, State) -> connecting({sql_cmd, {sql_query, ?KEEPALIVE_QUERY}, _Timestamp}, From, State) -> - (?GEN_FSM):reply(From, + p1_fsm:reply(From, {error, <<"SQL connection failed">>}), {next_state, connecting, State}; connecting({sql_cmd, Command, Timestamp} = Req, From, @@ -350,7 +348,7 @@ connecting({sql_cmd, Command, Timestamp} = Req, From, catch error:full -> Q = p1_queue:dropwhile( fun({sql_cmd, _, To, _Timestamp}) -> - (?GEN_FSM):reply( + p1_fsm:reply( To, {error, <<"SQL connection failed">>}), true end, State#state.pending_requests), @@ -393,7 +391,7 @@ code_change(_OldVsn, StateName, State, _Extra) -> %% monitoring the connection) handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, _StateName, State) -> - (?GEN_FSM):send_event(self(), connect), + p1_fsm:send_event(self(), connect), {next_state, connecting, State}; handle_info(Info, StateName, State) -> ?WARNING_MSG("unexpected info in ~p: ~p", @@ -734,16 +732,16 @@ sql_query_to_iolist(SQLQuery) -> abort_on_driver_error({error, <<"query timed out">>} = Reply, From) -> - (?GEN_FSM):reply(From, Reply), + p1_fsm:reply(From, Reply), {stop, timeout, get(?STATE_KEY)}; abort_on_driver_error({error, <<"Failed sending data on socket", _/binary>>} = Reply, From) -> - (?GEN_FSM):reply(From, Reply), + p1_fsm:reply(From, Reply), {stop, closed, get(?STATE_KEY)}; abort_on_driver_error(Reply, From) -> - (?GEN_FSM):reply(From, Reply), + p1_fsm:reply(From, Reply), {next_state, session_established, get(?STATE_KEY)}. %% == pure ODBC code diff --git a/src/ejd2sql.erl b/src/ejd2sql.erl index c4f00a551..c801eb973 100644 --- a/src/ejd2sql.erl +++ b/src/ejd2sql.erl @@ -58,6 +58,7 @@ modules() -> mod_offline, mod_privacy, mod_private, + mod_pubsub, mod_roster, mod_shared_roster, mod_vcard]. @@ -80,8 +81,8 @@ export(Server, Output, Module) -> case export(LServer, Table, IO, ConvertFun) of {atomic, ok} -> ok; {aborted, Reason} -> - ?ERROR_MSG("Failed export for module ~p: ~p", - [Module, Reason]) + ?ERROR_MSG("Failed export for module ~p and table ~p: ~p", + [Module, Table, Reason]) end end, Module:export(Server)), close_output(Output, IO). diff --git a/src/eldap.erl b/src/eldap.erl index f47550353..8e6b710b1 100644 --- a/src/eldap.erl +++ b/src/eldap.erl @@ -63,7 +63,7 @@ %%% active_bind - sent bind() request and waiting for response %%%---------------------------------------------------------------------- --behaviour(gen_fsm). +-behaviour(p1_fsm). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -148,7 +148,7 @@ start_link(Name) -> Reg_name = misc:binary_to_atom(<<"eldap_", Name/binary>>), - gen_fsm:start_link({local, Reg_name}, ?MODULE, [], []). + p1_fsm:start_link({local, Reg_name}, ?MODULE, [], []). -spec start_link(binary(), [binary()], inet:port_number(), binary(), binary(), tlsopts()) -> any(). @@ -156,7 +156,7 @@ start_link(Name) -> start_link(Name, Hosts, Port, Rootdn, Passwd, Opts) -> Reg_name = misc:binary_to_atom(<<"eldap_", Name/binary>>), - gen_fsm:start_link({local, Reg_name}, ?MODULE, + p1_fsm:start_link({local, Reg_name}, ?MODULE, [Hosts, Port, Rootdn, Passwd, Opts], []). -spec get_status(handle()) -> any(). @@ -166,7 +166,7 @@ start_link(Name, Hosts, Port, Rootdn, Passwd, Opts) -> %%% -------------------------------------------------------------------- get_status(Handle) -> Handle1 = get_handle(Handle), - gen_fsm:sync_send_all_state_event(Handle1, get_status). + p1_fsm:sync_send_all_state_event(Handle1, get_status). %%% -------------------------------------------------------------------- %%% Shutdown connection (and process) asynchronous. @@ -175,7 +175,7 @@ get_status(Handle) -> close(Handle) -> Handle1 = get_handle(Handle), - gen_fsm:send_all_state_event(Handle1, close). + p1_fsm:send_all_state_event(Handle1, close). %%% -------------------------------------------------------------------- %%% Add an entry. The entry field MUST NOT exist for the AddRequest @@ -192,7 +192,7 @@ close(Handle) -> %%% -------------------------------------------------------------------- add(Handle, Entry, Attributes) -> Handle1 = get_handle(Handle), - gen_fsm:sync_send_event(Handle1, + p1_fsm:sync_send_event(Handle1, {add, Entry, add_attrs(Attributes)}, ?CALL_TIMEOUT). %%% Do sanity check ! @@ -216,7 +216,7 @@ add_attrs(Attrs) -> %%% -------------------------------------------------------------------- delete(Handle, Entry) -> Handle1 = get_handle(Handle), - gen_fsm:sync_send_event(Handle1, {delete, Entry}, + p1_fsm:sync_send_event(Handle1, {delete, Entry}, ?CALL_TIMEOUT). %%% -------------------------------------------------------------------- @@ -234,7 +234,7 @@ delete(Handle, Entry) -> modify(Handle, Object, Mods) -> Handle1 = get_handle(Handle), - gen_fsm:sync_send_event(Handle1, {modify, Object, Mods}, + p1_fsm:sync_send_event(Handle1, {modify, Object, Mods}, ?CALL_TIMEOUT). %%% @@ -274,7 +274,7 @@ m(Operation, Type, Values) -> modify_dn(Handle, Entry, NewRDN, DelOldRDN, NewSup) -> Handle1 = get_handle(Handle), - gen_fsm:sync_send_event(Handle1, + p1_fsm:sync_send_event(Handle1, {modify_dn, Entry, NewRDN, bool_p(DelOldRDN), optional(NewSup)}, ?CALL_TIMEOUT). @@ -283,7 +283,7 @@ modify_dn(Handle, Entry, NewRDN, DelOldRDN, NewSup) -> modify_passwd(Handle, DN, Passwd) -> Handle1 = get_handle(Handle), - gen_fsm:sync_send_event(Handle1, + p1_fsm:sync_send_event(Handle1, {modify_passwd, DN, Passwd}, ?CALL_TIMEOUT). %%% -------------------------------------------------------------------- @@ -298,7 +298,7 @@ modify_passwd(Handle, DN, Passwd) -> bind(Handle, RootDN, Passwd) -> Handle1 = get_handle(Handle), - gen_fsm:sync_send_event(Handle1, {bind, RootDN, Passwd}, + p1_fsm:sync_send_event(Handle1, {bind, RootDN, Passwd}, ?CALL_TIMEOUT). %%% Sanity checks ! @@ -356,7 +356,7 @@ search(Handle, L) when is_list(L) -> call_search(Handle, A) -> Handle1 = get_handle(Handle), - gen_fsm:sync_send_event(Handle1, {search, A}, + p1_fsm:sync_send_event(Handle1, {search, A}, ?CALL_TIMEOUT). -spec parse_search_args(search_args()) -> eldap_search(). @@ -637,7 +637,7 @@ active(Event, From, S) -> %%---------------------------------------------------------------------- %% Func: handle_event/3 -%% Called when gen_fsm:send_all_state_event/2 is invoked. +%% Called when p1_fsm:send_all_state_event/2 is invoked. %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} @@ -680,7 +680,7 @@ handle_info({Tag, _Socket, Data}, StateName, S) case catch recvd_packet(Data, S) of {response, Response, RequestType} -> NewS = case Response of - {reply, Reply, To, S1} -> gen_fsm:reply(To, Reply), S1; + {reply, Reply, To, S1} -> p1_fsm:reply(To, Reply), S1; {ok, S1} -> S1 end, if StateName == active_bind andalso @@ -709,7 +709,7 @@ handle_info({timeout, Timer, {cmd_timeout, Id}}, StateName, S) -> case cmd_timeout(Timer, Id, S) of {reply, To, Reason, NewS} -> - gen_fsm:reply(To, Reason), + p1_fsm:reply(To, Reason), {next_state, StateName, NewS}; {error, _Reason} -> {next_state, StateName, S} end; diff --git a/src/ext_mod.erl b/src/ext_mod.erl index 80296f7b7..ecdfa04c3 100644 --- a/src/ext_mod.erl +++ b/src/ext_mod.erl @@ -146,16 +146,29 @@ get_commands_spec() -> %% -- public modules functions update() -> - add_sources(?REPOS), + Contrib = maps:put(?REPOS, [], maps:new()), + Jungles = lists:foldl(fun({Package, Spec}, Acc) -> + Repo = proplists:get_value(url, Spec, ""), + Mods = maps:get(Repo, Acc, []), + maps:put(Repo, [Package|Mods], Acc) + end, Contrib, modules_spec(sources_dir(), "*/*")), + Repos = maps:fold(fun(Repo, _Mods, Acc) -> + Update = add_sources(Repo), + ?INFO_MSG("Update packages from repo ~s: ~p", [Repo, Update]), + case Update of + ok -> Acc; + Error -> [{repository, Repo, Error}|Acc] + end + end, [], Jungles), Res = lists:foldl(fun({Package, Spec}, Acc) -> - Path = proplists:get_value(url, Spec, ""), - Update = add_sources(Package, Path), + Repo = proplists:get_value(url, Spec, ""), + Update = add_sources(Package, Repo), ?INFO_MSG("Update package ~s: ~p", [Package, Update]), case Update of ok -> Acc; - Error -> [Error|Acc] + Error -> [{Package, Repo, Error}|Acc] end - end, [], modules_spec(sources_dir(), "*")), + end, Repos, modules_spec(sources_dir(), "*")), case Res of [] -> ok; [Error|_] -> Error @@ -547,7 +560,9 @@ compile_result(Results) -> compile_options() -> [verbose, report_errors, report_warnings] ++ [{i, filename:join(app_dir(App), "include")} - || App <- [fast_xml, xmpp, p1_utils, ejabberd]]. + || App <- [fast_xml, xmpp, p1_utils, ejabberd]] + ++ [{i, filename:join(mod_dir(Mod), "include")} + || Mod <- installed()]. app_dir(App) -> case code:lib_dir(App) of @@ -562,6 +577,10 @@ app_dir(App) -> Dir end. +mod_dir({Package, Spec}) -> + Default = filename:join(modules_dir(), Package), + proplists:get_value(path, Spec, Default). + compile_erlang_file(Dest, File) -> compile_erlang_file(Dest, File, compile_options()). diff --git a/src/gen_mod.erl b/src/gen_mod.erl index 5bfa3b4d4..e17197dfb 100644 --- a/src/gen_mod.erl +++ b/src/gen_mod.erl @@ -34,7 +34,8 @@ stop_child/1, stop_child/2, config_reloaded/0]). -export([start_module/2, start_module/3, stop_module/2, stop_module_keep_config/2, - get_opt/2, get_opt/3, get_opt_host/3, opt_type/1, is_equal_opt/4, + get_opt/2, get_opt/3, get_opt_host/3, + get_opt_hosts/3, opt_type/1, is_equal_opt/4, get_module_opt/3, get_module_opt/4, get_module_opt_host/3, loaded_modules/1, loaded_modules_with_opts/1, get_hosts/2, get_module_proc/2, is_loaded/2, is_loaded_elsewhere/2, @@ -441,6 +442,20 @@ get_opt_host(Host, Opts, Default) -> Val = get_opt(host, Opts, Default), ejabberd_regexp:greplace(Val, <<"@HOST@">>, Host). +-spec get_opt_hosts(binary(), opts(), binary()) -> [binary()]. + +get_opt_hosts(Host, Opts, Default) -> + Vals = case get_opt(host, Opts, undefined) of + undefined -> + case get_opt(hosts, Opts, []) of + [] -> [Default]; + L -> L + end; + Val -> + [Val] + end, + [ejabberd_regexp:greplace(V, <<"@HOST@">>, Host) || V <- Vals]. + -spec get_validators(binary(), module(), opts()) -> dict:dict() | undef. get_validators(Host, Module, Opts) -> try Module:mod_opt_type('') of diff --git a/src/misc.erl b/src/misc.erl index 604a458af..32699e76b 100644 --- a/src/misc.erl +++ b/src/misc.erl @@ -32,8 +32,8 @@ hex_to_bin/1, hex_to_base64/1, expand_keyword/3, atom_to_binary/1, binary_to_atom/1, tuple_to_binary/1, l2i/1, i2l/1, i2l/2, expr_to_term/1, term_to_expr/1, - encode_pid/1, decode_pid/2, compile_exprs/2, join_atoms/2, - try_read_file/1]). + now_to_usec/1, usec_to_now/1, encode_pid/1, decode_pid/2, + compile_exprs/2, join_atoms/2, try_read_file/1]). %% Deprecated functions -export([decode_base64/1, encode_base64/1]). @@ -127,6 +127,18 @@ expr_to_term(Expr) -> term_to_expr(Term) -> list_to_binary(io_lib:print(Term)). +-spec now_to_usec(erlang:timestamp()) -> non_neg_integer(). +now_to_usec({MSec, Sec, USec}) -> + (MSec*1000000 + Sec)*1000000 + USec. + +-spec usec_to_now(non_neg_integer()) -> erlang:timestamp(). +usec_to_now(Int) -> + Secs = Int div 1000000, + USec = Int rem 1000000, + MSec = Secs div 1000000, + Sec = Secs rem 1000000, + {MSec, Sec, USec}. + l2i(I) when is_integer(I) -> I; l2i(L) when is_binary(L) -> binary_to_integer(L). @@ -192,22 +204,12 @@ join_atoms(Atoms, Sep) -> %% in configuration validators only. -spec try_read_file(file:filename_all()) -> binary(). try_read_file(Path) -> - Res = case file:read_file_info(Path) of - {ok, #file_info{type = Type, access = Access}} -> - case {Type, Access} of - {regular, read} -> ok; - {regular, read_write} -> ok; - {regular, _} -> {error, file:format_error(eaccess)}; - _ -> {error, "not a regular file"} - end; - {error, Why} -> - {error, file:format_error(Why)} - end, - case Res of - ok -> + case file:open(Path, [read]) of + {ok, Fd} -> + file:close(Fd), iolist_to_binary(Path); - {error, Reason} -> - ?ERROR_MSG("Failed to read ~s: ~s", [Path, Reason]), + {error, Why} -> + ?ERROR_MSG("Failed to read ~s: ~s", [Path, file:format_error(Why)]), erlang:error(badarg) end. diff --git a/src/mod_echo.erl b/src/mod_echo.erl index 861b1a0ef..79dd59962 100644 --- a/src/mod_echo.erl +++ b/src/mod_echo.erl @@ -43,7 +43,7 @@ -include("xmpp.hrl"). --record(state, {host = <<"">> :: binary()}). +-record(state, {hosts = [] :: [binary()]}). %%==================================================================== %% gen_mod API @@ -62,7 +62,9 @@ depends(_Host, _Opts) -> []. mod_opt_type(host) -> fun iolist_to_binary/1; -mod_opt_type(_) -> [host]. +mod_opt_type(hosts) -> + fun(L) -> lists:map(fun iolist_to_binary/1, L) end; +mod_opt_type(_) -> [host, hosts]. %%==================================================================== %% gen_server callbacks @@ -77,10 +79,13 @@ mod_opt_type(_) -> [host]. %%-------------------------------------------------------------------- init([Host, Opts]) -> process_flag(trap_exit, true), - MyHost = gen_mod:get_opt_host(Host, Opts, + Hosts = gen_mod:get_opt_hosts(Host, Opts, <<"echo.@HOST@">>), - ejabberd_router:register_route(MyHost, Host), - {ok, #state{host = MyHost}}. + lists:foreach( + fun(H) -> + ejabberd_router:register_route(H, Host) + end, Hosts), + {ok, #state{hosts = Hosts}}. %%-------------------------------------------------------------------- %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | @@ -101,17 +106,19 @@ handle_call(stop, _From, State) -> %% Description: Handling cast messages %%-------------------------------------------------------------------- handle_cast({reload, Host, NewOpts, OldOpts}, State) -> - NewMyHost = gen_mod:get_opt_host(Host, NewOpts, - <<"echo.@HOST@">>), - OldMyHost = gen_mod:get_opt_host(Host, OldOpts, - <<"echo.@HOST@">>), - if NewMyHost /= OldMyHost -> - ejabberd_router:register_route(NewMyHost, Host), - ejabberd_router:unregister_route(OldMyHost); - true -> - ok - end, - {noreply, State#state{host = NewMyHost}}; + NewMyHosts = gen_mod:get_opt_hosts(Host, NewOpts, + <<"echo.@HOST@">>), + OldMyHosts = gen_mod:get_opt_hosts(Host, OldOpts, + <<"echo.@HOST@">>), + lists:foreach( + fun(H) -> + ejabberd_router:unregister_route(H) + end, OldMyHosts -- NewMyHosts), + lists:foreach( + fun(H) -> + ejabberd_router:register_route(H, Host) + end, NewMyHosts -- OldMyHosts), + {noreply, State#state{hosts = NewMyHosts}}; handle_cast(Msg, State) -> ?WARNING_MSG("unexpected cast: ~p", [Msg]), {noreply, State}. @@ -147,7 +154,7 @@ handle_info(_Info, State) -> {noreply, State}. %% The return value is ignored. %%-------------------------------------------------------------------- terminate(_Reason, State) -> - ejabberd_router:unregister_route(State#state.host), ok. + lists:foreach(fun ejabberd_router:unregister_route/1, State#state.hosts). %%-------------------------------------------------------------------- %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index 7be05dbf1..ef881d14b 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -538,9 +538,17 @@ json_error(HTTPCode, JSONCode, Message) -> log(Call, Args, {Addr, Port}) -> AddrS = misc:ip_to_list({Addr, Port}), - ?INFO_MSG("API call ~s ~p from ~s:~p", [Call, Args, AddrS, Port]); + ?INFO_MSG("API call ~s ~p from ~s:~p", [Call, hide_sensitive_args(Args), AddrS, Port]); log(Call, Args, IP) -> - ?INFO_MSG("API call ~s ~p (~p)", [Call, Args, IP]). + ?INFO_MSG("API call ~s ~p (~p)", [Call, hide_sensitive_args(Args), IP]). + +hide_sensitive_args(Args=[_H|_T]) -> + lists:map( fun({<<"password">>, Password}) -> {<<"password">>, ejabberd_config:may_hide_data(Password)}; + ({<<"newpass">>,NewPassword}) -> {<<"newpass">>, ejabberd_config:may_hide_data(NewPassword)}; + (E) -> E end, + Args); +hide_sensitive_args(NonListArgs) -> + NonListArgs. permission_addon() -> Access = gen_mod:get_module_opt(global, ?MODULE, admin_ip_access, none), diff --git a/src/mod_http_fileserver.erl b/src/mod_http_fileserver.erl index c2144042e..4e3cfd08b 100644 --- a/src/mod_http_fileserver.erl +++ b/src/mod_http_fileserver.erl @@ -59,8 +59,13 @@ -define(HTTP_ERR_FILE_NOT_FOUND, {-1, 404, [], <<"Not found">>}). +-define(REQUEST_AUTH_HEADERS, + [{<<"WWW-Authenticate">>, <<"Basic realm=\"ejabberd\"">>}]). + -define(HTTP_ERR_FORBIDDEN, {-1, 403, [], <<"Forbidden">>}). +-define(HTTP_ERR_REQUEST_AUTH, + {-1, 401, ?REQUEST_AUTH_HEADERS, <<"Unauthorized">>}). -define(DEFAULT_CONTENT_TYPE, <<"application/octet-stream">>). @@ -317,12 +322,17 @@ serve(LocalPath, Auth, DocRoot, DirectoryIndices, CustomHeaders, DefaultContentT false end, case CanProceed of + false -> + ?HTTP_ERR_REQUEST_AUTH; true -> FileName = filename:join(filename:split(DocRoot) ++ LocalPath), case file:read_file_info(FileName) of - {error, enoent} -> ?HTTP_ERR_FILE_NOT_FOUND; - {error, enotdir} -> ?HTTP_ERR_FILE_NOT_FOUND; - {error, eacces} -> ?HTTP_ERR_FORBIDDEN; + {error, enoent} -> + ?HTTP_ERR_FILE_NOT_FOUND; + {error, enotdir} -> + ?HTTP_ERR_FILE_NOT_FOUND; + {error, eacces} -> + ?HTTP_ERR_FORBIDDEN; {ok, #file_info{type = directory}} -> serve_index(FileName, DirectoryIndices, CustomHeaders, diff --git a/src/mod_http_upload.erl b/src/mod_http_upload.erl index c8cd300f4..8d986d0d3 100644 --- a/src/mod_http_upload.erl +++ b/src/mod_http_upload.erl @@ -94,7 +94,7 @@ -record(state, {server_host :: binary(), - host :: binary(), + hosts :: [binary()], name :: binary(), access :: atom(), max_size :: pos_integer() | infinity, @@ -151,6 +151,8 @@ stop(ServerHost) -> mod_opt_type(host) -> fun iolist_to_binary/1; +mod_opt_type(hosts) -> + fun (L) -> lists:map(fun iolist_to_binary/1, L) end; mod_opt_type(name) -> fun iolist_to_binary/1; mod_opt_type(access) -> @@ -194,7 +196,7 @@ mod_opt_type(rm_on_unregister) -> mod_opt_type(thumbnail) -> fun(B) when is_boolean(B) -> B end; mod_opt_type(_) -> - [host, name, access, max_size, secret_length, jid_in_url, file_mode, + [host, hosts, name, access, max_size, secret_length, jid_in_url, file_mode, dir_mode, docroot, put_url, get_url, service_url, custom_headers, rm_on_unregister, thumbnail]. @@ -211,7 +213,7 @@ depends(_Host, _Opts) -> init([ServerHost, Opts]) -> process_flag(trap_exit, true), - Host = gen_mod:get_opt_host(ServerHost, Opts, <<"upload.@HOST@">>), + Hosts = gen_mod:get_opt_hosts(ServerHost, Opts, <<"upload.@HOST@">>), Name = gen_mod:get_opt(name, Opts, <<"HTTP File Upload">>), Access = gen_mod:get_opt(access, Opts, local), MaxSize = gen_mod:get_opt(max_size, Opts, 104857600), @@ -244,8 +246,11 @@ init([ServerHost, Opts]) -> false -> ok end, - ejabberd_router:register_route(Host, ServerHost), - {ok, #state{server_host = ServerHost, host = Host, name = Name, + lists:foreach( + fun(Host) -> + ejabberd_router:register_route(Host, ServerHost) + end, Hosts), + {ok, #state{server_host = ServerHost, hosts = Hosts, name = Name, access = Access, max_size = MaxSize, secret_length = SecretLength, jid_in_url = JIDinURL, file_mode = FileMode, dir_mode = DirMode, @@ -324,10 +329,9 @@ handle_info(Info, State) -> -spec terminate(normal | shutdown | {shutdown, _} | _, state()) -> ok. -terminate(Reason, #state{server_host = ServerHost, host = Host}) -> +terminate(Reason, #state{server_host = ServerHost, hosts = Hosts}) -> ?DEBUG("Stopping HTTP upload process for ~s: ~p", [ServerHost, Reason]), - ejabberd_router:unregister_route(Host), - ok. + lists:foreach(fun ejabberd_router:unregister_route/1, Hosts). -spec code_change({down, _} | _, state(), _) -> {ok, state()}. @@ -654,7 +658,7 @@ make_rand_string(S, N) -> make_rand_string([make_rand_char() | S], N - 1). -spec make_rand_char() -> char(). make_rand_char() -> - map_int_to_char(crypto:rand_uniform(0, 62)). + map_int_to_char(randoms:uniform(0, 61)). -spec map_int_to_char(0..61) -> char(). diff --git a/src/mod_irc.erl b/src/mod_irc.erl index fc85668e8..04687ea67 100644 --- a/src/mod_irc.erl +++ b/src/mod_irc.erl @@ -58,7 +58,7 @@ [<<"koi8-r">>, <<"iso8859-15">>, <<"iso8859-1">>, <<"iso8859-2">>, <<"utf-8">>, <<"utf-8+latin-1">>]). --record(state, {host = <<"">> :: binary(), +-record(state, {hosts = [] :: [binary()], server_host = <<"">> :: binary(), access = all :: atom()}). @@ -99,8 +99,7 @@ depends(_Host, _Opts) -> init([Host, Opts]) -> process_flag(trap_exit, true), ejabberd:start_app(iconv), - MyHost = gen_mod:get_opt_host(Host, Opts, - <<"irc.@HOST@">>), + MyHosts = gen_mod:get_opt_hosts(Host, Opts, <<"irc.@HOST@">>), Mod = gen_mod:db_mod(Host, Opts, ?MODULE), Mod:init(Host, Opts), Access = gen_mod:get_opt(access, Opts, all), @@ -108,10 +107,13 @@ init([Host, Opts]) -> [named_table, public, {keypos, #irc_connection.jid_server_host}]), IQDisc = gen_mod:get_opt(iqdisc, Opts, gen_iq_handler:iqdisc(Host)), - register_hooks(MyHost, IQDisc), - ejabberd_router:register_route(MyHost, Host), + lists:foreach( + fun(MyHost) -> + register_hooks(MyHost, IQDisc), + ejabberd_router:register_route(MyHost, Host) + end, MyHosts), {ok, - #state{host = MyHost, server_host = Host, + #state{hosts = MyHosts, server_host = Host, access = Access}}. %%-------------------------------------------------------------------- @@ -133,8 +135,8 @@ handle_call(stop, _From, State) -> %% Description: Handling cast messages %%-------------------------------------------------------------------- handle_cast({reload, ServerHost, NewOpts, OldOpts}, State) -> - NewHost = gen_mod:get_opt_host(ServerHost, NewOpts, <<"irc.@HOST@">>), - OldHost = gen_mod:get_opt_host(ServerHost, OldOpts, <<"irc.@HOST@">>), + NewHosts = gen_mod:get_opt_hosts(ServerHost, NewOpts, <<"irc.@HOST@">>), + OldHosts = gen_mod:get_opt_hosts(ServerHost, OldOpts, <<"irc.@HOST@">>), NewIQDisc = gen_mod:get_opt(iqdisc, NewOpts, gen_iq_handler:iqdisc(ServerHost)), OldIQDisc = gen_mod:get_opt(iqdisc, OldOpts, gen_iq_handler:iqdisc(ServerHost)), NewMod = gen_mod:db_mod(ServerHost, NewOpts, ?MODULE), @@ -145,20 +147,26 @@ handle_cast({reload, ServerHost, NewOpts, OldOpts}, State) -> true -> ok end, - if (NewIQDisc /= OldIQDisc) or (NewHost /= OldHost) -> - register_hooks(NewHost, NewIQDisc); - true -> - ok - end, - if NewHost /= OldHost -> - ejabberd_router:register_route(NewHost, ServerHost), - ejabberd_router:unregister_route(OldHost), - unregister_hooks(OldHost); + if (NewIQDisc /= OldIQDisc) -> + lists:foreach( + fun(NewHost) -> + register_hooks(NewHost, NewIQDisc) + end, NewHosts -- (NewHosts -- OldHosts)); true -> ok end, + lists:foreach( + fun(NewHost) -> + ejabberd_router:register_route(NewHost, ServerHost), + register_hooks(NewHost, NewIQDisc) + end, NewHosts -- OldHosts), + lists:foreach( + fun(OldHost) -> + ejabberd_router:unregister_route(OldHost), + unregister_hooks(OldHost) + end, OldHosts -- NewHosts), Access = gen_mod:get_opt(access, NewOpts, all), - {noreply, State#state{host = NewHost, access = Access}}; + {noreply, State#state{hosts = NewHosts, access = Access}}; handle_cast(Msg, State) -> ?WARNING_MSG("unexpected cast: ~p", [Msg]), {noreply, State}. @@ -170,9 +178,10 @@ handle_cast(Msg, State) -> %% Description: Handling all non call/cast messages %%-------------------------------------------------------------------- handle_info({route, Packet}, - #state{host = Host, server_host = ServerHost, - access = Access} = + #state{server_host = ServerHost, access = Access} = State) -> + To = xmpp:get_to(Packet), + Host = To#jid.lserver, case catch do_route(Host, ServerHost, Access, Packet) of {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]); _ -> ok @@ -187,9 +196,12 @@ handle_info(_Info, State) -> {noreply, State}. %% cleaning up. When it returns, the gen_server terminates with Reason. %% The return value is ignored. %%-------------------------------------------------------------------- -terminate(_Reason, #state{host = MyHost}) -> - ejabberd_router:unregister_route(MyHost), - unregister_hooks(MyHost). +terminate(_Reason, #state{hosts = MyHosts}) -> + lists:foreach( + fun(MyHost) -> + ejabberd_router:unregister_route(MyHost), + unregister_hooks(MyHost) + end, MyHosts). %%-------------------------------------------------------------------- %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} @@ -975,8 +987,10 @@ mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end; mod_opt_type(default_encoding) -> fun iolist_to_binary/1; mod_opt_type(host) -> fun iolist_to_binary/1; +mod_opt_type(hosts) -> + fun (L) -> lists:map(fun iolist_to_binary/1, L) end; mod_opt_type(_) -> - [access, db_type, default_encoding, host]. + [access, db_type, default_encoding, host, hosts]. -spec extract_ident(stanza()) -> binary(). extract_ident(Packet) -> diff --git a/src/mod_irc_connection.erl b/src/mod_irc_connection.erl index 1e90c4005..b7b2f8e1d 100644 --- a/src/mod_irc_connection.erl +++ b/src/mod_irc_connection.erl @@ -27,7 +27,7 @@ -author('alexey@process-one.net'). --behaviour(gen_fsm). +-behaviour(p1_fsm). %% External exports -export([start_link/12, start/13, route_chan/4, @@ -91,7 +91,7 @@ start(From, Host, ServerHost, Server, Username, start_link(From, Host, Server, Username, Encoding, Port, Password, Ident, RemoteAddr, RealName, WebircPassword, Mod) -> - gen_fsm:start_link(?MODULE, + p1_fsm:start_link(?MODULE, [From, Host, Server, Username, Encoding, Port, Password, Ident, RemoteAddr, RealName, WebircPassword, Mod], ?FSMOPTS). @@ -109,7 +109,7 @@ start_link(From, Host, Server, Username, Encoding, Port, %%---------------------------------------------------------------------- init([From, Host, Server, Username, Encoding, Port, Password, Ident, RemoteAddr, RealName, WebircPassword, Mod]) -> - gen_fsm:send_event(self(), init), + p1_fsm:send_event(self(), init), {ok, open_socket, #state{mod = Mod, encoding = Encoding, port = Port, password = Password, @@ -628,11 +628,11 @@ handle_info({tcp, _Socket, Data}, StateName, StateData#state{inbuf = NewBuf}}; handle_info({tcp_closed, _Socket}, StateName, StateData) -> - gen_fsm:send_event(self(), closed), + p1_fsm:send_event(self(), closed), {next_state, StateName, StateData}; handle_info({tcp_error, _Socket, _Reason}, StateName, StateData) -> - gen_fsm:send_event(self(), closed), + p1_fsm:send_event(self(), closed), {next_state, StateName, StateData}. %%---------------------------------------------------------------------- diff --git a/src/mod_mam.erl b/src/mod_mam.erl index d4c18d41b..674cefc05 100644 --- a/src/mod_mam.erl +++ b/src/mod_mam.erl @@ -434,8 +434,9 @@ message_is_archived(false, #{jid := JID}, Pkt) -> delete_old_messages(TypeBin, Days) when TypeBin == <<"chat">>; TypeBin == <<"groupchat">>; TypeBin == <<"all">> -> + CurrentTime = p1_time_compat:system_time(micro_seconds), Diff = Days * 24 * 60 * 60 * 1000000, - TimeStamp = usec_to_now(p1_time_compat:system_time(micro_seconds) - Diff), + TimeStamp = misc:usec_to_now(CurrentTime - Diff), Type = misc:binary_to_atom(TypeBin), DBTypes = lists:usort( lists:map( @@ -830,7 +831,7 @@ select(_LServer, JidRequestor, JidArchive, Query, RSM, Msgs = lists:flatmap( fun({Nick, Pkt, _HaveSubject, Now, _Size}) -> - TS = now_to_usec(Now), + TS = misc:now_to_usec(Now), case match_interval(Now, Start, End) and match_rsm(Now, RSM) of true -> @@ -979,24 +980,14 @@ match_interval(Now, Start, End) -> (Now >= Start) and (Now =< End). match_rsm(Now, #rsm_set{'after' = ID}) when is_binary(ID), ID /= <<"">> -> - Now1 = (catch usec_to_now(binary_to_integer(ID))), + Now1 = (catch misc:usec_to_now(binary_to_integer(ID))), Now > Now1; match_rsm(Now, #rsm_set{before = ID}) when is_binary(ID), ID /= <<"">> -> - Now1 = (catch usec_to_now(binary_to_integer(ID))), + Now1 = (catch misc:usec_to_now(binary_to_integer(ID))), Now < Now1; match_rsm(_Now, _) -> true. -now_to_usec({MSec, Sec, USec}) -> - (MSec*1000000 + Sec)*1000000 + USec. - -usec_to_now(Int) -> - Secs = Int div 1000000, - USec = Int rem 1000000, - MSec = Secs div 1000000, - Sec = Secs rem 1000000, - {MSec, Sec, USec}. - get_jids(undefined) -> []; get_jids(Js) -> diff --git a/src/mod_mix.erl b/src/mod_mix.erl index 4763447f3..90507665b 100644 --- a/src/mod_mix.erl +++ b/src/mod_mix.erl @@ -46,7 +46,7 @@ ?NS_MIX_NODES_CONFIG]). -record(state, {server_host :: binary(), - host :: binary()}). + hosts :: [binary()]}). %%%=================================================================== %%% API @@ -124,36 +124,39 @@ process_iq(#iq{lang = Lang} = IQ) -> %%%=================================================================== init([ServerHost, Opts]) -> process_flag(trap_exit, true), - Host = gen_mod:get_opt_host(ServerHost, Opts, <<"mix.@HOST@">>), - IQDisc = gen_mod:get_opt(iqdisc, Opts, gen_iq_handler:iqdisc(Host)), - ConfigTab = gen_mod:get_module_proc(Host, config), - ets:new(ConfigTab, [named_table]), - ets:insert(ConfigTab, {plugins, [<<"mix">>]}), - ejabberd_hooks:add(disco_local_items, Host, ?MODULE, disco_items, 100), - ejabberd_hooks:add(disco_local_features, Host, ?MODULE, disco_features, 100), - ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, disco_identity, 100), - ejabberd_hooks:add(disco_sm_items, Host, ?MODULE, disco_items, 100), - ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, disco_features, 100), - ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE, disco_identity, 100), - ejabberd_hooks:add(disco_info, Host, ?MODULE, disco_info, 100), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_DISCO_ITEMS, mod_disco, - process_local_iq_items, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_DISCO_INFO, mod_disco, - process_local_iq_info, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_DISCO_ITEMS, mod_disco, - process_local_iq_items, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_DISCO_INFO, mod_disco, - process_local_iq_info, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_PUBSUB, mod_pubsub, iq_sm, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_MIX_0, ?MODULE, process_iq, IQDisc), - ejabberd_router:register_route(Host, ServerHost), - {ok, #state{server_host = ServerHost, host = Host}}. + Hosts = gen_mod:get_opt_hosts(ServerHost, Opts, <<"mix.@HOST@">>), + IQDisc = gen_mod:get_opt(iqdisc, Opts, gen_iq_handler:iqdisc(ServerHost)), + lists:foreach( + fun(Host) -> + ConfigTab = gen_mod:get_module_proc(Host, config), + ets:new(ConfigTab, [named_table]), + ets:insert(ConfigTab, {plugins, [<<"mix">>]}), + ejabberd_hooks:add(disco_local_items, Host, ?MODULE, disco_items, 100), + ejabberd_hooks:add(disco_local_features, Host, ?MODULE, disco_features, 100), + ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, disco_identity, 100), + ejabberd_hooks:add(disco_sm_items, Host, ?MODULE, disco_items, 100), + ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, disco_features, 100), + ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE, disco_identity, 100), + ejabberd_hooks:add(disco_info, Host, ?MODULE, disco_info, 100), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, + ?NS_DISCO_ITEMS, mod_disco, + process_local_iq_items, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, + ?NS_DISCO_INFO, mod_disco, + process_local_iq_info, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, + ?NS_DISCO_ITEMS, mod_disco, + process_local_iq_items, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, + ?NS_DISCO_INFO, mod_disco, + process_local_iq_info, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, + ?NS_PUBSUB, mod_pubsub, iq_sm, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, + ?NS_MIX_0, ?MODULE, process_iq, IQDisc), + ejabberd_router:register_route(Host, ServerHost) + end, Hosts), + {ok, #state{server_host = ServerHost, hosts = Hosts}}. handle_call(_Request, _From, State) -> Reply = ok, @@ -180,22 +183,24 @@ handle_info({route, Packet}, State) -> handle_info(_Info, State) -> {noreply, State}. -terminate(_Reason, #state{host = Host}) -> - ejabberd_hooks:delete(disco_local_items, Host, ?MODULE, disco_items, 100), - ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, disco_features, 100), - ejabberd_hooks:delete(disco_local_identity, Host, ?MODULE, disco_identity, 100), - ejabberd_hooks:delete(disco_sm_items, Host, ?MODULE, disco_items, 100), - ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, disco_features, 100), - ejabberd_hooks:delete(disco_sm_identity, Host, ?MODULE, disco_identity, 100), - ejabberd_hooks:delete(disco_info, Host, ?MODULE, disco_info, 100), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_DISCO_ITEMS), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_DISCO_INFO), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_PUBSUB), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MIX_0), - ejabberd_router:unregister_route(Host), - ok. +terminate(_Reason, #state{hosts = Hosts}) -> + lists:foreach( + fun(Host) -> + ejabberd_hooks:delete(disco_local_items, Host, ?MODULE, disco_items, 100), + ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, disco_features, 100), + ejabberd_hooks:delete(disco_local_identity, Host, ?MODULE, disco_identity, 100), + ejabberd_hooks:delete(disco_sm_items, Host, ?MODULE, disco_items, 100), + ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, disco_features, 100), + ejabberd_hooks:delete(disco_sm_identity, Host, ?MODULE, disco_identity, 100), + ejabberd_hooks:delete(disco_info, Host, ?MODULE, disco_info, 100), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_DISCO_ITEMS), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_DISCO_INFO), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_PUBSUB), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MIX_0), + ejabberd_router:unregister_route(Host) + end, Hosts). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -316,4 +321,6 @@ depends(_Host, _Opts) -> mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; mod_opt_type(host) -> fun iolist_to_binary/1; -mod_opt_type(_) -> [host, iqdisc]. +mod_opt_type(hosts) -> + fun (L) -> lists:map(fun iolist_to_binary/1, L) end; +mod_opt_type(_) -> [host, hosts, iqdisc]. diff --git a/src/mod_muc.erl b/src/mod_muc.erl index 69fc2d0dc..2aebe2226 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -75,7 +75,7 @@ -include("mod_muc.hrl"). -record(state, - {host = <<"">> :: binary(), + {hosts = [] :: [binary()], server_host = <<"">> :: binary(), access = {none, none, none, none} :: {atom(), atom(), atom(), atom()}, history_size = 20 :: non_neg_integer(), @@ -151,8 +151,9 @@ room_destroyed(Host, Room, Pid, ServerHost) -> %% If Opts = default, the default room options are used. %% Else use the passed options as defined in mod_muc_room. create_room(Host, Name, From, Nick, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?MODULE), - gen_server:call(Proc, {create, Name, From, Nick, Opts}). + ServerHost = ejabberd_router:host_of_route(Host), + Proc = gen_mod:get_module_proc(ServerHost, ?MODULE), + gen_server:call(Proc, {create, Name, Host, From, Nick, Opts}). store_room(ServerHost, Host, Name, Opts) -> LServer = jid:nameprep(ServerHost), @@ -225,22 +226,26 @@ get_online_rooms_by_user(ServerHost, LUser, LServer) -> init([Host, Opts]) -> process_flag(trap_exit, true), IQDisc = gen_mod:get_opt(iqdisc, Opts, gen_iq_handler:iqdisc(Host)), - #state{access = Access, host = MyHost, + #state{access = Access, hosts = MyHosts, history_size = HistorySize, queue_type = QueueType, room_shaper = RoomShaper} = State = init_state(Host, Opts), Mod = gen_mod:db_mod(Host, Opts, ?MODULE), RMod = gen_mod:ram_db_mod(Host, Opts, ?MODULE), - Mod:init(Host, [{host, MyHost}|Opts]), - RMod:init(Host, [{host, MyHost}|Opts]), - register_iq_handlers(MyHost, IQDisc), - ejabberd_router:register_route(MyHost, Host), - load_permanent_rooms(MyHost, Host, Access, HistorySize, RoomShaper, QueueType), + Mod:init(Host, [{hosts, MyHosts}|Opts]), + RMod:init(Host, [{hosts, MyHosts}|Opts]), + lists:foreach( + fun(MyHost) -> + register_iq_handlers(MyHost, IQDisc), + ejabberd_router:register_route(MyHost, Host), + load_permanent_rooms(MyHost, Host, Access, HistorySize, + RoomShaper, QueueType) + end, MyHosts), {ok, State}. handle_call(stop, _From, State) -> {stop, normal, ok, State}; -handle_call({create, Room, From, Nick, Opts}, _From, - #state{host = Host, server_host = ServerHost, +handle_call({create, Room, Host, From, Nick, Opts}, _From, + #state{server_host = ServerHost, access = Access, default_room_opts = DefOpts, history_size = HistorySize, queue_type = QueueType, room_shaper = RoomShaper} = State) -> @@ -256,51 +261,59 @@ handle_call({create, Room, From, Nick, Opts}, _From, Nick, NewOpts, QueueType), RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RMod:register_online_room(ServerHost, Room, Host, Pid), + ejabberd_hooks:run(create_room, ServerHost, [ServerHost, Room, Host]), {reply, ok, State}. -handle_cast({reload, ServerHost, NewOpts, OldOpts}, #state{host = OldHost}) -> +handle_cast({reload, ServerHost, NewOpts, OldOpts}, #state{hosts = OldHosts}) -> NewIQDisc = gen_mod:get_opt(iqdisc, NewOpts, gen_iq_handler:iqdisc(ServerHost)), OldIQDisc = gen_mod:get_opt(iqdisc, OldOpts, gen_iq_handler:iqdisc(ServerHost)), NewMod = gen_mod:db_mod(ServerHost, NewOpts, ?MODULE), NewRMod = gen_mod:ram_db_mod(ServerHost, NewOpts, ?MODULE), OldMod = gen_mod:db_mod(ServerHost, OldOpts, ?MODULE), OldRMod = gen_mod:ram_db_mod(ServerHost, OldOpts, ?MODULE), - #state{host = NewHost} = NewState = init_state(ServerHost, NewOpts), + #state{hosts = NewHosts} = NewState = init_state(ServerHost, NewOpts), if NewMod /= OldMod -> - NewMod:init(ServerHost, [{host, NewHost}|NewOpts]); + NewMod:init(ServerHost, [{hosts, NewHosts}|NewOpts]); true -> ok end, if NewRMod /= OldRMod -> - NewRMod:init(ServerHost, [{host, NewHost}|NewOpts]); + NewRMod:init(ServerHost, [{hosts, NewHosts}|NewOpts]); true -> ok end, - if (NewIQDisc /= OldIQDisc) or (NewHost /= OldHost) -> - register_iq_handlers(NewHost, NewIQDisc); - true -> - ok - end, - if NewHost /= OldHost -> - ejabberd_router:register_route(NewHost, ServerHost), - ejabberd_router:unregister_route(OldHost), - unregister_iq_handlers(OldHost); + if (NewIQDisc /= OldIQDisc) -> + lists:foreach( + fun(NewHost) -> + register_iq_handlers(NewHost, NewIQDisc) + end, NewHosts -- (NewHosts -- OldHosts)); true -> ok end, + lists:foreach( + fun(NewHost) -> + ejabberd_router:register_route(NewHost, ServerHost), + register_iq_handlers(NewHost, NewIQDisc) + end, NewHosts -- OldHosts), + lists:foreach( + fun(OldHost) -> + ejabberd_router:unregister_route(OldHost), + unregister_iq_handlers(OldHost) + end, OldHosts -- NewHosts), {noreply, NewState}; handle_cast(Msg, State) -> ?WARNING_MSG("unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({route, Packet}, - #state{host = Host, server_host = ServerHost, + #state{server_host = ServerHost, access = Access, default_room_opts = DefRoomOpts, history_size = HistorySize, queue_type = QueueType, max_rooms_discoitems = MaxRoomsDiscoItems, room_shaper = RoomShaper} = State) -> From = xmpp:get_from(Packet), To = xmpp:get_to(Packet), + Host = To#jid.lserver, case catch do_route(Host, ServerHost, Access, HistorySize, RoomShaper, From, To, Packet, DefRoomOpts, MaxRoomsDiscoItems, QueueType) of @@ -319,9 +332,12 @@ handle_info(Info, State) -> ?ERROR_MSG("unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{host = MyHost}) -> - ejabberd_router:unregister_route(MyHost), - unregister_iq_handlers(MyHost). +terminate(_Reason, #state{hosts = MyHosts}) -> + lists:foreach( + fun(MyHost) -> + ejabberd_router:unregister_route(MyHost), + unregister_iq_handlers(MyHost) + end, MyHosts). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -329,8 +345,8 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. %%% Internal functions %%-------------------------------------------------------------------- init_state(Host, Opts) -> - MyHost = gen_mod:get_opt_host(Host, Opts, - <<"conference.@HOST@">>), + MyHosts = gen_mod:get_opt_hosts(Host, Opts, + <<"conference.@HOST@">>), Access = gen_mod:get_opt(access, Opts, all), AccessCreate = gen_mod:get_opt(access_create, Opts, all), AccessAdmin = gen_mod:get_opt(access_admin, Opts, none), @@ -341,7 +357,7 @@ init_state(Host, Opts) -> QueueType = gen_mod:get_opt(queue_type, Opts, ejabberd_config:default_queue_type(Host)), RoomShaper = gen_mod:get_opt(room_shaper, Opts, none), - #state{host = MyHost, + #state{hosts = MyHosts, server_host = Host, access = {Access, AccessCreate, AccessAdmin, AccessPersistent}, default_room_opts = DefRoomOpts, @@ -667,7 +683,7 @@ iq_disco_items(_ServerHost, _Host, _From, Lang, _MaxRoomsDiscoItems, _Node, _RSM {error, timeout | notfound}. get_room_disco_item({Name, Host, Pid}, Query) -> RoomJID = jid:make(Name, Host), - try gen_fsm:sync_send_all_state_event(Pid, Query, 100) of + try p1_fsm:sync_send_all_state_event(Pid, Query, 100) of {item, Desc} -> {ok, #disco_item{jid = RoomJID, name = Desc}}; false -> @@ -683,7 +699,7 @@ get_subscribed_rooms(ServerHost, Host, From) -> BareFrom = jid:remove_resource(From), lists:flatmap( fun({Name, _, Pid}) -> - case gen_fsm:sync_send_all_state_event(Pid, {is_subscribed, BareFrom}) of + case p1_fsm:sync_send_all_state_event(Pid, {is_subscribed, BareFrom}) of true -> [jid:make(Name, Host)]; false -> [] end; @@ -765,7 +781,7 @@ process_iq_register_set(ServerHost, Host, From, broadcast_service_message(ServerHost, Host, Msg) -> lists:foreach( fun({_, _, Pid}) -> - gen_fsm:send_all_state_event( + p1_fsm:send_all_state_event( Pid, {service_message, Msg}) end, get_online_rooms(ServerHost, Host)). @@ -850,6 +866,8 @@ mod_opt_type(ram_db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end; mod_opt_type(history_size) -> fun (I) when is_integer(I), I >= 0 -> I end; mod_opt_type(host) -> fun iolist_to_binary/1; +mod_opt_type(hosts) -> + fun (L) -> lists:map(fun iolist_to_binary/1, L) end; mod_opt_type(max_room_desc) -> fun (infinity) -> infinity; (I) when is_integer(I), I > 0 -> I @@ -943,7 +961,7 @@ mod_opt_type({default_room_options, presence_broadcast}) -> end; mod_opt_type(_) -> [access, access_admin, access_create, access_persistent, - db_type, ram_db_type, history_size, host, + db_type, ram_db_type, history_size, host, hosts, max_room_desc, max_room_id, max_room_name, max_rooms_discoitems, max_user_conferences, max_users, max_users_admin_threshold, max_users_presence, diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index 2d1e66ba5..332c83b55 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -597,7 +597,7 @@ muc_create_room(ServerHost, {Name, Host, _}, DefRoomOpts) -> destroy_room(Name, Service) -> case mod_muc:find_online_room(Name, Service) of {ok, Pid} -> - gen_fsm:send_all_state_event(Pid, destroy), + p1_fsm:send_all_state_event(Pid, destroy), ok; error -> error @@ -716,11 +716,11 @@ get_rooms(ServerHost) -> end, Hosts). get_room_config(Room_pid) -> - {ok, R} = gen_fsm:sync_send_all_state_event(Room_pid, get_config), + {ok, R} = p1_fsm:sync_send_all_state_event(Room_pid, get_config), R. get_room_state(Room_pid) -> - {ok, R} = gen_fsm:sync_send_all_state_event(Room_pid, get_state), + {ok, R} = p1_fsm:sync_send_all_state_event(Room_pid, get_state), R. %%--------------- @@ -786,7 +786,7 @@ find_serverhost(Host, ServerHosts) -> ServerHost. act_on_room(destroy, {N, H, Pid}, SH) -> - gen_fsm:send_all_state_event( + p1_fsm:send_all_state_event( Pid, {destroy, <<"Room destroyed by rooms_unused_destroy.">>}), mod_muc:room_destroyed(H, N, Pid, SH), mod_muc:forget_room(SH, H, N); @@ -888,7 +888,7 @@ change_room_option(Name, Service, OptionString, ValueString) -> {Option, Value} = format_room_option(OptionString, ValueString), Config = get_room_config(Pid), Config2 = change_option(Option, Value, Config), - {ok, _} = gen_fsm:sync_send_all_state_event(Pid, {change_config, Config2}), + {ok, _} = p1_fsm:sync_send_all_state_event(Pid, {change_config, Config2}), ok end. @@ -983,7 +983,7 @@ get_room_affiliations(Name, Service) -> case mod_muc:find_online_room(Name, Service) of {ok, Pid} -> %% Get the PID of the online room, then request its state - {ok, StateData} = gen_fsm:sync_send_all_state_event(Pid, get_state), + {ok, StateData} = p1_fsm:sync_send_all_state_event(Pid, get_state), Affiliations = ?DICT:to_list(StateData#state.affiliations), lists:map( fun({{Uname, Domain, _Res}, {Aff, Reason}}) when is_atom(Aff)-> @@ -1012,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, <<"">>}, undefined}), + {ok, StateData} = p1_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 -> @@ -1035,7 +1035,7 @@ subscribe_room(User, Nick, Room, Nodes) -> UserJID -> case get_room_pid(Name, Host) of Pid when is_pid(Pid) -> - case gen_fsm:sync_send_all_state_event( + case p1_fsm:sync_send_all_state_event( Pid, {muc_subscribe, UserJID, Nick, NodeList}) of {ok, SubscribedNodes} -> @@ -1062,7 +1062,7 @@ unsubscribe_room(User, Room) -> UserJID -> case get_room_pid(Name, Host) of Pid when is_pid(Pid) -> - case gen_fsm:sync_send_all_state_event( + case p1_fsm:sync_send_all_state_event( Pid, {muc_unsubscribe, UserJID}) of ok -> @@ -1085,7 +1085,7 @@ unsubscribe_room(User, Room) -> get_subscribers(Name, Host) -> case get_room_pid(Name, Host) of Pid when is_pid(Pid) -> - {ok, JIDList} = gen_fsm:sync_send_all_state_event(Pid, get_subscribers), + {ok, JIDList} = p1_fsm:sync_send_all_state_event(Pid, get_subscribers), [jid:encode(jid:remove_resource(J)) || J <- JIDList]; _ -> throw({error, "The room does not exist"}) diff --git a/src/mod_muc_log.erl b/src/mod_muc_log.erl index 3d4c87c0f..61101d1c2 100644 --- a/src/mod_muc_log.erl +++ b/src/mod_muc_log.erl @@ -1142,7 +1142,7 @@ get_room_state(RoomName, MucService) -> -spec get_room_state(pid()) -> mod_muc_room:state(). get_room_state(RoomPid) -> - {ok, R} = gen_fsm:sync_send_all_state_event(RoomPid, + {ok, R} = p1_fsm:sync_send_all_state_event(RoomPid, get_state), R. diff --git a/src/mod_muc_mnesia.erl b/src/mod_muc_mnesia.erl index 53f31cb9f..015c5ec43 100644 --- a/src/mod_muc_mnesia.erl +++ b/src/mod_muc_mnesia.erl @@ -296,7 +296,7 @@ import(_LServer, <<"muc_registered">>, %%% gen_server callbacks %%%=================================================================== init([Host, Opts]) -> - MyHost = proplists:get_value(host, Opts), + MyHosts = proplists:get_value(hosts, Opts), case gen_mod:db_mod(Host, Opts, mod_muc) of ?MODULE -> ejabberd_mnesia:create(?MODULE, muc_room, @@ -318,7 +318,10 @@ init([Host, Opts]) -> {type, ordered_set}, {attributes, record_info(fields, muc_online_room)}]), catch ets:new(muc_online_users, [bag, named_table, public, {keypos, 2}]), - clean_table_from_bad_node(node(), MyHost), + lists:foreach( + fun(MyHost) -> + clean_table_from_bad_node(node(), MyHost) + end, MyHosts), mnesia:subscribe(system); _ -> ok diff --git a/src/mod_muc_riak.erl b/src/mod_muc_riak.erl index 8dd1eddf8..42e644fdd 100644 --- a/src/mod_muc_riak.erl +++ b/src/mod_muc_riak.erl @@ -60,7 +60,7 @@ restore_room(_LServer, Host, Name) -> forget_room(_LServer, Host, Name) -> {atomic, ejabberd_riak:delete(muc_room, {Name, Host})}. -can_use_nick(LServer, Host, JID, Nick) -> +can_use_nick(_LServer, Host, JID, Nick) -> {LUser, LServer, _} = jid:tolower(JID), LUS = {LUser, LServer}, case ejabberd_riak:get_by_index(muc_registered, diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index ec1cffd6a..2a1ca6011 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -27,7 +27,7 @@ -author('alexey@process-one.net'). --behaviour(gen_fsm). +-behaviour(p1_fsm). %% External exports -export([start_link/10, @@ -94,23 +94,23 @@ %%%---------------------------------------------------------------------- start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, Nick, DefRoomOpts, QueueType) -> - gen_fsm:start(?MODULE, [Host, ServerHost, Access, Room, HistorySize, + p1_fsm:start(?MODULE, [Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, Nick, DefRoomOpts, QueueType], ?FSMOPTS). start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType) -> - gen_fsm:start(?MODULE, [Host, ServerHost, Access, Room, HistorySize, + p1_fsm:start(?MODULE, [Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType], ?FSMOPTS). start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, Nick, DefRoomOpts, QueueType) -> - gen_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, + p1_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, Nick, DefRoomOpts, QueueType], ?FSMOPTS). start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType) -> - gen_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, + p1_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType], ?FSMOPTS). @@ -703,7 +703,7 @@ terminate(Reason, _StateName, StateData) -> -spec route(pid(), stanza()) -> ok. route(Pid, Packet) -> #jid{lresource = Nick} = xmpp:get_to(Packet), - gen_fsm:send_event(Pid, {route, Nick, Packet}). + p1_fsm:send_event(Pid, {route, Nick, Packet}). -spec process_groupchat_message(message(), state()) -> fsm_next(). process_groupchat_message(#message{from = From, lang = Lang} = Packet, StateData) -> @@ -4004,6 +4004,7 @@ tab_add_online_user(JID, StateData) -> Room = StateData#state.room, Host = StateData#state.host, ServerHost = StateData#state.server_host, + ejabberd_hooks:run(join_room, ServerHost, [ServerHost, Room, Host, JID]), mod_muc:register_online_user(ServerHost, jid:tolower(JID), Room, Host). -spec tab_remove_online_user(jid(), state()) -> any(). @@ -4011,6 +4012,7 @@ tab_remove_online_user(JID, StateData) -> Room = StateData#state.room, Host = StateData#state.host, ServerHost = StateData#state.server_host, + ejabberd_hooks:run(leave_room, ServerHost, [ServerHost, Room, Host, JID]), mod_muc:unregister_online_user(ServerHost, jid:tolower(JID), Room, Host). -spec tab_count_user(jid(), state()) -> non_neg_integer(). diff --git a/src/mod_multicast.erl b/src/mod_multicast.erl index 4d3e481a7..e10315b7a 100644 --- a/src/mod_multicast.erl +++ b/src/mod_multicast.erl @@ -998,8 +998,10 @@ build_service_limit_record(LimitOpts) -> build_limit_record(LimitOptsR, remote)}. get_from_limitopts(LimitOpts, SenderT) -> - {SenderT, Result} = lists:keyfind(SenderT, 1, LimitOpts), - Result. + case lists:keyfind(SenderT, 1, LimitOpts) of + false -> []; + {SenderT, Result} -> Result + end. build_remote_limit_record(LimitOpts, SenderT) -> build_limit_record(LimitOpts, SenderT). @@ -1120,10 +1122,10 @@ mod_opt_type(host) -> fun iolist_to_binary/1; 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 + fun ({message, infinite} = O) -> O; + ({presence, infinite} = O) -> O; + ({message, I} = O) when is_integer(I) -> O; + ({presence, I} = O) when is_integer(I) -> O end, L) end; mod_opt_type(_) -> [access, host, {limits, local}, {limits, remote}]. diff --git a/src/mod_proxy65.erl b/src/mod_proxy65.erl index aee324960..671baef9a 100644 --- a/src/mod_proxy65.erl +++ b/src/mod_proxy65.erl @@ -112,6 +112,8 @@ 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(hosts) -> + fun(L) -> lists:map(fun iolist_to_binary/1, L) end; mod_opt_type(hostname) -> fun iolist_to_binary/1; mod_opt_type(ip) -> fun (S) -> @@ -131,7 +133,7 @@ mod_opt_type(ram_db_type) -> mod_opt_type(Opt) -> case mod_proxy65_stream:listen_opt_type(Opt) of Opts when is_list(Opts) -> - [access, host, hostname, ip, name, port, + [access, host, hosts, hostname, ip, name, port, max_connections, ram_db_type] ++ Opts; Fun -> Fun diff --git a/src/mod_proxy65_service.erl b/src/mod_proxy65_service.erl index b27f3bc20..aaece980a 100644 --- a/src/mod_proxy65_service.erl +++ b/src/mod_proxy65_service.erl @@ -43,7 +43,7 @@ -define(PROCNAME, ejabberd_mod_proxy65_service). --record(state, {myhost = <<"">> :: binary()}). +-record(state, {myhosts = [] :: [binary()]}). %%%------------------------ %%% gen_server callbacks @@ -61,24 +61,27 @@ reload(Host, NewOpts, OldOpts) -> init([Host, Opts]) -> process_flag(trap_exit, true), IQDisc = gen_mod:get_opt(iqdisc, Opts, gen_iq_handler:iqdisc(Host)), - MyHost = gen_mod:get_opt_host(Host, Opts, <<"proxy.@HOST@">>), - gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_INFO, - ?MODULE, process_disco_info, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_ITEMS, - ?MODULE, process_disco_items, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_VCARD, - ?MODULE, process_vcard, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_BYTESTREAMS, - ?MODULE, process_bytestreams, IQDisc), - ejabberd_router:register_route(MyHost, Host), - {ok, #state{myhost = MyHost}}. + MyHosts = gen_mod:get_opt_hosts(Host, Opts, <<"proxy.@HOST@">>), + lists:foreach( + fun(MyHost) -> + gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_INFO, + ?MODULE, process_disco_info, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_ITEMS, + ?MODULE, process_disco_items, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_VCARD, + ?MODULE, process_vcard, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_BYTESTREAMS, + ?MODULE, process_bytestreams, IQDisc), + ejabberd_router:register_route(MyHost, Host) + end, MyHosts), + {ok, #state{myhosts = MyHosts}}. -terminate(_Reason, #state{myhost = MyHost}) -> - ejabberd_router:unregister_route(MyHost), - gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_INFO), - gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_ITEMS), - gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_VCARD), - gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_BYTESTREAMS). +terminate(_Reason, #state{myhosts = MyHosts}) -> + lists:foreach( + fun(MyHost) -> + ejabberd_router:unregister_route(MyHost), + unregister_handlers(MyHost) + end, MyHosts). handle_info({route, #iq{} = Packet}, State) -> ejabberd_router:process_iq(Packet), @@ -89,33 +92,29 @@ handle_call(_Request, _From, State) -> {reply, ok, State}. handle_cast({reload, ServerHost, NewOpts, OldOpts}, State) -> - NewHost = gen_mod:get_opt_host(ServerHost, NewOpts, <<"proxy.@HOST@">>), - OldHost = gen_mod:get_opt_host(ServerHost, OldOpts, <<"proxy.@HOST@">>), + NewHosts = gen_mod:get_opt_hosts(ServerHost, NewOpts, <<"proxy.@HOST@">>), + OldHosts = gen_mod:get_opt_hosts(ServerHost, OldOpts, <<"proxy.@HOST@">>), NewIQDisc = gen_mod:get_opt(iqdisc, NewOpts, gen_iq_handler:iqdisc(ServerHost)), OldIQDisc = gen_mod:get_opt(iqdisc, OldOpts, gen_iq_handler:iqdisc(ServerHost)), - if (NewIQDisc /= OldIQDisc) or (NewHost /= OldHost) -> - gen_iq_handler:add_iq_handler(ejabberd_local, NewHost, ?NS_DISCO_INFO, - ?MODULE, process_disco_info, NewIQDisc), - gen_iq_handler:add_iq_handler(ejabberd_local, NewHost, ?NS_DISCO_ITEMS, - ?MODULE, process_disco_items, NewIQDisc), - gen_iq_handler:add_iq_handler(ejabberd_local, NewHost, ?NS_VCARD, - ?MODULE, process_vcard, NewIQDisc), - gen_iq_handler:add_iq_handler(ejabberd_local, NewHost, ?NS_BYTESTREAMS, - ?MODULE, process_bytestreams, NewIQDisc); + if (NewIQDisc /= OldIQDisc) -> + lists:foreach( + fun(NewHost) -> + register_handlers(NewHost, NewIQDisc) + end, NewHosts -- (NewHosts -- OldHosts)); true -> ok end, - if NewHost /= OldHost -> - ejabberd_router:register_route(NewHost, ServerHost), - ejabberd_router:unregister_route(OldHost), - gen_iq_handler:remove_iq_handler(ejabberd_local, OldHost, ?NS_DISCO_INFO), - gen_iq_handler:remove_iq_handler(ejabberd_local, OldHost, ?NS_DISCO_ITEMS), - gen_iq_handler:remove_iq_handler(ejabberd_local, OldHost, ?NS_VCARD), - gen_iq_handler:remove_iq_handler(ejabberd_local, OldHost, ?NS_BYTESTREAMS); - true -> - ok - end, - {noreply, State#state{myhost = NewHost}}; + lists:foreach( + fun(NewHost) -> + ejabberd_router:register_route(NewHost, ServerHost), + register_handlers(NewHost, NewIQDisc) + end, NewHosts -- OldHosts), + lists:foreach( + fun(OldHost) -> + ejabberd_router:unregister_route(OldHost), + unregister_handlers(OldHost) + end, OldHosts -- NewHosts), + {noreply, State#state{myhosts = NewHosts}}; handle_cast(Msg, State) -> ?WARNING_MSG("unexpected cast: ~p", [Msg]), {noreply, State}. @@ -276,3 +275,19 @@ get_my_ip() -> max_connections(ServerHost) -> gen_mod:get_module_opt(ServerHost, mod_proxy65, max_connections, infinity). + +register_handlers(Host, IQDisc) -> + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO, + ?MODULE, process_disco_info, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS, + ?MODULE, process_disco_items, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_VCARD, + ?MODULE, process_vcard, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_BYTESTREAMS, + ?MODULE, process_bytestreams, IQDisc). + +unregister_handlers(Host) -> + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_BYTESTREAMS). diff --git a/src/mod_proxy65_stream.erl b/src/mod_proxy65_stream.erl index 899b66727..1fa6ff804 100644 --- a/src/mod_proxy65_stream.erl +++ b/src/mod_proxy65_stream.erl @@ -26,7 +26,7 @@ -author('xram@jabber.ru'). --behaviour(gen_fsm). +-behaviour(p1_fsm). %% gen_fsm callbacks. -export([init/1, handle_event/3, handle_sync_event/4, @@ -75,7 +75,7 @@ start({gen_tcp, Socket}, Opts1) -> [Socket, Host, Opts]). start_link(Socket, Host, Opts) -> - gen_fsm:start_link(?MODULE, [Socket, Host, Opts], []). + p1_fsm:start_link(?MODULE, [Socket, Host, Opts], []). init([Socket, Host, Opts]) -> process_flag(trap_exit, true), @@ -106,9 +106,9 @@ socket_type() -> raw. stop(StreamPid) -> StreamPid ! stop. activate({P1, J1}, {P2, J2}) -> - case catch {gen_fsm:sync_send_all_state_event(P1, + case catch {p1_fsm:sync_send_all_state_event(P1, get_socket), - gen_fsm:sync_send_all_state_event(P2, get_socket)} + p1_fsm:sync_send_all_state_event(P2, get_socket)} of {S1, S2} when is_port(S1), is_port(S2) -> P1 ! {activate, P2, S2, J1, J2}, @@ -197,7 +197,7 @@ handle_info({tcp, _S, Data}, StateName, StateData) when StateName /= wait_for_activation -> erlang:cancel_timer(StateData#state.timer), TRef = erlang:send_after(?WAIT_TIMEOUT, self(), stop), - gen_fsm:send_event(self(), Data), + p1_fsm:send_event(self(), Data), {next_state, StateName, StateData#state{timer = TRef}}; %% Activation message. handle_info({activate, PeerPid, PeerSocket, IJid, TJid}, diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl index 1a620cb6b..a67ae5bfc 100644 --- a/src/mod_pubsub.erl +++ b/src/mod_pubsub.erl @@ -52,7 +52,7 @@ %% exports for hooks -export([presence_probe/3, caps_add/3, caps_update/3, in_subscription/6, out_subscription/4, - on_user_offline/3, remove_user/2, + on_user_online/1, on_user_offline/2, remove_user/2, disco_local_identity/5, disco_local_features/5, disco_local_items/5, disco_sm_identity/5, disco_sm_features/5, disco_sm_items/5, @@ -89,11 +89,7 @@ %% API and gen_server callbacks -export([start/2, stop/1, init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3, depends/2]). - --export([send_loop/1, mod_opt_type/1]). - --define(LOOPNAME, ejabberd_mod_pubsub_loop). + terminate/2, code_change/3, depends/2, export/1, mod_opt_type/1]). %%==================================================================== %% API @@ -189,7 +185,7 @@ -record(state, { server_host, - host, + hosts, access, pep_mapping = [], ignore_pep_from_offline = true, @@ -205,7 +201,7 @@ -type(state() :: #state{ server_host :: binary(), - host :: mod_pubsub:hostPubsub(), + hosts :: [mod_pubsub:hostPubsub()], access :: atom(), pep_mapping :: [{binary(), binary()}], ignore_pep_from_offline :: boolean(), @@ -243,43 +239,66 @@ stop(Host) -> init([ServerHost, Opts]) -> process_flag(trap_exit, true), ?DEBUG("pubsub init ~p ~p", [ServerHost, Opts]), - Host = gen_mod:get_opt_host(ServerHost, Opts, <<"pubsub.@HOST@">>), - ejabberd_router:register_route(Host, ServerHost), + Hosts = gen_mod:get_opt_hosts(ServerHost, Opts, <<"pubsub.@HOST@">>), Access = gen_mod:get_opt(access_createnode, Opts, all), PepOffline = gen_mod:get_opt(ignore_pep_from_offline, Opts, true), - IQDisc = gen_mod:get_opt(iqdisc, Opts, gen_iq_handler:iqdisc(Host)), + IQDisc = gen_mod:get_opt(iqdisc, Opts, gen_iq_handler:iqdisc(ServerHost)), LastItemCache = gen_mod:get_opt(last_item_cache, Opts, false), MaxItemsNode = gen_mod:get_opt(max_items_node, Opts, ?MAXITEMS), MaxSubsNode = gen_mod:get_opt(max_subscriptions_node, Opts), - case gen_mod:db_type(ServerHost, ?MODULE) of - mnesia -> pubsub_index:init(Host, ServerHost, Opts); - _ -> ok - end, - {Plugins, NodeTree, PepMapping} = init_plugins(Host, ServerHost, Opts), - DefaultModule = plugin(Host, hd(Plugins)), - BaseOptions = DefaultModule:options(), - DefaultNodeCfg = filter_node_options( - gen_mod:get_opt(default_node_config, Opts, []), - BaseOptions), ejabberd_mnesia:create(?MODULE, pubsub_last_item, - [{ram_copies, [node()]}, - {attributes, record_info(fields, pubsub_last_item)}]), - lists:foreach( - fun(H) -> - T = gen_mod:get_module_proc(H, config), - ets:new(T, [set, named_table]), - ets:insert(T, {nodetree, NodeTree}), - ets:insert(T, {plugins, Plugins}), - ets:insert(T, {last_item_cache, LastItemCache}), - ets:insert(T, {max_items_node, MaxItemsNode}), - ets:insert(T, {max_subscriptions_node, MaxSubsNode}), - ets:insert(T, {default_node_config, DefaultNodeCfg}), - ets:insert(T, {pep_mapping, PepMapping}), - ets:insert(T, {ignore_pep_from_offline, PepOffline}), - ets:insert(T, {host, Host}), - ets:insert(T, {access, Access}) - end, [Host, ServerHost]), - ejabberd_hooks:add(sm_remove_connection_hook, ServerHost, + [{ram_copies, [node()]}, + {attributes, record_info(fields, pubsub_last_item)}]), + AllPlugins = + lists:flatmap( + fun(Host) -> + ejabberd_router:register_route(Host, ServerHost), + case gen_mod:db_type(ServerHost, ?MODULE) of + mnesia -> pubsub_index:init(Host, ServerHost, Opts); + _ -> ok + end, + {Plugins, NodeTree, PepMapping} = init_plugins(Host, ServerHost, Opts), + DefaultModule = plugin(Host, hd(Plugins)), + BaseOptions = DefaultModule:options(), + DefaultNodeCfg = filter_node_options( + gen_mod:get_opt(default_node_config, Opts, []), + BaseOptions), + lists:foreach( + fun(H) -> + T = gen_mod:get_module_proc(H, config), + try + ets:new(T, [set, named_table]), + ets:insert(T, {nodetree, NodeTree}), + ets:insert(T, {plugins, Plugins}), + ets:insert(T, {last_item_cache, LastItemCache}), + ets:insert(T, {max_items_node, MaxItemsNode}), + ets:insert(T, {max_subscriptions_node, MaxSubsNode}), + ets:insert(T, {default_node_config, DefaultNodeCfg}), + ets:insert(T, {pep_mapping, PepMapping}), + ets:insert(T, {ignore_pep_from_offline, PepOffline}), + ets:insert(T, {host, Host}), + ets:insert(T, {access, Access}) + catch error:badarg when H == ServerHost -> + ok + end + end, [Host, ServerHost]), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO, + ?MODULE, process_disco_info, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS, + ?MODULE, process_disco_items, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_PUBSUB, + ?MODULE, process_pubsub, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_PUBSUB_OWNER, + ?MODULE, process_pubsub_owner, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_VCARD, + ?MODULE, process_vcard, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_COMMANDS, + ?MODULE, process_commands, IQDisc), + Plugins + end, Hosts), + ejabberd_hooks:add(c2s_session_opened, ServerHost, + ?MODULE, on_user_online, 75), + ejabberd_hooks:add(c2s_terminated, ServerHost, ?MODULE, on_user_offline, 75), ejabberd_hooks:add(disco_local_identity, ServerHost, ?MODULE, disco_local_identity, 75), @@ -297,19 +316,7 @@ init([ServerHost, Opts]) -> ?MODULE, remove_user, 50), ejabberd_hooks:add(c2s_handle_info, ServerHost, ?MODULE, c2s_handle_info, 50), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO, - ?MODULE, process_disco_info, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS, - ?MODULE, process_disco_items, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_PUBSUB, - ?MODULE, process_pubsub, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_PUBSUB_OWNER, - ?MODULE, process_pubsub_owner, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_VCARD, - ?MODULE, process_vcard, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_COMMANDS, - ?MODULE, process_commands, IQDisc), - case lists:member(?PEPNODE, Plugins) of + case lists:member(?PEPNODE, AllPlugins) of true -> ejabberd_hooks:add(caps_add, ServerHost, ?MODULE, caps_add, 80), @@ -328,35 +335,16 @@ init([ServerHost, Opts]) -> false -> ok end, - {_, State} = init_send_loop(ServerHost), - {ok, State}. - -init_send_loop(ServerHost) -> NodeTree = config(ServerHost, nodetree), Plugins = config(ServerHost, plugins), - LastItemCache = config(ServerHost, last_item_cache), - MaxItemsNode = config(ServerHost, max_items_node), PepMapping = config(ServerHost, pep_mapping), - PepOffline = config(ServerHost, ignore_pep_from_offline), - Host = config(ServerHost, host), - Access = config(ServerHost, access), DBType = gen_mod:db_type(ServerHost, ?MODULE), - State = #state{host = Host, server_host = ServerHost, - access = Access, pep_mapping = PepMapping, - ignore_pep_from_offline = PepOffline, - last_item_cache = LastItemCache, - max_items_node = MaxItemsNode, nodetree = NodeTree, - plugins = Plugins, db_type = DBType}, - Proc = gen_mod:get_module_proc(ServerHost, ?LOOPNAME), - Pid = case whereis(Proc) of - undefined -> - SendLoop = spawn(?MODULE, send_loop, [State]), - register(Proc, SendLoop), - SendLoop; - Loop -> - Loop - end, - {Pid, State}. + {ok, #state{hosts = Hosts, server_host = ServerHost, + access = Access, pep_mapping = PepMapping, + ignore_pep_from_offline = PepOffline, + last_item_cache = LastItemCache, + max_items_node = MaxItemsNode, nodetree = NodeTree, + plugins = Plugins, db_type = DBType}}. depends(ServerHost, Opts) -> Host = gen_mod:get_opt_host(ServerHost, Opts, <<"pubsub.@HOST@">>), @@ -406,94 +394,6 @@ terminate_plugins(Host, ServerHost, Plugins, TreePlugin) -> TreePlugin:terminate(Host, ServerHost), ok. -get_subscribed(User, Server) -> - Items = ejabberd_hooks:run_fold(roster_get, Server, [], [{User, Server}]), - lists:filtermap( - fun(#roster{jid = LJID, subscription = Sub}) - when Sub == both orelse Sub == from -> - {true, LJID}; - (_) -> - false - end, Items). - -send_loop(State) -> - receive - {presence, JID, _Pid} -> - Host = State#state.host, - ServerHost = State#state.server_host, - DBType = State#state.db_type, - LJID = jid:tolower(JID), - BJID = jid:remove_resource(LJID), - lists:foreach( - fun(PType) -> - Subs = get_subscriptions_for_send_last(Host, PType, DBType, JID, LJID, BJID), - lists:foreach( - fun({NodeRec, _, _, SubJID}) -> - {_, Node} = NodeRec#pubsub_node.nodeid, - Nidx = NodeRec#pubsub_node.id, - Options = NodeRec#pubsub_node.options, - [send_items(Host, Node, Nidx, PType, Options, SubJID, last) - || NodeRec#pubsub_node.type == PType] - end, - lists:usort(Subs)) - end, - State#state.plugins), - if not State#state.ignore_pep_from_offline -> - {User, Server, Resource} = LJID, - Contacts = get_subscribed(User, Server), - lists:foreach( - fun({U, S, R}) when S == ServerHost -> - case user_resources(U, S) of - [] -> %% offline - PeerJID = jid:make(U, S, R), - self() ! {presence, User, Server, [Resource], PeerJID}; - _ -> %% online - %% this is already handled by presence probe - ok - end; - (_) -> - %% we can not do anything in any cases - ok - end, Contacts); - true -> - ok - end, - send_loop(State); - {presence, User, Server, Resources, JID} -> - spawn(fun() -> - Host = State#state.host, - Owner = jid:remove_resource(jid:tolower(JID)), - lists:foreach(fun(#pubsub_node{nodeid = {_, Node}, type = Type, id = Nidx, options = Options}) -> - case match_option(Options, send_last_published_item, on_sub_and_presence) of - true -> - lists:foreach(fun(Resource) -> - LJID = {User, Server, Resource}, - Subscribed = case get_option(Options, access_model) of - open -> true; - presence -> true; - whitelist -> false; % subscribers are added manually - authorize -> false; % likewise - roster -> - Grps = get_option(Options, roster_groups_allowed, []), - {OU, OS, _} = Owner, - element(2, get_roster_info(OU, OS, LJID, Grps)) - end, - if Subscribed -> send_items(Owner, Node, Nidx, Type, Options, LJID, last); - true -> ok - end - end, - Resources); - _ -> - ok - end - end, - tree_action(Host, get_nodes, [Owner, JID])) - end), - send_loop(State); - stop -> - ok - end. - %% ------- %% disco hooks handling functions %% @@ -546,13 +446,11 @@ disco_identity(Host, Node, From) -> case get_allowed_items_call(Host, Nidx, From, Type, Options, Owners) of {result, _} -> - {result, [#identity{category = <<"pubsub">>, - type = <<"pep">>}, - #identity{category = <<"pubsub">>, - type = <<"leaf">>, + {result, [#identity{category = <<"pubsub">>, type = <<"pep">>}, + #identity{category = <<"pubsub">>, type = <<"leaf">>, name = case get_option(Options, title) of false -> <<>>; - [Title] -> Title + Title -> Title end}]}; _ -> {result, []} @@ -586,8 +484,7 @@ disco_features(Host, Node, From) -> Type, Options, Owners) of {result, _} -> {result, - [?NS_PUBSUB | - [feature(F) || F <- plugin_features(Host, <<"pep">>)]]}; + [?NS_PUBSUB | [feature(F) || F <- plugin_features(Host, <<"pep">>)]]}; _ -> {result, []} end @@ -620,7 +517,7 @@ disco_items(Host, <<>>, From) -> jid = jid:make(Host), name = case get_option(Options, title) of false -> <<>>; - [Title] -> Title + Title -> Title end} | Acc]; _ -> Acc @@ -655,12 +552,12 @@ disco_items(Host, Node, From) -> end. %% ------- -%% presence hooks handling functions +%% presence and session hooks handling functions %% -spec caps_add(jid(), jid(), [binary()]) -> ok. -caps_add(#jid{luser = U, lserver = S, lresource = R}, #jid{lserver = Host} = JID, _Features) - when Host =/= S -> +caps_add(#jid{lserver = S1} = From, #jid{lserver = S2} = To, _Features) + when S1 =/= S2 -> %% When a remote contact goes online while the local user is offline, the %% remote contact won't receive last items from the local user even if %% ignore_pep_from_offline is set to false. To work around this issue a bit, @@ -670,36 +567,36 @@ caps_add(#jid{luser = U, lserver = S, lresource = R}, #jid{lserver = Host} = JID %% contact becomes available; the former is also executed when the local %% user goes online (because that triggers the contact to send a presence %% packet with CAPS). - presence(Host, {presence, U, S, [R], JID}); + send_last_pep(To, From); caps_add(_From, _To, _Feature) -> ok. -spec caps_update(jid(), jid(), [binary()]) -> ok. -caps_update(#jid{luser = U, lserver = S, lresource = R}, #jid{lserver = Host} = JID, _Features) -> - presence(Host, {presence, U, S, [R], JID}). +caps_update(From, To, _Features) -> + send_last_pep(To, From). -spec presence_probe(jid(), jid(), pid()) -> ok. -presence_probe(#jid{luser = U, lserver = S, lresource = R} = JID, JID, Pid) -> - presence(S, {presence, JID, Pid}), - presence(S, {presence, U, S, [R], JID}); presence_probe(#jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}, _Pid) -> %% ignore presence_probe from my other ressources - %% to not get duplicated last items ok; -presence_probe(#jid{luser = U, lserver = S, lresource = R}, #jid{lserver = S} = JID, _Pid) -> - presence(S, {presence, U, S, [R], JID}); -presence_probe(_Host, _JID, _Pid) -> - %% ignore presence_probe from remote contacts, - %% those are handled via caps_add +presence_probe(#jid{lserver = S} = From, #jid{lserver = S} = To, _Pid) -> + send_last_pep(To, From); +presence_probe(_From, _To, _Pid) -> + %% ignore presence_probe from remote contacts, those are handled via caps_add ok. -presence(ServerHost, Presence) -> - {SendLoop, _} = case whereis(gen_mod:get_module_proc(ServerHost, ?LOOPNAME)) of - undefined -> init_send_loop(ServerHost); - Pid -> {Pid, undefined} - end, - SendLoop ! Presence, - ok. +-spec on_user_online(ejabberd_c2s:state()) -> ejabberd_c2s:state(). +on_user_online(C2SState) -> + JID = maps:get(jid, C2SState), + send_last_items(JID), + C2SState. + +-spec on_user_offline(ejabberd_c2s:state(), atom()) -> ejabberd_c2s:state(). +on_user_offline(#{jid := JID} = C2SState, _Reason) -> + purge_offline(jid:tolower(JID)), + C2SState; +on_user_offline(C2SState, _Reason) -> + C2SState. %% ------- %% subscription hooks handling functions @@ -708,14 +605,8 @@ presence(ServerHost, Presence) -> -spec out_subscription( binary(), binary(), jid(), subscribed | unsubscribed | subscribe | unsubscribe) -> boolean(). -out_subscription(User, Server, JID, subscribed) -> - Owner = jid:make(User, Server), - {PUser, PServer, PResource} = jid:tolower(JID), - PResources = case PResource of - <<>> -> user_resources(PUser, PServer); - _ -> [PResource] - end, - presence(Server, {presence, PUser, PServer, PResources, Owner}), +out_subscription(User, Server, To, subscribed) -> + send_last_pep(jid:make(User, Server), To), true; out_subscription(_, _, _, _) -> true. @@ -864,7 +755,7 @@ handle_info(_Info, State) -> %%-------------------------------------------------------------------- %% @private terminate(_Reason, - #state{host = Host, server_host = ServerHost, nodetree = TreePlugin, plugins = Plugins}) -> + #state{hosts = Hosts, server_host = ServerHost, nodetree = TreePlugin, plugins = Plugins}) -> case lists:member(?PEPNODE, Plugins) of true -> ejabberd_hooks:delete(caps_add, ServerHost, @@ -884,7 +775,9 @@ terminate(_Reason, false -> ok end, - ejabberd_hooks:delete(sm_remove_connection_hook, ServerHost, + ejabberd_hooks:delete(c2s_session_opened, ServerHost, + ?MODULE, on_user_online, 75), + ejabberd_hooks:delete(c2s_terminated, ServerHost, ?MODULE, on_user_offline, 75), ejabberd_hooks:delete(disco_local_identity, ServerHost, ?MODULE, disco_local_identity, 75), @@ -902,20 +795,17 @@ terminate(_Reason, ?MODULE, remove_user, 50), ejabberd_hooks:delete(c2s_handle_info, ServerHost, ?MODULE, c2s_handle_info, 50), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_PUBSUB), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_PUBSUB_OWNER), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_COMMANDS), - case whereis(gen_mod:get_module_proc(ServerHost, ?LOOPNAME)) of - undefined -> - ?ERROR_MSG("~s process is dead, pubsub was broken", [?LOOPNAME]); - Pid -> - Pid ! stop - end, - terminate_plugins(Host, ServerHost, Plugins, TreePlugin), - ejabberd_router:unregister_route(Host). + lists:foreach( + fun(Host) -> + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_PUBSUB), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_PUBSUB_OWNER), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_COMMANDS), + terminate_plugins(Host, ServerHost, Plugins, TreePlugin), + ejabberd_router:unregister_route(Host) + end, Hosts). %%-------------------------------------------------------------------- %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} @@ -1723,7 +1613,7 @@ delete_node(Host, Node, Owner) -> %%
  • The node does not support subscriptions.
  • %%
  • The node does not exist.
  • %% --spec subscribe_node(host(), binary(), jid(), binary(), [{binary(), [binary()]}]) -> +-spec subscribe_node(host(), binary(), jid(), jid(), [{binary(), [binary()]}]) -> {result, pubsub()} | {error, stanza_error()}. subscribe_node(Host, Node, From, JID, Configuration) -> SubModule = subscription_plugin(Host), @@ -1795,7 +1685,7 @@ subscribe_node(Host, Node, From, JID, Configuration) -> Nidx = TNode#pubsub_node.id, Type = TNode#pubsub_node.type, Options = TNode#pubsub_node.options, - send_items(Host, Node, Nidx, Type, Options, Subscriber, last), + send_items(Host, Node, Nidx, Type, Options, Subscriber, 1), ServerHost = serverhost(Host), ejabberd_hooks:run(pubsub_subscribe_node, ServerHost, [ServerHost, Host, Node, Subscriber, SubId]), @@ -2067,8 +1957,6 @@ purge_node(Host, Node, Owner) -> %% @doc

    Return the items of a given node.

    %%

    The number of items to return is limited by MaxItems.

    %%

    The permission are not checked in this function.

    -%% @todo We probably need to check that the user doing the query has the right -%% to read the items. -spec get_items(host(), binary(), jid(), binary(), binary(), [binary()], undefined | rsm_set()) -> {result, pubsub()} | {error, stanza_error()}. @@ -2151,38 +2039,23 @@ get_allowed_items_call(Host, Nidx, From, Type, Options, Owners, RSM) -> {PS, RG} = get_presence_and_roster_permissions(Host, From, Owners, AccessModel, AllowedGroups), node_call(Host, Type, get_items, [Nidx, From, AccessModel, PS, RG, undefined, RSM]). -get_last_items(Host, Type, Nidx, LJID, Count) -> +get_last_items(Host, Type, Nidx, LJID, 1) -> + case get_cached_item(Host, Nidx) of + undefined -> + case node_action(Host, Type, get_last_items, [Nidx, LJID, 1]) of + {result, Items} -> Items; + _ -> [] + end; + LastItem -> + [LastItem] + end; +get_last_items(Host, Type, Nidx, LJID, Count) when Count > 1 -> case node_action(Host, Type, get_last_items, [Nidx, LJID, Count]) of {result, Items} -> Items; _ -> [] - end. - -%% @doc

    Resend the items of a node to the user.

    -%% @todo use cache-last-item feature -send_items(Host, Node, Nidx, Type, Options, LJID, last) -> - case get_last_items(Host, Type, Nidx, LJID, 1) of - [LastItem] -> - Stanza = items_event_stanza(Node, Options, [LastItem]), - dispatch_items(Host, LJID, Node, Stanza); - _ -> - ok end; -send_items(Host, Node, Nidx, Type, Options, LJID, Number) when Number > 0 -> - Stanza = items_event_stanza(Node, Options, get_last_items(Host, Type, Nidx, Number, LJID)), - dispatch_items(Host, LJID, Node, Stanza); -send_items(Host, Node, _Nidx, _Type, Options, LJID, _) -> - Stanza = items_event_stanza(Node, Options, []), - dispatch_items(Host, LJID, Node, Stanza). - -dispatch_items({FromU, FromS, FromR}, To, Node, Stanza) -> - SenderResource = user_resource(FromU, FromS, FromR), - ejabberd_sm:route(jid:make(FromU, FromS, SenderResource), - {send_filtered, {pep_message, <<((Node))/binary, "+notify">>}, - jid:make(FromU, FromS), jid:make(To), - Stanza}); -dispatch_items(From, To, _Node, Stanza) -> - ejabberd_router:route( - xmpp:set_from_to(Stanza, service_jid(From), jid:make(To))). +get_last_items(_Host, _Type, _Nidx, _LJID, _Count) -> + []. %% @doc

    Return the list of affiliations as an XMPP response.

    -spec get_affiliations(host(), binary(), jid(), [binary()]) -> @@ -2529,14 +2402,15 @@ get_subscriptions_for_send_last(Host, PType, sql, JID, LJID, BJID) -> {result, Subs} = node_action(Host, PType, get_entity_subscriptions_for_send_last, [Host, JID]), - [{Node, Sub, SubId, SubJID} + [{Node, SubId, SubJID} || {Node, Sub, SubId, SubJID} <- Subs, Sub =:= subscribed, (SubJID == LJID) or (SubJID == BJID)]; + % sql version already filter result by on_sub_and_presence get_subscriptions_for_send_last(Host, PType, _, JID, LJID, BJID) -> {result, Subs} = node_action(Host, PType, get_entity_subscriptions, [Host, JID]), - [{Node, Sub, SubId, SubJID} + [{Node, SubId, SubJID} || {Node, Sub, SubId, SubJID} <- Subs, Sub =:= subscribed, (SubJID == LJID) or (SubJID == BJID), match_option(Node, send_last_published_item, on_sub_and_presence)]. @@ -2925,8 +2799,9 @@ get_options_for_subs(Host, Nidx, Subs, true) -> broadcast_stanza(Host, _Node, _Nidx, _Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) -> NotificationType = get_option(NodeOptions, notification_type, headline), BroadcastAll = get_option(NodeOptions, broadcast_all_resources), %% XXX this is not standard, but usefull - From = service_jid(Host), - Stanza = add_message_type(BaseStanza, NotificationType), + Stanza = add_message_type( + xmpp:set_from(BaseStanza, service_jid(Host)), + NotificationType), %% Handles explicit subscriptions SubIDsByJID = subscribed_nodes_by_jid(NotifyType, SubsByDepth), lists:foreach(fun ({LJID, _NodeName, SubIDs}) -> @@ -2949,7 +2824,7 @@ broadcast_stanza(Host, _Node, _Nidx, _Type, NodeOptions, SubsByDepth, NotifyType end, lists:foreach(fun(To) -> ejabberd_router:route( - xmpp:set_from_to(StanzaToSend, From, jid:make(To))) + xmpp:set_to(StanzaToSend, jid:make(To))) end, LJIDs) end, SubIDsByJID). @@ -2958,55 +2833,142 @@ broadcast_stanza({LUser, LServer, LResource}, Publisher, Node, Nidx, Type, NodeO %% Handles implicit presence subscriptions SenderResource = user_resource(LUser, LServer, LResource), NotificationType = get_option(NodeOptions, notification_type, headline), - Stanza = add_message_type(BaseStanza, NotificationType), + Stanza = add_message_type( + xmpp:set_from(BaseStanza, jid:make(LUser, LServer)), + NotificationType), %% set the from address on the notification to the bare JID of the account owner %% Also, add "replyto" if entity has presence subscription to the account owner %% See XEP-0163 1.1 section 4.3.1 ejabberd_sm:route(jid:make(LUser, LServer, SenderResource), {pep_message, <<((Node))/binary, "+notify">>, - jid:make(LUser, LServer), add_extended_headers( Stanza, extended_headers([Publisher]))}); broadcast_stanza(Host, _Publisher, Node, Nidx, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) -> broadcast_stanza(Host, Node, Nidx, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM). -spec c2s_handle_info(ejabberd_c2s:state(), term()) -> ejabberd_c2s:state(). -c2s_handle_info(#{server := Server} = C2SState, - {pep_message, Feature, From, Packet}) -> - LServer = jid:nameprep(Server), - lists:foreach( - fun({USR, Caps}) -> - Features = mod_caps:get_features(LServer, Caps), - case lists:member(Feature, Features) of - true -> - To = jid:make(USR), - NewPacket = xmpp:set_from_to(Packet, From, To), - ejabberd_router:route(NewPacket); - false -> - ok - end - end, mod_caps:list_features(C2SState)), +c2s_handle_info(#{lserver := LServer} = C2SState, + {pep_message, Feature, Packet}) -> + [maybe_send_pep_stanza(LServer, USR, Caps, Feature, Packet) + || {USR, Caps} <- mod_caps:list_features(C2SState)], {stop, C2SState}; -c2s_handle_info(#{server := Server} = C2SState, - {send_filtered, {pep_message, Feature}, From, To, Packet}) -> - LServer = jid:nameprep(Server), - case mod_caps:get_user_caps(To, C2SState) of - {ok, Caps} -> - Features = mod_caps:get_features(LServer, Caps), - case lists:member(Feature, Features) of - true -> - NewPacket = xmpp:set_from_to(Packet, From, To), - ejabberd_router:route(NewPacket); - false -> - ok - end; - error -> - ok +c2s_handle_info(#{lserver := LServer} = C2SState, + {pep_message, Feature, Packet, USR}) -> + case mod_caps:get_user_caps(USR, C2SState) of + {ok, Caps} -> maybe_send_pep_stanza(LServer, USR, Caps, Feature, Packet); + error -> ok end, {stop, C2SState}; c2s_handle_info(C2SState, _) -> C2SState. +send_items(Host, Node, Nidx, Type, Options, LJID, Number) -> + send_items(Host, Node, Nidx, Type, Options, Host, LJID, LJID, Number). +send_items(Host, Node, Nidx, Type, Options, Publisher, SubLJID, ToLJID, Number) -> + case get_last_items(Host, Type, Nidx, SubLJID, Number) of + [] -> + ok; + Items -> + Stanza = items_event_stanza(Node, Options, Items), + send_stanza(Publisher, ToLJID, Node, Stanza) + end. + +send_stanza({LUser, LServer, _} = Publisher, USR, Node, BaseStanza) -> + Stanza = xmpp:set_from(BaseStanza, jid:make(LUser, LServer)), + USRs = case USR of + {PUser, PServer, <<>>} -> + [{PUser, PServer, PRessource} + || PRessource <- user_resources(PUser, PServer)]; + _ -> + [USR] + end, + [ejabberd_sm:route(jid:make(Publisher), + {pep_message, <<((Node))/binary, "+notify">>, + add_extended_headers( + Stanza, extended_headers([Publisher])), + To}) || To <- USRs]; +send_stanza(Host, USR, _Node, Stanza) -> + ejabberd_router:route( + xmpp:set_from_to(Stanza, service_jid(Host), jid:make(USR))). + +maybe_send_pep_stanza(LServer, USR, Caps, Feature, Packet) -> + Features = mod_caps:get_features(LServer, Caps), + case lists:member(Feature, Features) of + true -> + ejabberd_router:route(xmpp:set_to(Packet, jid:make(USR))); + false -> + ok + end. + +send_last_items(JID) -> + ServerHost = JID#jid.lserver, + Host = host(ServerHost), + DBType = config(ServerHost, db_type), + LJID = jid:tolower(JID), + BJID = jid:remove_resource(LJID), + lists:foreach( + fun(PType) -> + Subs = get_subscriptions_for_send_last(Host, PType, DBType, JID, LJID, BJID), + lists:foreach( + fun({#pubsub_node{nodeid = {_, Node}, type = Type, id = Nidx, + options = Options}, _, SubJID}) + when Type == PType-> + send_items(Host, Node, Nidx, PType, Options, Host, SubJID, LJID, 1); + (_) -> + ok + end, + lists:usort(Subs)) + end, config(ServerHost, plugins)). +% pep_from_offline hack can not work anymore, as sender c2s does not +% exists when sender is offline, so we can't get match receiver caps +% does it make sens to send PEP from an offline contact anyway ? +% case config(ServerHost, ignore_pep_from_offline) of +% false -> +% Roster = ejabberd_hooks:run_fold(roster_get, ServerHost, [], +% [{JID#jid.luser, ServerHost}]), +% lists:foreach( +% fun(#roster{jid = {U, S, R}, subscription = Sub}) +% when Sub == both orelse Sub == from, +% S == ServerHost -> +% case user_resources(U, S) of +% [] -> send_last_pep(jid:make(U, S, R), JID); +% _ -> ok %% this is already handled by presence probe +% end; +% (_) -> +% ok %% we can not do anything in any cases +% end, Roster); +% true -> +% ok +% end. +send_last_pep(From, To) -> + ServerHost = From#jid.lserver, + Host = host(ServerHost), + Publisher = jid:tolower(From), + Owner = jid:remove_resource(Publisher), + lists:foreach( + fun(#pubsub_node{nodeid = {_, Node}, type = Type, id = Nidx, options = Options}) -> + case match_option(Options, send_last_published_item, on_sub_and_presence) of + true -> + LJID = jid:tolower(To), + Subscribed = case get_option(Options, access_model) of + open -> true; + presence -> true; + whitelist -> false; % subscribers are added manually + authorize -> false; % likewise + roster -> + Grps = get_option(Options, roster_groups_allowed, []), + {OU, OS, _} = Owner, + element(2, get_roster_info(OU, OS, LJID, Grps)) + end, + if Subscribed -> send_items(Owner, Node, Nidx, Type, Options, Publisher, LJID, LJID, 1); + true -> ok + end; + _ -> + ok + end + end, + tree_action(Host, get_nodes, [Owner, From])). + subscribed_nodes_by_jid(NotifyType, SubsByDepth) -> NodesToDeliver = fun (Depth, Node, Subs, Acc) -> NodeName = case Node#pubsub_node.nodeid of @@ -3179,9 +3141,6 @@ node_owners_call(_Host, _Type, _Nidx, Owners) -> %% @doc

    Return the maximum number of items for a given node.

    %%

    Unlimited means that there is no limit in the number of items that can %% be stored.

    -%% @todo In practice, the current data structure means that we cannot manage -%% millions of items on a given node. This should be addressed in a new -%% version. -spec max_items(host(), [{atom(), any()}]) -> non_neg_integer(). max_items(Host, Options) -> case get_option(Options, persist_items) of @@ -3785,14 +3744,6 @@ subid_shim(SubIds) -> extended_headers(Jids) -> [#address{type = replyto, jid = Jid} || Jid <- Jids]. --spec on_user_offline(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> ok. -on_user_offline(_, JID, _) -> - {User, Server, Resource} = jid:tolower(JID), - case user_resources(User, Server) of - [] -> purge_offline({User, Server, Resource}); - _ -> ok - end. - -spec purge_offline(ljid()) -> ok. purge_offline(LJID) -> Host = host(element(2, LJID)), @@ -3833,7 +3784,7 @@ purge_offline(LJID) -> end end, lists:usort(lists:flatten(Affs))); {Error, _} -> - ?DEBUG("on_user_offline ~p", [Error]) + ?ERROR_MSG("can not purge offline: ~p", [Error]) end. -spec purge_offline(host(), ljid(), binary()) -> ok | {error, stanza_error()}. @@ -3845,13 +3796,13 @@ purge_offline(Host, LJID, Node) -> {result, {[], _}} -> ok; {result, {Items, _}} -> - {User, Server, _} = LJID, + {User, Server, Resource} = LJID, PublishModel = get_option(Options, publish_model), ForceNotify = get_option(Options, notify_retract), {_, NodeId} = Node#pubsub_node.nodeid, lists:foreach(fun - (#pubsub_item{itemid = {ItemId, _}, modification = {_, {U, S, _}}}) - when (U == User) and (S == Server) -> + (#pubsub_item{itemid = {ItemId, _}, modification = {_, {U, S, R}}}) + when (U == User) and (S == Server) and (R == Resource) -> case node_action(Host, Type, delete_item, [Nidx, {U, S, <<>>}, PublishModel, ItemId]) of {result, {_, broadcast}} -> broadcast_retract_items(Host, NodeId, Nidx, Type, Options, [ItemId], ForceNotify), @@ -3871,9 +3822,14 @@ purge_offline(Host, LJID, Node) -> Error end. +export(Server) -> + pubsub_db_sql:export(Server). + mod_opt_type(access_createnode) -> fun acl:access_rules_validator/1; mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end; mod_opt_type(host) -> fun iolist_to_binary/1; +mod_opt_type(hosts) -> + fun (L) -> lists:map(fun iolist_to_binary/1, L) end; mod_opt_type(ignore_pep_from_offline) -> fun (A) when is_boolean(A) -> A end; mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; @@ -3892,7 +3848,7 @@ mod_opt_type(pep_mapping) -> mod_opt_type(plugins) -> fun (A) when is_list(A) -> A end; mod_opt_type(_) -> - [access_createnode, db_type, host, + [access_createnode, db_type, host, hosts, ignore_pep_from_offline, iqdisc, last_item_cache, max_items_node, nodetree, pep_mapping, plugins, max_subscriptions_node, default_node_config]. diff --git a/src/mod_push.erl b/src/mod_push.erl new file mode 100644 index 000000000..2ca0bf525 --- /dev/null +++ b/src/mod_push.erl @@ -0,0 +1,596 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_push.erl +%%% Author : Holger Weiss +%%% Purpose : Push Notifications (XEP-0357) +%%% Created : 15 Jul 2017 by Holger Weiss +%%% +%%% +%%% ejabberd, Copyright (C) 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(mod_push). +-author('holger@zedat.fu-berlin.de'). +-protocol({xep, 357, '0.2'}). + +-behavior(gen_mod). + +%% gen_mod callbacks. +-export([start/2, stop/1, reload/3, mod_opt_type/1, depends/2]). + +%% ejabberd_hooks callbacks. +-export([disco_sm_features/5, c2s_session_pending/1, c2s_copy_session/2, + c2s_handle_cast/2, c2s_stanza/3, mam_message/6, offline_message/1, + remove_user/2]). + +%% gen_iq_handler callback. +-export([process_iq/1]). + +%% ejabberd command. +-export([get_commands_spec/0, delete_old_sessions/1]). + +%% API (used by mod_push_keepalive). +-export([notify/1, notify/3, notify/5]). + +-include("ejabberd.hrl"). +-include("ejabberd_commands.hrl"). +-include("logger.hrl"). +-include("xmpp.hrl"). + +-define(PUSH_CACHE, push_cache). + +-type c2s_state() :: ejabberd_c2s:state(). +-type timestamp() :: erlang:timestamp(). +-type push_session() :: {timestamp(), ljid(), binary(), xdata()}. + +-callback init(binary(), gen_mod:opts()) + -> any(). +-callback store_session(binary(), binary(), timestamp(), jid(), binary(), + xdata()) + -> {ok, push_session()} | error. +-callback lookup_session(binary(), binary(), jid(), binary()) + -> {ok, push_session()} | error. +-callback lookup_session(binary(), binary(), timestamp()) + -> {ok, push_session()} | error. +-callback lookup_sessions(binary(), binary(), jid()) + -> {ok, [push_session()]} | error. +-callback lookup_sessions(binary(), binary()) + -> {ok, [push_session()]} | error. +-callback lookup_sessions(binary()) + -> {ok, [push_session()]} | error. +-callback delete_session(binary(), binary(), timestamp()) + -> ok | error. +-callback delete_old_sessions(binary() | global, erlang:timestamp()) + -> any(). +-callback use_cache(binary()) + -> boolean(). +-callback cache_nodes(binary()) + -> [node()]. + +-optional_callbacks([use_cache/1, cache_nodes/1]). + +%%-------------------------------------------------------------------- +%% gen_mod callbacks. +%%-------------------------------------------------------------------- +-spec start(binary(), gen_mod:opts()) -> ok. +start(Host, Opts) -> + IQDisc = gen_mod:get_opt(iqdisc, Opts, gen_iq_handler:iqdisc(Host)), + Mod = gen_mod:db_mod(Host, Opts, ?MODULE), + Mod:init(Host, Opts), + init_cache(Mod, Host, Opts), + register_iq_handlers(Host, IQDisc), + register_hooks(Host), + ejabberd_commands:register_commands(get_commands_spec()). + +-spec stop(binary()) -> ok. +stop(Host) -> + unregister_hooks(Host), + unregister_iq_handlers(Host), + ejabberd_commands:unregister_commands(get_commands_spec()). + +-spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. +reload(Host, NewOpts, OldOpts) -> + NewMod = gen_mod:db_mod(Host, NewOpts, ?MODULE), + OldMod = gen_mod:db_mod(Host, OldOpts, ?MODULE), + if NewMod /= OldMod -> + NewMod:init(Host, NewOpts); + true -> + ok + end, + case gen_mod:is_equal_opt(iqdisc, NewOpts, OldOpts, + gen_iq_handler:iqdisc(Host)) of + {false, IQDisc, _} -> + register_iq_handlers(Host, IQDisc); + true -> + ok + end. + +-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. +depends(_Host, _Opts) -> + []. + +-spec mod_opt_type(atom()) -> fun((term()) -> term()) | [atom()]. +mod_opt_type(db_type) -> + fun(T) -> ejabberd_config:v_db(?MODULE, T) end; +mod_opt_type(O) when O == cache_life_time; O == cache_size -> + fun(I) when is_integer(I), I > 0 -> I; + (infinity) -> infinity + end; +mod_opt_type(O) when O == use_cache; O == cache_missed -> + fun (B) when is_boolean(B) -> B end; +mod_opt_type(iqdisc) -> + fun gen_iq_handler:check_type/1; +mod_opt_type(_) -> + [db_type, cache_life_time, cache_size, use_cache, cache_missed, iqdisc]. + +%%-------------------------------------------------------------------- +%% ejabberd command callback. +%%-------------------------------------------------------------------- +-spec get_commands_spec() -> [ejabberd_commands()]. +get_commands_spec() -> + [#ejabberd_commands{name = delete_old_push_sessions, tags = [purge], + desc = "Remove push sessions older than DAYS", + module = ?MODULE, function = delete_old_sessions, + args = [{days, integer}], + result = {res, rescode}}]. + +-spec delete_old_sessions(non_neg_integer()) -> ok | any(). +delete_old_sessions(Days) -> + CurrentTime = p1_time_compat:system_time(micro_seconds), + Diff = Days * 24 * 60 * 60 * 1000000, + TimeStamp = misc:usec_to_now(CurrentTime - Diff), + DBTypes = lists:usort( + lists:map( + fun(Host) -> + case gen_mod:db_type(Host, ?MODULE) of + sql -> {sql, Host}; + Other -> {Other, global} + end + end, ?MYHOSTS)), + Results = lists:map( + fun({DBType, Host}) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:delete_old_sessions(Host, TimeStamp) + end, DBTypes), + ets_cache:clear(?PUSH_CACHE, ejabberd_cluster:get_nodes()), + case lists:filter(fun(Res) -> Res /= ok end, Results) of + [] -> + ?INFO_MSG("Deleted push sessions older than ~B days", [Days]), + ok; + [NotOk | _] -> + ?ERROR_MSG("Error while deleting old push sessions: ~p", [NotOk]), + NotOk + end. + +%%-------------------------------------------------------------------- +%% Register/unregister hooks. +%%-------------------------------------------------------------------- +-spec register_hooks(binary()) -> ok. +register_hooks(Host) -> + ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, + disco_sm_features, 50), + ejabberd_hooks:add(c2s_session_pending, Host, ?MODULE, + c2s_session_pending, 50), + ejabberd_hooks:add(c2s_copy_session, Host, ?MODULE, + c2s_copy_session, 50), + ejabberd_hooks:add(c2s_handle_cast, Host, ?MODULE, + c2s_handle_cast, 50), + ejabberd_hooks:add(c2s_handle_send, Host, ?MODULE, + c2s_stanza, 50), + ejabberd_hooks:add(store_mam_message, Host, ?MODULE, + mam_message, 50), + ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, + offline_message, 50), + ejabberd_hooks:add(remove_user, Host, ?MODULE, + remove_user, 50). + +-spec unregister_hooks(binary()) -> ok. +unregister_hooks(Host) -> + ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, + disco_sm_features, 50), + ejabberd_hooks:delete(c2s_session_pending, Host, ?MODULE, + c2s_session_pending, 50), + ejabberd_hooks:delete(c2s_copy_session, Host, ?MODULE, + c2s_copy_session, 50), + ejabberd_hooks:delete(c2s_handle_cast, Host, ?MODULE, + c2s_handle_cast, 50), + ejabberd_hooks:delete(c2s_handle_send, Host, ?MODULE, + c2s_stanza, 50), + ejabberd_hooks:delete(store_mam_message, Host, ?MODULE, + mam_message, 50), + ejabberd_hooks:delete(offline_message_hook, Host, ?MODULE, + offline_message, 50), + ejabberd_hooks:delete(remove_user, Host, ?MODULE, + remove_user, 50). + +%%-------------------------------------------------------------------- +%% Service discovery. +%%-------------------------------------------------------------------- +-spec disco_sm_features(empty | {result, [binary()]} | {error, stanza_error()}, + jid(), jid(), binary(), binary()) + -> {result, [binary()]} | {error, stanza_error()}. +disco_sm_features(empty, From, To, Node, Lang) -> + disco_sm_features({result, []}, From, To, Node, Lang); +disco_sm_features({result, OtherFeatures}, + #jid{luser = U, lserver = S}, + #jid{luser = U, lserver = S}, <<"">>, _Lang) -> + {result, [?NS_PUSH_0 | OtherFeatures]}; +disco_sm_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + +%%-------------------------------------------------------------------- +%% IQ handlers. +%%-------------------------------------------------------------------- +-spec register_iq_handlers(binary(), gen_iq_handler:type()) -> ok. +register_iq_handlers(Host, IQDisc) -> + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_PUSH_0, + ?MODULE, process_iq, IQDisc). + +-spec unregister_iq_handlers(binary()) -> ok. +unregister_iq_handlers(Host) -> + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_PUSH_0). + +-spec process_iq(iq()) -> iq(). +process_iq(#iq{type = get, lang = Lang} = IQ) -> + Txt = <<"Value 'get' of 'type' attribute is not allowed">>, + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_iq(#iq{lang = Lang, sub_els = [#push_enable{node = <<>>}]} = IQ) -> + Txt = <<"Enabling push without 'node' attribute is not supported">>, + xmpp:make_error(IQ, xmpp:err_feature_not_implemented(Txt, Lang)); +process_iq(#iq{from = #jid{lserver = LServer} = JID, + to = #jid{lserver = LServer}, + sub_els = [#push_enable{jid = PushJID, + node = Node, + xdata = XData}]} = IQ) -> + case enable(JID, PushJID, Node, XData) of + ok -> + xmpp:make_iq_result(IQ); + error -> + xmpp:make_error(IQ, xmpp:err_internal_server_error()) + end; +process_iq(#iq{from = #jid{lserver = LServer} = JID, + to = #jid{lserver = LServer}, + sub_els = [#push_disable{jid = PushJID, + node = Node}]} = IQ) -> + case disable(JID, PushJID, Node) of + ok -> + xmpp:make_iq_result(IQ); + error -> + xmpp:make_error(IQ, xmpp:err_item_not_found()) + end; +process_iq(IQ) -> + xmpp:make_error(IQ, xmpp:err_not_allowed()). + +-spec enable(jid(), jid(), binary(), xdata()) -> ok | error. +enable(#jid{luser = LUser, lserver = LServer, lresource = LResource} = JID, + PushJID, Node, XData) -> + case ejabberd_sm:get_session_sid(LUser, LServer, LResource) of + {TS, PID} -> + case store_session(LUser, LServer, TS, PushJID, Node, XData) of + {ok, _} -> + ?INFO_MSG("Enabling push notifications for ~s", + [jid:encode(JID)]), + ejabberd_c2s:cast(PID, push_enable); + error -> + ?ERROR_MSG("Cannot enable push for ~s: database error", + [jid:encode(JID)]), + error + end; + none -> + ?WARNING_MSG("Cannot enable push for ~s: session not found", + [jid:encode(JID)]), + error + end. + +-spec disable(jid(), jid(), binary() | undefined) -> ok | error. +disable(#jid{luser = LUser, lserver = LServer, lresource = LResource} = JID, + PushJID, Node) -> + case ejabberd_sm:get_session_sid(LUser, LServer, LResource) of + {_TS, PID} -> + ?INFO_MSG("Disabling push notifications for ~s", + [jid:encode(JID)]), + ejabberd_c2s:cast(PID, push_disable); + none -> + ?WARNING_MSG("Session not found while disabling push for ~s", + [jid:encode(JID)]) + end, + if Node /= undefined -> + delete_session(LUser, LServer, PushJID, Node); + true -> + delete_sessions(LUser, LServer, PushJID) + end. + +%%-------------------------------------------------------------------- +%% Hook callbacks. +%%-------------------------------------------------------------------- +-spec c2s_stanza(c2s_state(), xmpp_element() | xmlel(), term()) -> c2s_state(). +c2s_stanza(#{push_enabled := true, mgmt_state := pending} = State, + _Pkt, _SendResult) -> + notify(State), + State; +c2s_stanza(State, _Pkt, _SendResult) -> + State. + +-spec mam_message(message() | drop, binary(), binary(), jid(), + chat | groupchat, recv | send) -> message(). +mam_message(#message{meta = #{push_notified := true}} = Pkt, + _LUser, _LServer, _Peer, _Type, _Dir) -> + Pkt; +mam_message(#message{} = Pkt, LUser, LServer, _Peer, chat, _Dir) -> + case lookup_sessions(LUser, LServer) of + {ok, [_|_] = Clients} -> + case drop_online_sessions(LUser, LServer, Clients) of + [_|_] = Clients1 -> + ?DEBUG("Notifying ~s@~s of MAM message", [LUser, LServer]), + notify(LUser, LServer, Clients1); + [] -> + ok + end; + _ -> + ok + end, + xmpp:put_meta(Pkt, push_notified, true); +mam_message(Pkt, _LUser, _LServer, _Peer, _Type, _Dir) -> + Pkt. + +-spec offline_message({any(), message()}) -> {any(), message()}. +offline_message({_Action, #message{meta = #{push_notified := true}}} = Acc) -> + Acc; +offline_message({Action, #message{to = #jid{luser = LUser, + lserver = LServer}} = Pkt}) -> + case lookup_sessions(LUser, LServer) of + {ok, [_|_] = Clients} -> + ?DEBUG("Notifying ~s@~s of offline message", [LUser, LServer]), + notify(LUser, LServer, Clients); + _ -> + ok + end, + {Action, xmpp:put_meta(Pkt, push_notified, true)}. + +-spec c2s_session_pending(c2s_state()) -> c2s_state(). +c2s_session_pending(#{push_enabled := true, mgmt_queue := Queue} = State) -> + case p1_queue:len(Queue) of + Len when Len > 0 -> + ?DEBUG("Notifying client of unacknowledged messages", []), + notify(State), + State; + 0 -> + State + end; +c2s_session_pending(State) -> + State. + +-spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state(). +c2s_copy_session(State, #{push_enabled := true}) -> + State#{push_enabled => true}; +c2s_copy_session(State, _) -> + State. + +-spec c2s_handle_cast(c2s_state(), any()) -> c2s_state() | {stop, c2s_state()}. +c2s_handle_cast(State, push_enable) -> + {stop, State#{push_enabled => true}}; +c2s_handle_cast(State, push_disable) -> + {stop, maps:remove(push_enabled, State)}; +c2s_handle_cast(State, _Msg) -> + State. + +-spec remove_user(binary(), binary()) -> ok | error. +remove_user(LUser, LServer) -> + ?INFO_MSG("Removing any push sessions of ~s@~s", [LUser, LServer]), + Mod = gen_mod:db_mod(LServer, ?MODULE), + LookupFun = fun() -> Mod:lookup_sessions(LUser, LServer) end, + delete_sessions(LUser, LServer, LookupFun, Mod). + +%%-------------------------------------------------------------------- +%% Generate push notifications. +%%-------------------------------------------------------------------- +-spec notify(c2s_state()) -> ok. +notify(#{jid := #jid{luser = LUser, lserver = LServer}, sid := {TS, _}}) -> + case lookup_session(LUser, LServer, TS) of + {ok, Client} -> + notify(LUser, LServer, [Client]); + error -> + ok + end. + +-spec notify(binary(), binary(), [push_session()]) -> ok. +notify(LUser, LServer, Clients) -> + lists:foreach( + fun({TS, PushLJID, Node, XData}) -> + HandleResponse = fun(#iq{type = result}) -> + ok; + (#iq{type = error}) -> + delete_session(LUser, LServer, TS); + (timeout) -> + ok % Hmm. + end, + notify(LServer, PushLJID, Node, XData, HandleResponse) + end, Clients). + +-spec notify(binary(), ljid(), binary(), xdata(), + fun((iq() | timeout) -> any())) -> ok. +notify(LServer, PushLJID, Node, XData, HandleResponse) -> + From = jid:make(LServer), + Item = #ps_item{xml_els = [xmpp:encode(#push_notification{})]}, + PubSub = #pubsub{publish = #ps_publish{node = Node, items = [Item]}, + publish_options = XData}, + IQ = #iq{type = set, + from = From, + to = jid:make(PushLJID), + id = randoms:get_string(), + sub_els = [PubSub]}, + ejabberd_local:route_iq(IQ, HandleResponse), + ok. + +%%-------------------------------------------------------------------- +%% Internal functions. +%%-------------------------------------------------------------------- +-spec store_session(binary(), binary(), timestamp(), jid(), binary(), xdata()) + -> {ok, push_session()} | error. +store_session(LUser, LServer, TS, PushJID, Node, XData) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + delete_session(LUser, LServer, PushJID, Node), + case use_cache(Mod, LServer) of + true -> + ets_cache:delete(?PUSH_CACHE, {LUser, LServer}, + cache_nodes(Mod, LServer)), + ets_cache:update( + ?PUSH_CACHE, + {LUser, LServer, TS}, {ok, {TS, PushJID, Node, XData}}, + fun() -> + Mod:store_session(LUser, LServer, TS, PushJID, Node, + XData) + end, cache_nodes(Mod, LServer)); + false -> + Mod:store_session(LUser, LServer, TS, PushJID, Node, XData) + end. + +-spec lookup_session(binary(), binary(), timestamp()) + -> {ok, push_session()} | error. +lookup_session(LUser, LServer, TS) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + case use_cache(Mod, LServer) of + true -> + ets_cache:lookup( + ?PUSH_CACHE, {LUser, LServer, TS}, + fun() -> Mod:lookup_session(LUser, LServer, TS) end); + false -> + Mod:lookup_session(LUser, LServer, TS) + end. + +-spec lookup_sessions(binary(), binary()) -> {ok, [push_session()]} | error. +lookup_sessions(LUser, LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + case use_cache(Mod, LServer) of + true -> + ets_cache:lookup( + ?PUSH_CACHE, {LUser, LServer}, + fun() -> Mod:lookup_sessions(LUser, LServer) end); + false -> + Mod:lookup_sessions(LUser, LServer) + end. + +-spec delete_session(binary(), binary(), timestamp()) -> ok | error. +delete_session(LUser, LServer, TS) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + ok = Mod:delete_session(LUser, LServer, TS), + case use_cache(Mod, LServer) of + true -> + ets_cache:delete(?PUSH_CACHE, {LUser, LServer}, + cache_nodes(Mod, LServer)), + ets_cache:delete(?PUSH_CACHE, {LUser, LServer, TS}, + cache_nodes(Mod, LServer)); + false -> + ok + end. + +-spec delete_session(binary(), binary(), jid(), binary()) -> ok | error. +delete_session(LUser, LServer, PushJID, Node) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:lookup_session(LUser, LServer, PushJID, Node) of + {ok, {TS, _, _, _}} -> + delete_session(LUser, LServer, TS); + error -> + error + end. + +-spec delete_sessions(binary(), binary(), jid()) -> ok | error. +delete_sessions(LUser, LServer, PushJID) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + LookupFun = fun() -> Mod:lookup_sessions(LUser, LServer, PushJID) end, + delete_sessions(LUser, LServer, LookupFun, Mod). + +-spec delete_sessions(binary(), binary(), fun(() -> ok | error), module()) + -> ok | error. +delete_sessions(LUser, LServer, LookupFun, Mod) -> + case LookupFun() of + {ok, Clients} -> + case use_cache(Mod, LServer) of + true -> + ets_cache:delete(?PUSH_CACHE, {LUser, LServer}, + cache_nodes(Mod, LServer)); + false -> + ok + end, + lists:foreach( + fun({TS, _, _, _}) -> + ok = Mod:delete_session(LUser, LServer, TS), + case use_cache(Mod, LServer) of + true -> + ets_cache:delete(?PUSH_CACHE, + {LUser, LServer, TS}, + cache_nodes(Mod, LServer)); + false -> + ok + end + end, Clients); + error -> + error + end. + +-spec drop_online_sessions(binary(), binary(), [push_session()]) + -> [push_session()]. +drop_online_sessions(LUser, LServer, Clients) -> + SessIDs = ejabberd_sm:get_session_sids(LUser, LServer), + [Client || {TS, _, _, _} = Client <- Clients, + lists:keyfind(TS, 1, SessIDs) == false]. + +%%-------------------------------------------------------------------- +%% Caching. +%%-------------------------------------------------------------------- +-spec init_cache(module(), binary(), gen_mod:opts()) -> ok. +init_cache(Mod, Host, Opts) -> + case use_cache(Mod, Host) of + true -> + CacheOpts = cache_opts(Host, Opts), + ets_cache:new(?PUSH_CACHE, CacheOpts); + false -> + ets_cache:delete(?PUSH_CACHE) + end. + +-spec cache_opts(binary(), gen_mod:opts()) -> [proplists:property()]. +cache_opts(Host, Opts) -> + MaxSize = gen_mod:get_opt( + cache_size, Opts, + ejabberd_config:cache_size(Host)), + CacheMissed = gen_mod:get_opt( + cache_missed, Opts, + ejabberd_config:cache_missed(Host)), + LifeTime = case gen_mod:get_opt( + cache_life_time, Opts, + ejabberd_config:cache_life_time(Host)) of + infinity -> infinity; + I -> timer:seconds(I) + end, + [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + +-spec use_cache(module(), binary()) -> boolean(). +use_cache(Mod, Host) -> + case erlang:function_exported(Mod, use_cache, 1) of + true -> Mod:use_cache(Host); + false -> + gen_mod:get_module_opt( + Host, ?MODULE, use_cache, + ejabberd_config:use_cache(Host)) + end. + +-spec cache_nodes(module(), binary()) -> [node()]. +cache_nodes(Mod, Host) -> + case erlang:function_exported(Mod, cache_nodes, 1) of + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() + end. diff --git a/src/mod_push_keepalive.erl b/src/mod_push_keepalive.erl new file mode 100644 index 000000000..bde62fc67 --- /dev/null +++ b/src/mod_push_keepalive.erl @@ -0,0 +1,236 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_push_keepalive.erl +%%% Author : Holger Weiss +%%% Purpose : Keep pending XEP-0198 sessions alive with XEP-0357 +%%% Created : 15 Jul 2017 by Holger Weiss +%%% +%%% +%%% ejabberd, Copyright (C) 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(mod_push_keepalive). +-author('holger@zedat.fu-berlin.de'). + +-behavior(gen_mod). + +%% gen_mod callbacks. +-export([start/2, stop/1, reload/3, mod_opt_type/1, depends/2]). + +%% ejabberd_hooks callbacks. +-export([c2s_session_pending/1, c2s_session_resumed/1, c2s_copy_session/2, + c2s_handle_cast/2, c2s_handle_info/2, c2s_stanza/3]). + +-include("logger.hrl"). +-include("xmpp.hrl"). + +-define(PUSH_BEFORE_TIMEOUT_SECS, 120). + +-type c2s_state() :: ejabberd_c2s:state(). + +%%-------------------------------------------------------------------- +%% gen_mod callbacks. +%%-------------------------------------------------------------------- +-spec start(binary(), gen_mod:opts()) -> ok. +start(Host, Opts) -> + case gen_mod:get_opt(wake_on_start, Opts, false) of + true -> + wake_all(Host); + false -> + ok + end, + register_hooks(Host). + +-spec stop(binary()) -> ok. +stop(Host) -> + unregister_hooks(Host). + +-spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. +reload(Host, NewOpts, OldOpts) -> + case gen_mod:is_equal_opt(wake_on_start, NewOpts, OldOpts, false) of + {false, true, _} -> + wake_all(Host); + _ -> + ok + end, + ok. + +-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. +depends(_Host, _Opts) -> + [{mod_push, hard}, + {mod_client_state, soft}, + {mod_stream_mgmt, soft}]. + +-spec mod_opt_type(atom()) -> fun((term()) -> term()) | [atom()]. +mod_opt_type(resume_timeout) -> + fun(I) when is_integer(I), I >= 0 -> I; + (undefined) -> undefined + end; +mod_opt_type(wake_on_start) -> + fun (B) when is_boolean(B) -> B end; +mod_opt_type(wake_on_timeout) -> + fun (B) when is_boolean(B) -> B end; +mod_opt_type(O) when O == cache_life_time; O == cache_size -> + fun(I) when is_integer(I), I > 0 -> I; + (infinity) -> infinity + end; +mod_opt_type(O) when O == use_cache; O == cache_missed -> + fun (B) when is_boolean(B) -> B end; +mod_opt_type(_) -> + [resume_timeout, wake_on_start, wake_on_timeout, db_type, cache_life_time, + cache_size, use_cache, cache_missed, iqdisc]. + +%%-------------------------------------------------------------------- +%% Register/unregister hooks. +%%-------------------------------------------------------------------- +-spec register_hooks(binary()) -> ok. +register_hooks(Host) -> + ejabberd_hooks:add(c2s_session_pending, Host, ?MODULE, + c2s_session_pending, 50), + ejabberd_hooks:add(c2s_session_resumed, Host, ?MODULE, + c2s_session_resumed, 50), + ejabberd_hooks:add(c2s_copy_session, Host, ?MODULE, + c2s_copy_session, 50), + ejabberd_hooks:add(c2s_handle_cast, Host, ?MODULE, + c2s_handle_cast, 40), + ejabberd_hooks:add(c2s_handle_info, Host, ?MODULE, + c2s_handle_info, 50), + ejabberd_hooks:add(c2s_handle_send, Host, ?MODULE, + c2s_stanza, 50). + +-spec unregister_hooks(binary()) -> ok. +unregister_hooks(Host) -> + ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, + disco_sm_features, 50), + ejabberd_hooks:delete(c2s_session_pending, Host, ?MODULE, + c2s_session_pending, 50), + ejabberd_hooks:delete(c2s_session_resumed, Host, ?MODULE, + c2s_session_resumed, 50), + ejabberd_hooks:delete(c2s_copy_session, Host, ?MODULE, + c2s_copy_session, 50), + ejabberd_hooks:delete(c2s_handle_cast, Host, ?MODULE, + c2s_handle_cast, 40), + ejabberd_hooks:delete(c2s_handle_info, Host, ?MODULE, + c2s_handle_info, 50), + ejabberd_hooks:delete(c2s_handle_send, Host, ?MODULE, + c2s_stanza, 50). + +%%-------------------------------------------------------------------- +%% Hook callbacks. +%%-------------------------------------------------------------------- +-spec c2s_stanza(c2s_state(), xmpp_element() | xmlel(), term()) -> c2s_state(). +c2s_stanza(#{push_enabled := true, mgmt_state := pending} = State, + _Pkt, _SendResult) -> + maybe_restore_resume_timeout(State); +c2s_stanza(State, _Pkt, _SendResult) -> + State. + +-spec c2s_session_pending(c2s_state()) -> c2s_state(). +c2s_session_pending(#{push_enabled := true, mgmt_queue := Queue} = State) -> + case p1_queue:len(Queue) of + 0 -> + State1 = maybe_adjust_resume_timeout(State), + maybe_start_wakeup_timer(State1); + _ -> + State + end; +c2s_session_pending(State) -> + State. + +-spec c2s_session_resumed(c2s_state()) -> c2s_state(). +c2s_session_resumed(#{push_enabled := true} = State) -> + maybe_restore_resume_timeout(State); +c2s_session_resumed(State) -> + State. + +-spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state(). +c2s_copy_session(State, #{push_enabled := true, + push_resume_timeout := ResumeTimeout, + push_wake_on_timeout := WakeOnTimeout}) -> + State#{push_resume_timeout => ResumeTimeout, + push_wake_on_timeout => WakeOnTimeout}; +c2s_copy_session(State, _) -> + State. + +-spec c2s_handle_cast(c2s_state(), any()) -> c2s_state(). +c2s_handle_cast(#{lserver := LServer} = State, push_enable) -> + ResumeTimeout = gen_mod:get_module_opt(LServer, ?MODULE, + resume_timeout, 86400), + WakeOnTimeout = gen_mod:get_module_opt(LServer, ?MODULE, + wake_on_timeout, true), + State#{push_resume_timeout => ResumeTimeout, + push_wake_on_timeout => WakeOnTimeout}; +c2s_handle_cast(State, push_disable) -> + State1 = maps:remove(push_resume_timeout, State), + maps:remove(push_wake_on_timeout, State1); +c2s_handle_cast(State, _Msg) -> + State. + +-spec c2s_handle_info(c2s_state(), any()) -> c2s_state() | {stop, c2s_state()}. +c2s_handle_info(#{push_enabled := true, mgmt_state := pending, + jid := JID} = State, {timeout, _, push_keepalive}) -> + ?INFO_MSG("Waking ~s before session times out", [jid:encode(JID)]), + mod_push:notify(State), + {stop, State}; +c2s_handle_info(State, _) -> + State. + +%%-------------------------------------------------------------------- +%% Internal functions. +%%-------------------------------------------------------------------- +-spec maybe_adjust_resume_timeout(c2s_state()) -> c2s_state(). +maybe_adjust_resume_timeout(#{push_resume_timeout := undefined} = State) -> + State; +maybe_adjust_resume_timeout(#{push_resume_timeout := Timeout} = State) -> + OrigTimeout = mod_stream_mgmt:get_resume_timeout(State), + ?DEBUG("Adjusting resume timeout to ~B seconds", [Timeout]), + State1 = mod_stream_mgmt:set_resume_timeout(State, Timeout), + State1#{push_resume_timeout_orig => OrigTimeout}. + +-spec maybe_restore_resume_timeout(c2s_state()) -> c2s_state(). +maybe_restore_resume_timeout(#{push_resume_timeout_orig := Timeout} = State) -> + ?DEBUG("Restoring resume timeout to ~B seconds", [Timeout]), + State1 = mod_stream_mgmt:set_resume_timeout(State, Timeout), + maps:remove(push_resume_timeout_orig, State1); +maybe_restore_resume_timeout(State) -> + State. + +-spec maybe_start_wakeup_timer(c2s_state()) -> c2s_state(). +maybe_start_wakeup_timer(#{push_wake_on_timeout := true, + push_resume_timeout := ResumeTimeout} = State) + when is_integer(ResumeTimeout), ResumeTimeout > ?PUSH_BEFORE_TIMEOUT_SECS -> + WakeTimeout = ResumeTimeout - ?PUSH_BEFORE_TIMEOUT_SECS, + ?DEBUG("Scheduling wake-up timer to fire in ~B seconds", [WakeTimeout]), + erlang:start_timer(timer:seconds(WakeTimeout), self(), push_keepalive), + State; +maybe_start_wakeup_timer(State) -> + State. + +-spec wake_all(binary()) -> ok | error. +wake_all(LServer) -> + ?INFO_MSG("Waking all push clients on ~s", [LServer]), + Mod = gen_mod:db_mod(LServer, mod_push), + case Mod:lookup_sessions(LServer) of + {ok, Sessions} -> + IgnoreResponse = fun(_) -> ok end, + lists:foreach(fun({_, PushLJID, Node, XData}) -> + mod_push:notify(LServer, PushLJID, Node, + XData, IgnoreResponse) + end, Sessions); + error -> + error + end. diff --git a/src/mod_push_mnesia.erl b/src/mod_push_mnesia.erl new file mode 100644 index 000000000..04ea8d60a --- /dev/null +++ b/src/mod_push_mnesia.erl @@ -0,0 +1,204 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_push_mnesia.erl +%%% Author : Holger Weiss +%%% Purpose : Mnesia backend for Push Notifications (XEP-0357) +%%% Created : 15 Jul 2017 by Holger Weiss +%%% +%%% +%%% ejabberd, Copyright (C) 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(mod_push_mnesia). +-author('holger@zedat.fu-berlin.de'). + +-behavior(mod_push). + +%% API +-export([init/2, store_session/6, lookup_session/4, lookup_session/3, + lookup_sessions/3, lookup_sessions/2, lookup_sessions/1, + delete_session/3, delete_old_sessions/2]). + +-include_lib("stdlib/include/ms_transform.hrl"). +-include("logger.hrl"). +-include("xmpp.hrl"). + +-record(push_session, + {us = {<<"">>, <<"">>} :: {binary(), binary()}, + timestamp = p1_time_compat:timestamp() :: erlang:timestamp(), + service = {<<"">>, <<"">>, <<"">>} :: ljid(), + node = <<"">> :: binary(), + xdata = #xdata{} :: xdata()}). + +%%%------------------------------------------------------------------- +%%% API +%%%------------------------------------------------------------------- +init(_Host, _Opts) -> + ejabberd_mnesia:create(?MODULE, push_session, + [{disc_only_copies, [node()]}, + {type, bag}, + {attributes, record_info(fields, push_session)}]). + +store_session(LUser, LServer, TS, PushJID, Node, XData) -> + US = {LUser, LServer}, + PushLJID = jid:tolower(PushJID), + MaxSessions = ejabberd_sm:get_max_user_sessions(LUser, LServer), + F = fun() -> + if is_integer(MaxSessions) -> + enforce_max_sessions(US, MaxSessions - 1); + MaxSessions == infinity -> + ok + end, + mnesia:write(#push_session{us = US, + timestamp = TS, + service = PushLJID, + node = Node, + xdata = XData}) + end, + case mnesia:transaction(F) of + {atomic, ok} -> + {ok, {TS, PushLJID, Node, XData}}; + {aborted, E} -> + ?ERROR_MSG("Cannot store push session for ~s@~s: ~p", + [LUser, LServer, E]), + error + end. + +lookup_session(LUser, LServer, PushJID, Node) -> + PushLJID = jid:tolower(PushJID), + MatchSpec = ets:fun2ms( + fun(#push_session{us = {U, S}, service = P, node = N} = Rec) + when U == LUser, + S == LServer, + P == PushLJID, + N == Node -> + Rec + end), + case mnesia:dirty_select(push_session, MatchSpec) of + [#push_session{timestamp = TS, xdata = XData}] -> + {ok, {TS, PushLJID, Node, XData}}; + _ -> + ?DEBUG("No push session found for ~s@~s (~p, ~s)", + [LUser, LServer, PushJID, Node]), + error + end. + +lookup_session(LUser, LServer, TS) -> + MatchSpec = ets:fun2ms( + fun(#push_session{us = {U, S}, timestamp = T} = Rec) + when U == LUser, + S == LServer, + T == TS -> + Rec + end), + case mnesia:dirty_select(push_session, MatchSpec) of + [#push_session{service = PushLJID, node = Node, xdata = XData}] -> + {ok, {TS, PushLJID, Node, XData}}; + _ -> + ?DEBUG("No push session found for ~s@~s (~p)", + [LUser, LServer, TS]), + error + end. + +lookup_sessions(LUser, LServer, PushJID) -> + PushLJID = jid:tolower(PushJID), + MatchSpec = ets:fun2ms( + fun(#push_session{us = {U, S}, service = P, node = N} = Rec) + when U == LUser, + S == LServer, + P == PushLJID -> + Rec + end), + {ok, mnesia:dirty_select(push_session, MatchSpec)}. + +lookup_sessions(LUser, LServer) -> + Records = mnesia:dirty_read(push_session, {LUser, LServer}), + Clients = [{TS, PushLJID, Node, XData} + || #push_session{timestamp = TS, + service = PushLJID, + node = Node, + xdata = XData} <- Records], + {ok, Clients}. + +lookup_sessions(LServer) -> + MatchSpec = ets:fun2ms( + fun(#push_session{us = {_U, S}, + timestamp = TS, + service = PushLJID, + node = Node, + xdata = XData}) + when S == LServer -> + {TS, PushLJID, Node, XData} + end), + {ok, mnesia:dirty_select(push_session, MatchSpec)}. + +delete_session(LUser, LServer, TS) -> + MatchSpec = ets:fun2ms( + fun(#push_session{us = {U, S}, timestamp = T} = Rec) + when U == LUser, + S == LServer, + T == TS -> + Rec + end), + F = fun() -> + Recs = mnesia:select(push_session, MatchSpec), + lists:foreach(fun mnesia:delete_object/1, Recs) + end, + case mnesia:transaction(F) of + {atomic, ok} -> + ok; + {aborted, E} -> + ?ERROR_MSG("Cannot delete push seesion of ~s@~s: ~p", + [LUser, LServer, E]), + error + end. + +delete_old_sessions(_LServer, Time) -> + DelIfOld = fun(#push_session{timestamp = T} = Rec, ok) when T < Time -> + mnesia:delete_object(Rec); + (_Rec, ok) -> + ok + end, + F = fun() -> + mnesia:foldl(DelIfOld, ok, push_session) + end, + case mnesia:transaction(F) of + {atomic, ok} -> + ok; + {aborted, E} -> + ?ERROR_MSG("Cannot delete old push sessions: ~p", [E]), + error + end. + +%%-------------------------------------------------------------------- +%% Internal functions. +%%-------------------------------------------------------------------- +-spec enforce_max_sessions({binary(), binary()}, non_neg_integer()) -> ok. +enforce_max_sessions({U, S} = US, Max) -> + Recs = mnesia:wread({push_session, US}), + NumRecs = length(Recs), + if NumRecs > Max -> + NumOldRecs = NumRecs - Max, + Recs1 = lists:keysort(#push_session.timestamp, Recs), + Recs2 = lists:reverse(Recs1), + OldRecs = lists:sublist(Recs2, Max + 1, NumOldRecs), + ?INFO_MSG("Disabling ~B old push session(s) of ~s@~s", + [NumOldRecs, U, S]), + lists:foreach(fun(Rec) -> mnesia:delete_object(Rec) end, OldRecs); + true -> + ok + end. diff --git a/src/mod_roster.erl b/src/mod_roster.erl index 28bc06171..7bc5f7de7 100644 --- a/src/mod_roster.erl +++ b/src/mod_roster.erl @@ -35,7 +35,6 @@ -module(mod_roster). -protocol({xep, 237, '1.3'}). --protocol({xep, 321, '0.1'}). -author('alexey@process-one.net'). @@ -441,14 +440,13 @@ decode_item(Item, R, Managed) -> end, groups = Item#roster_item.groups}. -process_iq_set(#iq{from = From, to = To, +process_iq_set(#iq{from = _From, to = To, sub_els = [#roster_query{items = [QueryItem]}]} = IQ) -> #jid{user = User, luser = LUser, lserver = LServer} = To, - Managed = {From#jid.luser, From#jid.lserver} /= {LUser, LServer}, LJID = jid:tolower(QueryItem#roster_item.jid), F = fun () -> Item = get_roster_item(LUser, LServer, LJID), - Item2 = decode_item(QueryItem, Item, Managed), + Item2 = decode_item(QueryItem, Item, false), Item3 = ejabberd_hooks:run_fold(roster_process_item, LServer, Item2, [LServer]), @@ -1200,8 +1198,6 @@ mod_opt_type(access) -> fun acl:access_rules_validator/1; mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end; mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; -mod_opt_type(managers) -> - fun (B) when is_list(B) -> B end; mod_opt_type(store_current_id) -> fun (B) when is_boolean(B) -> B end; mod_opt_type(versioning) -> @@ -1213,5 +1209,5 @@ mod_opt_type(O) when O == cache_life_time; O == cache_size -> mod_opt_type(O) when O == use_cache; O == cache_missed -> fun (B) when is_boolean(B) -> B end; mod_opt_type(_) -> - [access, db_type, iqdisc, managers, store_current_id, + [access, db_type, iqdisc, store_current_id, versioning, cache_life_time, cache_size, use_cache, cache_missed]. diff --git a/src/mod_sip_proxy.erl b/src/mod_sip_proxy.erl index 19a02e8e4..25f035377 100644 --- a/src/mod_sip_proxy.erl +++ b/src/mod_sip_proxy.erl @@ -27,8 +27,7 @@ -ifndef(SIP). -export([]). -else. --define(GEN_FSM, p1_fsm). --behaviour(?GEN_FSM). +-behaviour(p1_fsm). %% API -export([start/2, start_link/2, route/3, route/4]). @@ -58,10 +57,10 @@ start(LServer, Opts) -> supervisor:start_child(mod_sip_proxy_sup, [LServer, Opts]). start_link(LServer, Opts) -> - ?GEN_FSM:start_link(?MODULE, [LServer, Opts], []). + p1_fsm:start_link(?MODULE, [LServer, Opts], []). route(SIPMsg, _SIPSock, TrID, Pid) -> - ?GEN_FSM:send_event(Pid, {SIPMsg, TrID}). + p1_fsm:send_event(Pid, {SIPMsg, TrID}). route(#sip{hdrs = Hdrs} = Req, LServer, Opts) -> case proplists:get_bool(authenticated, Opts) of diff --git a/src/mod_stream_mgmt.erl b/src/mod_stream_mgmt.erl index 127eea3e8..2f6b0fc71 100644 --- a/src/mod_stream_mgmt.erl +++ b/src/mod_stream_mgmt.erl @@ -33,6 +33,8 @@ c2s_unbinded_packet/2, c2s_closed/2, c2s_terminated/2, c2s_handle_send/3, c2s_handle_info/2, c2s_handle_call/3, c2s_handle_recv/3]). +%% adjust pending session timeout +-export([get_resume_timeout/1, set_resume_timeout/2]). -include("xmpp.hrl"). -include("logger.hrl"). @@ -69,7 +71,12 @@ start(Host, _Opts) -> ejabberd_hooks:add(c2s_terminated, Host, ?MODULE, c2s_terminated, 50). stop(Host) -> - %% TODO: do something with global 'c2s_init' hook + case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of + true -> + ok; + false -> + ejabberd_hooks:delete(c2s_init, ?MODULE, c2s_stream_init, 50) + end, ejabberd_hooks:delete(c2s_stream_started, Host, ?MODULE, c2s_stream_started, 50), ejabberd_hooks:delete(c2s_post_auth_features, Host, ?MODULE, @@ -235,8 +242,9 @@ c2s_handle_info(#{mgmt_ack_timer := TRef, jid := JID, mod := Mod} = State, [jid:encode(JID)]), State1 = Mod:close(State), {stop, transition_to_pending(State1)}; -c2s_handle_info(#{mgmt_state := pending, jid := JID, mod := Mod} = State, - {timeout, _, pending_timeout}) -> +c2s_handle_info(#{mgmt_state := pending, + mgmt_pending_timer := TRef, jid := JID, mod := Mod} = State, + {timeout, TRef, pending_timeout}) -> ?DEBUG("Timed out waiting for resumption of stream for ~s", [jid:encode(JID)]), Mod:stop(State#{mgmt_state => timeout}); @@ -282,6 +290,20 @@ c2s_terminated(#{mgmt_state := MgmtState, mgmt_stanzas_in := In, sid := SID, c2s_terminated(State, _Reason) -> State. +%%%=================================================================== +%%% Adjust pending session timeout +%%%=================================================================== +-spec get_resume_timeout(state()) -> non_neg_integer(). +get_resume_timeout(#{mgmt_timeout := Timeout}) -> + Timeout. + +-spec set_resume_timeout(state(), non_neg_integer()) -> state(). +set_resume_timeout(#{mgmt_timeout := Timeout} = State, Timeout) -> + State; +set_resume_timeout(State, Timeout) -> + State1 = restart_pending_timer(State, Timeout), + State1#{mgmt_timeout => Timeout}. + %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -388,8 +410,6 @@ handle_resume(#{user := User, lserver := LServer, sockmod := SockMod, previd = AttrId}), State3 = resend_unacked_stanzas(State2), State4 = send(State3, #sm_r{xmlns = AttrXmlns}), - %% TODO: move this to mod_client_state - %% csi_flush_queue(State4), State5 = ejabberd_hooks:run_fold(c2s_session_resumed, LServer, State4, []), ?INFO_MSG("(~s) Resumed session for ~s", [SockMod:pp(Socket), jid:encode(JID)]), @@ -408,8 +428,8 @@ transition_to_pending(#{mgmt_state := active, jid := JID, lserver := LServer, mgmt_timeout := Timeout} = State) -> State1 = cancel_ack_timer(State), ?INFO_MSG("Waiting for resumption of stream for ~s", [jid:encode(JID)]), - erlang:start_timer(timer:seconds(Timeout), self(), pending_timeout), - State2 = State1#{mgmt_state => pending}, + TRef = erlang:start_timer(timer:seconds(Timeout), self(), pending_timeout), + State2 = State1#{mgmt_state => pending, mgmt_pending_timer => TRef}, ejabberd_hooks:run_fold(c2s_session_pending, LServer, State2, []); transition_to_pending(State) -> State. @@ -648,20 +668,33 @@ add_resent_delay_info(_State, El, _Time) -> send(#{mod := Mod} = State, Pkt) -> Mod:send(State, Pkt). +-spec restart_pending_timer(state(), non_neg_integer()) -> state(). +restart_pending_timer(#{mgmt_pending_timer := TRef} = State, NewTimeout) -> + cancel_timer(TRef), + NewTRef = erlang:start_timer(timer:seconds(NewTimeout), self(), + pending_timeout), + State#{mgmt_pending_timer => NewTRef}; +restart_pending_timer(State, _NewTimeout) -> + State. + -spec cancel_ack_timer(state()) -> state(). cancel_ack_timer(#{mgmt_ack_timer := TRef} = State) -> - case erlang:cancel_timer(TRef) of - false -> - receive {timeout, TRef, _} -> ok - after 0 -> ok - end; - _ -> - ok - end, + cancel_timer(TRef), maps:remove(mgmt_ack_timer, State); cancel_ack_timer(State) -> State. +-spec cancel_timer(reference()) -> ok. +cancel_timer(TRef) -> + case erlang:cancel_timer(TRef) of + false -> + receive {timeout, TRef, _} -> ok + after 0 -> ok + end; + _ -> + ok + end. + -spec bounce_message_queue() -> ok. bounce_message_queue() -> receive {route, Pkt} -> diff --git a/src/mod_vcard.erl b/src/mod_vcard.erl index 495393f72..67d01a085 100644 --- a/src/mod_vcard.erl +++ b/src/mod_vcard.erl @@ -67,7 +67,7 @@ -optional_callbacks([use_cache/1, cache_nodes/1]). --record(state, {host :: binary(), server_host :: binary()}). +-record(state, {hosts :: [binary()], server_host :: binary()}). %%==================================================================== %% gen_mod callbacks @@ -95,37 +95,40 @@ init([Host, Opts]) -> ?NS_VCARD, ?MODULE, process_sm_iq, IQDisc), ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, get_sm_features, 50), - MyHost = gen_mod:get_opt_host(Host, Opts, <<"vjud.@HOST@">>), + MyHosts = gen_mod:get_opt_hosts(Host, Opts, <<"vjud.@HOST@">>), Search = gen_mod:get_opt(search, Opts, false), if Search -> - ejabberd_hooks:add( - disco_local_items, MyHost, ?MODULE, disco_items, 100), - ejabberd_hooks:add( - disco_local_features, MyHost, ?MODULE, disco_features, 100), - ejabberd_hooks:add( - disco_local_identity, MyHost, ?MODULE, disco_identity, 100), - gen_iq_handler:add_iq_handler( - ejabberd_local, MyHost, ?NS_SEARCH, ?MODULE, process_search, IQDisc), - gen_iq_handler:add_iq_handler( - ejabberd_local, MyHost, ?NS_VCARD, ?MODULE, process_vcard, IQDisc), - gen_iq_handler:add_iq_handler( - ejabberd_local, MyHost, ?NS_DISCO_ITEMS, mod_disco, - process_local_iq_items, IQDisc), - gen_iq_handler:add_iq_handler( - ejabberd_local, MyHost, ?NS_DISCO_INFO, mod_disco, - process_local_iq_info, IQDisc), - case Mod:is_search_supported(Host) of - false -> - ?WARNING_MSG("vcard search functionality is " - "not implemented for ~s backend", - [gen_mod:db_type(Host, Opts, ?MODULE)]); - true -> - ejabberd_router:register_route(MyHost, Host) - end; + lists:foreach( + fun(MyHost) -> + ejabberd_hooks:add( + disco_local_items, MyHost, ?MODULE, disco_items, 100), + ejabberd_hooks:add( + disco_local_features, MyHost, ?MODULE, disco_features, 100), + ejabberd_hooks:add( + disco_local_identity, MyHost, ?MODULE, disco_identity, 100), + gen_iq_handler:add_iq_handler( + ejabberd_local, MyHost, ?NS_SEARCH, ?MODULE, process_search, IQDisc), + gen_iq_handler:add_iq_handler( + ejabberd_local, MyHost, ?NS_VCARD, ?MODULE, process_vcard, IQDisc), + gen_iq_handler:add_iq_handler( + ejabberd_local, MyHost, ?NS_DISCO_ITEMS, mod_disco, + process_local_iq_items, IQDisc), + gen_iq_handler:add_iq_handler( + ejabberd_local, MyHost, ?NS_DISCO_INFO, mod_disco, + process_local_iq_info, IQDisc), + case Mod:is_search_supported(Host) of + false -> + ?WARNING_MSG("vcard search functionality is " + "not implemented for ~s backend", + [gen_mod:db_type(Host, Opts, ?MODULE)]); + true -> + ejabberd_router:register_route(MyHost, Host) + end + end, MyHosts); true -> ok end, - {ok, #state{host = MyHost, server_host = Host}}. + {ok, #state{hosts = MyHosts, server_host = Host}}. handle_call(_Call, _From, State) -> {noreply, State}. @@ -144,21 +147,24 @@ handle_info(Info, State) -> ?WARNING_MSG("unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{host = MyHost, server_host = Host}) -> +terminate(_Reason, #state{hosts = MyHosts, server_host = Host}) -> ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_VCARD), ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50), Mod = gen_mod:db_mod(Host, ?MODULE), Mod:stop(Host), - ejabberd_router:unregister_route(MyHost), - ejabberd_hooks:delete(disco_local_items, MyHost, ?MODULE, disco_items, 100), - ejabberd_hooks:delete(disco_local_features, MyHost, ?MODULE, disco_features, 100), - ejabberd_hooks:delete(disco_local_identity, MyHost, ?MODULE, disco_identity, 100), - gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_SEARCH), - gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_VCARD), - gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_ITEMS), - gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_INFO). + lists:foreach( + fun(MyHost) -> + ejabberd_router:unregister_route(MyHost), + ejabberd_hooks:delete(disco_local_items, MyHost, ?MODULE, disco_items, 100), + ejabberd_hooks:delete(disco_local_features, MyHost, ?MODULE, disco_features, 100), + ejabberd_hooks:delete(disco_local_identity, MyHost, ?MODULE, disco_identity, 100), + gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_SEARCH), + gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_VCARD), + gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_ITEMS), + gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_INFO) + end, MyHosts). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -527,6 +533,8 @@ mod_opt_type(allow_return_all) -> fun (B) when is_boolean(B) -> B end; mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end; mod_opt_type(host) -> fun iolist_to_binary/1; +mod_opt_type(hosts) -> + fun (L) -> lists:map(fun iolist_to_binary/1, L) end; mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; mod_opt_type(matches) -> fun (infinity) -> infinity; @@ -543,6 +551,6 @@ mod_opt_type(O) when O == cache_life_time; O == cache_size -> mod_opt_type(O) when O == use_cache; O == cache_missed -> fun (B) when is_boolean(B) -> B end; mod_opt_type(_) -> - [allow_return_all, db_type, host, iqdisc, matches, + [allow_return_all, db_type, host, hosts, iqdisc, matches, search, search_all_hosts, cache_life_time, cache_size, use_cache, cache_missed]. diff --git a/src/mod_vcard_ldap.erl b/src/mod_vcard_ldap.erl index f1f076468..38c4747e6 100644 --- a/src/mod_vcard_ldap.erl +++ b/src/mod_vcard_ldap.erl @@ -47,7 +47,7 @@ -record(state, {serverhost = <<"">> :: binary(), - myhost = <<"">> :: binary(), + myhosts = [] :: [binary()], eldap_id = <<"">> :: binary(), search = false :: boolean(), servers = [] :: [binary()], @@ -351,8 +351,7 @@ default_search_reported() -> {<<"Organization Unit">>, <<"ORGUNIT">>}]. parse_options(Host, Opts) -> - MyHost = gen_mod:get_opt_host(Host, Opts, - <<"vjud.@HOST@">>), + MyHosts = gen_mod:get_opt_hosts(Host, Opts, <<"vjud.@HOST@">>), Search = gen_mod:get_opt(search, Opts, false), Matches = gen_mod:get_opt(matches, Opts, 30), Eldap_ID = misc:atom_to_binary(gen_mod:get_module_proc(Host, ?PROCNAME)), @@ -394,7 +393,7 @@ parse_options(Host, Opts) -> end, SearchReported) ++ UIDAttrs), - #state{serverhost = Host, myhost = MyHost, + #state{serverhost = Host, myhosts = MyHosts, eldap_id = Eldap_ID, search = Search, servers = Cfg#eldap_config.servers, backups = Cfg#eldap_config.backups, diff --git a/src/node_pep.erl b/src/node_pep.erl index 6dd7a68c4..cc0dd41fb 100644 --- a/src/node_pep.erl +++ b/src/node_pep.erl @@ -119,7 +119,7 @@ create_node(Nidx, Owner) -> delete_node(Nodes) -> {result, {_, _, Result}} = node_flat:delete_node(Nodes), - {result, {[], Result}}. + {result, {default, Result}}. subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> diff --git a/src/node_pep_sql.erl b/src/node_pep_sql.erl index 68a2ce946..b84c945bd 100644 --- a/src/node_pep_sql.erl +++ b/src/node_pep_sql.erl @@ -73,7 +73,7 @@ create_node(Nidx, Owner) -> delete_node(Nodes) -> {result, {_, _, Result}} = node_flat_sql:delete_node(Nodes), - {result, {[], Result}}. + {result, {default, Result}}. subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> diff --git a/src/prosody2ejabberd.erl b/src/prosody2ejabberd.erl index c60f2c24b..2c7dabb48 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", "pubsub"]) + "privacy", "pep", "pubsub"]) end, HostDirs); {error, Why} = Err -> ?ERROR_MSG("failed to list ~s: ~s", @@ -67,12 +67,24 @@ convert_dir(Path, Host, Type) -> lists:foreach( fun(File) -> FilePath = filename:join(Path, File), - case eval_file(FilePath) of - {ok, Data} -> - Name = iolist_to_binary(filename:rootname(File)), - convert_data(Host, Type, Name, Data); - Err -> - Err + case Type of + "pep" -> + case filelib:is_dir(FilePath) of + true -> + JID = list_to_binary(File ++ "@" ++ Host), + convert_dir(FilePath, JID, "pubsub"); + false -> + ok + end; + _ -> + case eval_file(FilePath) of + {ok, Data} -> + Name = iolist_to_binary(filename:rootname(File)), + convert_data(url_decode(Host), Type, + url_decode(Name), Data); + Err -> + Err + end end end, Files); {error, enoent} -> @@ -212,44 +224,49 @@ 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]; +convert_data(HostStr, "pubsub", Node, [Data]) -> + case decode_pubsub_host(HostStr) of + Host when is_binary(Host); + is_tuple(Host) -> + Type = node_type(Host), + NodeData = convert_node_config(HostStr, 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 ~p:~n~p", + [Node, Host, NodeData]), Error end; Error -> - ?ERROR_MSG("failed to import pubsub node ~s on host ~s:~n~p", - [Node, Host, NodeData]), + ?ERROR_MSG("failed to import pubsub node: ~p", [Error]), Error end; convert_data(_Host, _Type, _User, _Data) -> @@ -383,10 +400,15 @@ url_decode(<>, Acc) -> 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)). +decode_pubsub_host(Host) -> + try jid:decode(Host) of + #jid{luser = <<>>, lserver = LServer} -> LServer; + #jid{luser = LUser, lserver = LServer} -> {LUser, LServer, <<>>} + catch _:{bad_jid, _} -> bad_jid + end. + +node_type({_U, _S, _R}) -> <<"pep">>; +node_type(Host) -> hd(mod_pubsub:plugins(Host)). max_items(Config, Default) -> case round(proplists:get_value(<<"max_items">>, Config, Default)) of @@ -422,7 +444,7 @@ 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 + try jid:decode(proplists:get_value(ItemId, Authors, Host)) of JID -> [El] = deserialize(Item), [{ItemId, JID, El#xmlel.children}] @@ -496,5 +518,5 @@ deserialize([{_, S}|T], #xmlel{children = Els} = El, Acc) when is_binary(S) -> deserialize(T, El#xmlel{children = [{xmlcdata, S}|Els]}, Acc); deserialize([{_, L}|T], #xmlel{children = Els} = El, Acc) when is_list(L) -> deserialize(T, El#xmlel{children = deserialize(L) ++ Els}, Acc); -deserialize([], El, Acc) -> - [El|Acc]. +deserialize([], #xmlel{children = Els} = El, Acc) -> + [El#xmlel{children = lists:reverse(Els)}|Acc]. diff --git a/src/pubsub_db_sql.erl b/src/pubsub_db_sql.erl index ae7a9fde6..ae28184db 100644 --- a/src/pubsub_db_sql.erl +++ b/src/pubsub_db_sql.erl @@ -25,12 +25,16 @@ -module(pubsub_db_sql). +-compile([{parse_transform, ejabberd_sql_pt}]). + -author("pablo.polvorin@process-one.net"). -include("pubsub.hrl"). +-include("ejabberd_sql_pt.hrl"). -export([add_subscription/1, read_subscription/1, delete_subscription/1, update_subscription/1]). +-export([export/1]). %% TODO: Those -spec lines produce errors in old Erlang versions. %% They can be enabled again in ejabberd 3.0 because it uses R12B or higher. @@ -139,3 +143,93 @@ sql_to_integer(N) -> binary_to_integer(N). sql_to_boolean(B) -> B == <<"1">>. sql_to_timestamp(T) -> xmpp_util:decode_timestamp(T). + +%% REVIEW: +%% * this code takes NODEID from Itemid2, and forgets about Nodeidx +%% * this code assumes Payload only contains one xmlelement() +%% * PUBLISHER is taken from Creation +export(_Server) -> + [{pubsub_item, + fun(_Host, #pubsub_item{itemid = {Itemid1, NODEID}, + %nodeidx = _Nodeidx, + creation = {{C1, C2, C3}, Cusr}, + modification = {{M1, M2, M3}, _Musr}, + payload = Payload}) -> + ITEMID = ejabberd_sql:escape(Itemid1), + CREATION = ejabberd_sql:escape(list_to_binary( + string:join([string:right(integer_to_list(I),6,$0)||I<-[C1,C2,C3]],":"))), + MODIFICATION = ejabberd_sql:escape(list_to_binary( + string:join([string:right(integer_to_list(I),6,$0)||I<-[M1,M2,M3]],":"))), + PUBLISHER = ejabberd_sql:escape(jid:encode(Cusr)), + [PayloadEl] = [El || {xmlel,_,_,_} = El <- Payload], + PAYLOAD = ejabberd_sql:escape(fxml:element_to_binary(PayloadEl)), + [?SQL("delete from pubsub_item where itemid=%(ITEMID)s;"), + ?SQL("insert into pubsub_item(itemid,nodeid,creation,modification,publisher,payload) \n" + " values (%(ITEMID)s, %(NODEID)d, %(CREATION)s, + %(MODIFICATION)s, %(PUBLISHER)s, %(PAYLOAD)s);")]; + (_Host, _R) -> + [] + end}, +%% REVIEW: +%% * From the mnesia table, the #pubsub_state.items is not used in ODBC +%% * Right now AFFILIATION is the first letter of Affiliation +%% * Right now SUBSCRIPTIONS expects only one Subscription +%% * Right now SUBSCRIPTIONS letter is the first letter of Subscription + {pubsub_state, + fun(_Host, #pubsub_state{stateid = {Jid, Stateid}, + %nodeidx = Nodeidx, + items = _Items, + affiliation = Affiliation, + subscriptions = Subscriptions}) -> + STATEID = list_to_binary(integer_to_list(Stateid)), + JID = ejabberd_sql:escape(jid:encode(Jid)), + NODEID = <<"unknown">>, %% TODO: integer_to_list(Nodeidx), + AFFILIATION = list_to_binary(string:substr(atom_to_list(Affiliation),1,1)), + SUBSCRIPTIONS = list_to_binary(parse_subscriptions(Subscriptions)), + [?SQL("delete from pubsub_state where stateid=%(STATEID)s;"), + ?SQL("insert into pubsub_state(stateid,jid,nodeid,affiliation,subscriptions)\n" + " values (%(STATEID)s, %(JID)s, %(NODEID)s, %(AFFILIATION)s, %(SUBSCRIPTIONS)s);")]; + (_Host, _R) -> + [] + end}, + +%% REVIEW: +%% * Parents is not migrated to PARENTs +%% * Probably some option VALs are not correctly represented in mysql + {pubsub_node, + fun(_Host, #pubsub_node{nodeid = {Hostid, Nodeid}, + id = Id, + parents = _Parents, + type = Type, + owners = Owners, + options = Options}) -> + HOST = case Hostid of + {U,S,R} -> ejabberd_sql:escape(jid:encode({U,S,R})); + _ -> ejabberd_sql:escape(Hostid) + end, + NODE = ejabberd_sql:escape(Nodeid), + PARENT = <<"">>, + IdB = integer_to_binary(Id), + TYPE = ejabberd_sql:escape(<>), + [?SQL("delete from pubsub_node where nodeid=%(Id)d;"), + ?SQL("insert into pubsub_node(host,node,nodeid,parent,type) \n" + " values (%(HOST)s, %(NODE)s, %(Id)d, %(PARENT)s, %(TYPE)s);"), + ?SQL("delete from pubsub_node_option where nodeid=%(Id)d;"), + [["insert into pubsub_node_option(nodeid,name,val)\n" + " values (", IdB, ", '", atom_to_list(Name), "', '", + io_lib:format("~p", [Val]), "');\n"] || {Name,Val} <- Options], + ?SQL("delete from pubsub_node_owner where nodeid=%(Id)d;"), + [["insert into pubsub_node_owner(nodeid,owner)\n" + " values (", IdB, ", '", jid:encode(Usr), "');\n"] || Usr <- Owners],"\n"]; + (_Host, _R) -> + [] + end}]. + +parse_subscriptions([]) -> + ""; +parse_subscriptions([{State, Item}]) -> + STATE = case State of + subscribed -> "s" + end, + string:join([STATE, Item],":"). + diff --git a/src/randoms.erl b/src/randoms.erl index ad07b47c2..7686edcff 100644 --- a/src/randoms.erl +++ b/src/randoms.erl @@ -32,6 +32,20 @@ -define(THRESHOLD, 16#10000000000000000). +-ifdef(RAND_UNIFORM). +get_string() -> + R = rand:uniform(?THRESHOLD), + integer_to_binary(R). + +uniform() -> + rand:uniform(). + +uniform(N) -> + rand:uniform(N). + +uniform(N, M) -> + rand:uniform(M-N+1) + N-1. +-else. get_string() -> R = crypto:rand_uniform(0, ?THRESHOLD), integer_to_binary(R). @@ -44,6 +58,7 @@ uniform(N) -> uniform(N, M) -> crypto:rand_uniform(N, M+1). +-endif. -ifdef(STRONG_RAND_BYTES). bytes(N) -> diff --git a/test/ejabberd_SUITE.erl b/test/ejabberd_SUITE.erl index 17465617b..97c56159a 100644 --- a/test/ejabberd_SUITE.erl +++ b/test/ejabberd_SUITE.erl @@ -431,6 +431,7 @@ db_tests(DB) when DB == mnesia; DB == redis -> mam_tests:single_cases(), carbons_tests:single_cases(), csi_tests:single_cases(), + push_tests:single_cases(), test_unregister]}, muc_tests:master_slave_cases(), privacy_tests:master_slave_cases(), @@ -441,7 +442,8 @@ db_tests(DB) when DB == mnesia; DB == redis -> vcard_tests:master_slave_cases(), announce_tests:master_slave_cases(), carbons_tests:master_slave_cases(), - csi_tests:master_slave_cases()]; + csi_tests:master_slave_cases(), + push_tests:master_slave_cases()]; db_tests(_) -> [{single_user, [sequence], [test_register, @@ -748,25 +750,25 @@ test_component_send(Config) -> disconnect(Config). s2s_dialback(Config) -> - ejabberd_s2s:stop_all_connections(), + ejabberd_s2s:stop_s2s_connections(), ejabberd_config:add_option(s2s_use_starttls, false), ejabberd_config:add_option(domain_certfile, "self-signed-cert.pem"), s2s_ping(Config). s2s_optional(Config) -> - ejabberd_s2s:stop_all_connections(), + ejabberd_s2s:stop_s2s_connections(), ejabberd_config:add_option(s2s_use_starttls, optional), ejabberd_config:add_option(domain_certfile, "self-signed-cert.pem"), s2s_ping(Config). s2s_required(Config) -> - ejabberd_s2s:stop_all_connections(), + ejabberd_s2s:stop_s2s_connections(), ejabberd_config:add_option(s2s_use_starttls, required), ejabberd_config:add_option(domain_certfile, "self-signed-cert.pem"), s2s_ping(Config). s2s_required_trusted(Config) -> - ejabberd_s2s:stop_all_connections(), + ejabberd_s2s:stop_s2s_connections(), ejabberd_config:add_option(s2s_use_starttls, required), ejabberd_config:add_option(domain_certfile, "cert.pem"), s2s_ping(Config). diff --git a/test/ejabberd_SUITE_data/ejabberd.yml b/test/ejabberd_SUITE_data/ejabberd.yml index 89618c0c0..a648cb422 100644 --- a/test/ejabberd_SUITE_data/ejabberd.yml +++ b/test/ejabberd_SUITE_data/ejabberd.yml @@ -231,7 +231,11 @@ Welcome to this XMPP server." mod_disco: [] mod_ping: [] mod_proxy65: [] + mod_push: [] + mod_push_keepalive: [] mod_s2s_dialback: [] + mod_stream_mgmt: + resume_timeout: 3 mod_legacy_auth: [] mod_register: welcome_message: @@ -290,7 +294,11 @@ Welcome to this XMPP server." mod_disco: [] mod_ping: [] mod_proxy65: [] + mod_push: [] + mod_push_keepalive: [] mod_s2s_dialback: [] + mod_stream_mgmt: + resume_timeout: 3 mod_legacy_auth: [] mod_register: welcome_message: @@ -450,6 +458,8 @@ listen: port: @@web_port@@ module: ejabberd_http captcha: true + request_handlers: + "/api": mod_http_api - port: @@component_port@@ module: ejabberd_service @@ -466,6 +476,7 @@ modules: mod_proxy65: [] mod_legacy: [] mod_muc: [] + mod_muc_admin: [] mod_register: welcome_message: subject: "Welcome!" @@ -488,3 +499,8 @@ outgoing_s2s_port: @@s2s_port@@ shaper: fast: 50000 normal: 10000 + +api_permissions: + "public commands": + who: all + what: "*" diff --git a/test/ejabberd_SUITE_data/extauth.py b/test/ejabberd_SUITE_data/extauth.py index fa2c9efd0..263d6464e 100755 --- a/test/ejabberd_SUITE_data/extauth.py +++ b/test/ejabberd_SUITE_data/extauth.py @@ -3,23 +3,27 @@ import struct def read(): (pkt_size,) = struct.unpack('>H', sys.stdin.read(2)) - pkt = sys.stdin.read(pkt_size).split(':') - cmd = pkt[0] - args_num = len(pkt) - 1 - if cmd == 'auth' and args_num >= 3: - if pkt[1] == "wrong": + pkt = sys.stdin.read(pkt_size) + cmd = pkt.split(':')[0] + if cmd == 'auth': + u, s, p = pkt.split(':', 3)[1:] + if u == "wrong": write(False) else: write(True) - elif cmd == 'isuser' and args_num == 2: + elif cmd == 'isuser': + u, s = pkt.split(':', 2)[1:] + elif cmd == 'setpass': + u, s, p = pkt.split(':', 3)[1:] write(True) - elif cmd == 'setpass' and args_num >= 3: + elif cmd == 'tryregister': + u, s, p = pkt.split(':', 3)[1:] write(True) - elif cmd == 'tryregister' and args_num >= 3: + elif cmd == 'removeuser': + u, s = pkt.split(':', 2)[1:] write(True) - elif cmd == 'removeuser' and args_num == 2: - write(True) - elif cmd == 'removeuser3' and args_num >= 3: + elif cmd == 'removeuser3': + u, s, p = pkt.split(':', 3)[1:] write(True) else: write(False) diff --git a/test/muc_tests.erl b/test/muc_tests.erl index 9ded2e0fe..bcceb6938 100644 --- a/test/muc_tests.erl +++ b/test/muc_tests.erl @@ -53,7 +53,8 @@ single_cases() -> single_test(service_vcard), single_test(configure_non_existent), single_test(cancel_configure_non_existent), - single_test(service_subscriptions)]}. + single_test(service_subscriptions), + single_test(set_room_affiliation)]}. service_presence_error(Config) -> Service = muc_jid(Config), @@ -242,6 +243,32 @@ service_subscriptions(Config) -> end, Rooms), disconnect(Config). +set_room_affiliation(Config) -> + #jid{server = RoomService} = muc_jid(Config), + RoomName = <<"set_room_affiliation">>, + RoomJID = jid:make(RoomName, RoomService), + MyJID = my_jid(Config), + PeerJID = jid:remove_resource(?config(slave, Config)), + + ct:pal("joining room ~p", [RoomJID]), + ok = join_new(Config, RoomJID), + + ct:pal("setting affiliation in room ~p to 'member' for ~p", [RoomJID, PeerJID]), + ServerHost = ?config(server_host, Config), + WebPort = ct:get_config(web_port, 5280), + RequestURL = "http://" ++ ServerHost ++ ":" ++ integer_to_list(WebPort) ++ "/api/set_room_affiliation", + Headers = [{"X-Admin", "true"}], + ContentType = "application/json", + Body = jiffy:encode(#{name => RoomName, service => RoomService, jid => jid:encode(PeerJID), affiliation => member}), + {ok, {{_, 200, _}, _, _}} = httpc:request(post, {RequestURL, Headers, ContentType, Body}, [], []), + + #message{id = _, from = RoomJID, to = MyJID, sub_els = [ + #muc_user{items = [ + #muc_item{affiliation = member, role = none, jid = PeerJID}]}]} = recv_message(Config), + + ok = leave(Config, RoomJID), + disconnect(Config). + %%%=================================================================== %%% Master-slave tests %%%=================================================================== diff --git a/test/push_tests.erl b/test/push_tests.erl new file mode 100644 index 000000000..b1f3a8b78 --- /dev/null +++ b/test/push_tests.erl @@ -0,0 +1,234 @@ +%%%------------------------------------------------------------------- +%%% Author : Holger Weiss +%%% Created : 15 Jul 2017 by Holger Weiss +%%% +%%% +%%% ejabberd, Copyright (C) 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(push_tests). + +%% API +-compile(export_all). +-import(suite, [close_socket/1, connect/1, disconnect/1, get_event/1, + get_features/2, make_iq_result/1, my_jid/1, put_event/2, recv/1, + recv_iq/1, recv_message/1, self_presence/2, send/2, send_recv/2, + server_jid/1]). + +-include("suite.hrl"). + +-define(PUSH_NODE, <<"d3v1c3">>). +-define(PUSH_XDATA_FIELDS, + [#xdata_field{var = <<"FORM_TYPE">>, + values = [?NS_PUBSUB_PUBLISH_OPTIONS]}, + #xdata_field{var = <<"secret">>, + values = [<<"c0nf1d3nt14l">>]}]). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {push_single, [sequence], + [single_test(feature_enabled), + single_test(unsupported_iq)]}. + +feature_enabled(Config) -> + BareMyJID = jid:remove_resource(my_jid(Config)), + Features = get_features(Config, BareMyJID), + true = lists:member(?NS_PUSH_0, Features), + disconnect(Config). + +unsupported_iq(Config) -> + PushJID = my_jid(Config), + lists:foreach( + fun(SubEl) -> + #iq{type = error} = + send_recv(Config, #iq{type = get, sub_els = [SubEl]}) + end, [#push_enable{jid = PushJID}, #push_disable{jid = PushJID}]), + disconnect(Config). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {push_master_slave, [sequence], + [master_slave_test(sm), + master_slave_test(offline), + master_slave_test(mam)]}. + +sm_master(Config) -> + ct:comment("Waiting for the slave to close the socket"), + peer_down = get_event(Config), + ct:comment("Waiting a bit in order to test the keepalive feature"), + ct:sleep(5000), % Without mod_push_keepalive, the session would time out. + ct:comment("Sending message to the slave"), + send_test_message(Config), + ct:comment("Handling push notification"), + handle_notification(Config), + ct:comment("Receiving bounced message from the slave"), + #message{type = error} = recv_message(Config), + ct:comment("Closing the connection"), + disconnect(Config). + +sm_slave(Config) -> + ct:comment("Enabling push notifications"), + ok = enable_push(Config), + ct:comment("Enabling stream management"), + ok = enable_sm(Config), + ct:comment("Closing the socket"), + close_socket(Config). + +offline_master(Config) -> + ct:comment("Waiting for the slave to be ready"), + ready = get_event(Config), + ct:comment("Sending message to the slave"), + send_test_message(Config), % No push notification, slave is online. + ct:comment("Waiting for the slave to disconnect"), + peer_down = get_event(Config), + ct:comment("Sending message to offline storage"), + send_test_message(Config), + ct:comment("Handling push notification for offline message"), + handle_notification(Config), + ct:comment("Closing the connection"), + disconnect(Config). + +offline_slave(Config) -> + ct:comment("Re-enabling push notifications"), + ok = enable_push(Config), + ct:comment("Letting the master know that we're ready"), + put_event(Config, ready), + ct:comment("Receiving message from the master"), + recv_test_message(Config), + ct:comment("Closing the connection"), + disconnect(Config). + +mam_master(Config) -> + ct:comment("Waiting for the slave to be ready"), + ready = get_event(Config), + ct:comment("Sending message to the slave"), + send_test_message(Config), + ct:comment("Handling push notification for MAM message"), + handle_notification(Config), + ct:comment("Closing the connection"), + disconnect(Config). + +mam_slave(Config) -> + self_presence(Config, available), + ct:comment("Receiving message from offline storage"), + recv_test_message(Config), + %% Don't re-enable push notifications, otherwise the notification would be + %% suppressed while the slave is online. + ct:comment("Enabling MAM"), + ok = enable_mam(Config), + ct:comment("Letting the master know that we're ready"), + put_event(Config, ready), + ct:comment("Receiving message from the master"), + recv_test_message(Config), + ct:comment("Waiting for the master to disconnect"), + peer_down = get_event(Config), + ct:comment("Disabling push notifications"), + ok = disable_push(Config), + ct:comment("Closing the connection and cleaning up"), + clean(disconnect(Config)). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("push_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("push_" ++ atom_to_list(T)), [parallel], + [list_to_atom("push_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("push_" ++ atom_to_list(T) ++ "_slave")]}. + +enable_sm(Config) -> + send(Config, #sm_enable{xmlns = ?NS_STREAM_MGMT_3, resume = true}), + case recv(Config) of + #sm_enabled{resume = true} -> + ok; + #sm_failed{reason = Reason} -> + Reason + end. + +enable_mam(Config) -> + case send_recv( + Config, #iq{type = set, sub_els = [#mam_prefs{xmlns = ?NS_MAM_1, + default = always}]}) of + #iq{type = result} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +enable_push(Config) -> + %% Usually, the push JID would be a server JID (such as push.example.com). + %% We specify the peer's full user JID instead, so the push notifications + %% will be sent to the peer. + PushJID = ?config(peer, Config), + XData = #xdata{type = submit, fields = ?PUSH_XDATA_FIELDS}, + case send_recv( + Config, #iq{type = set, + sub_els = [#push_enable{jid = PushJID, + node = ?PUSH_NODE, + xdata = XData}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +disable_push(Config) -> + PushJID = ?config(peer, Config), + case send_recv( + Config, #iq{type = set, + sub_els = [#push_disable{jid = PushJID, + node = ?PUSH_NODE}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +send_test_message(Config) -> + Peer = ?config(peer, Config), + Msg = #message{to = Peer, body = [#text{data = <<"test">>}]}, + send(Config, Msg). + +recv_test_message(Config) -> + Peer = ?config(peer, Config), + #message{from = Peer, + body = [#text{data = <<"test">>}]} = recv_message(Config). + +handle_notification(Config) -> + From = server_jid(Config), + Item = #ps_item{xml_els = [xmpp:encode(#push_notification{})]}, + Publish = #ps_publish{node = ?PUSH_NODE, items = [Item]}, + XData = #xdata{type = submit, fields = ?PUSH_XDATA_FIELDS}, + PubSub = #pubsub{publish = Publish, publish_options = XData}, + IQ = #iq{type = set, from = From, sub_els = [PubSub]} = recv_iq(Config), + send(Config, make_iq_result(IQ)). + +clean(Config) -> + {U, S, _} = jid:tolower(my_jid(Config)), + mod_push:remove_user(U, S), + mod_mam:remove_user(U, S), + Config.