From 4b012a99d2bdd6d22f05676e9a7989409e314fca Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Fri, 26 Jan 2018 15:02:06 +0300 Subject: [PATCH] Introduce option 'captcha' for mod_block_strangers When the option is set to `true`, the module will generate CAPTCHA challenges for incoming subscription requests. The option also implies that option `drop` is set to `true`. Note that the module won't generate CAPTCHA challenges for messages: they will still be rejected if `drop` is set to `true`. Fixes #2246 --- src/ejabberd_c2s.erl | 14 ++- src/ejabberd_captcha.erl | 152 ++++++++++++++++++++++++--------- src/ejabberd_sm.erl | 20 ++--- src/ejabberd_sup.erl | 2 +- src/mod_block_strangers.erl | 145 +++++++++++++++++++++++-------- src/mod_caps.erl | 7 +- src/mod_last.erl | 4 +- src/mod_mam.erl | 8 +- src/mod_privacy.erl | 7 +- src/mod_pubsub.erl | 29 +++---- src/mod_roster.erl | 58 +++++++------ src/mod_roster_mnesia.erl | 4 +- src/mod_roster_riak.erl | 3 +- src/mod_roster_sql.erl | 72 +++++++--------- src/mod_shared_roster.erl | 90 +++++++++---------- src/mod_shared_roster_ldap.erl | 32 +++---- 16 files changed, 390 insertions(+), 257 deletions(-) diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 6e7ba1e67..06d4cf12d 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -643,8 +643,8 @@ route_probe_reply(_, _) -> ok. -spec process_presence_out(state(), presence()) -> state(). -process_presence_out(#{user := User, server := Server, lserver := LServer, - jid := JID, lang := Lang, pres_a := PresA} = State, +process_presence_out(#{lserver := LServer, jid := JID, + lang := Lang, pres_a := PresA} = State, #presence{from = From, to = To, type = Type} = Pres) -> if Type == subscribe; Type == subscribed; Type == unsubscribe; Type == unsubscribed -> @@ -656,9 +656,7 @@ process_presence_out(#{user := User, server := Server, lserver := LServer, AccessErr = xmpp:err_forbidden(AccessErrTxt, Lang), send_error(State, Pres, AccessErr); allow -> - ejabberd_hooks:run(roster_out_subscription, - LServer, - [User, Server, To, Type]) + ejabberd_hooks:run(roster_out_subscription, LServer, [Pres]) end; true -> ok end, @@ -839,9 +837,9 @@ route_multiple(#{lserver := LServer}, JIDs, Pkt) -> ejabberd_router_multicast:route_multicast(From, LServer, JIDs, Pkt). get_subscription(#jid{luser = LUser, lserver = LServer}, JID) -> - {Subscription, _} = ejabberd_hooks:run_fold( - roster_get_jid_info, LServer, {none, []}, - [LUser, LServer, JID]), + {Subscription, _, _} = ejabberd_hooks:run_fold( + roster_get_jid_info, LServer, {none, none, []}, + [LUser, LServer, JID]), Subscription. -spec resource_conflict_action(binary(), binary(), binary()) -> diff --git a/src/ejabberd_captcha.erl b/src/ejabberd_captcha.erl index e1f070380..3fad361fd 100644 --- a/src/ejabberd_captcha.erl +++ b/src/ejabberd_captcha.erl @@ -41,7 +41,8 @@ -export([create_captcha/6, build_captcha_html/2, check_captcha/2, process_reply/1, process/2, is_feature_available/0, create_captcha_x/5, - opt_type/1]). + opt_type/1, host_up/1, host_down/1, + config_reloaded/0, process_iq/1]). -include("xmpp.hrl"). -include("ejabberd.hrl"). @@ -53,7 +54,8 @@ -type image_error() :: efbig | enodata | limit | malformed_image | timeout. --record(state, {limits = treap:empty() :: treap:treap()}). +-record(state, {limits = treap:empty() :: treap:treap(), + enabled = false :: boolean()}). -record(captcha, {id :: binary(), pid :: pid() | undefined, @@ -214,6 +216,25 @@ process_reply(#xcaptcha{xdata = #xdata{} = X}) -> process_reply(_) -> {error, malformed}. +process_iq(#iq{type = set, lang = Lang, sub_els = [#xcaptcha{} = El]} = IQ) -> + case process_reply(El) of + ok -> + xmpp:make_iq_result(IQ); + {error, malformed} -> + Txt = <<"Incorrect CAPTCHA submit">>, + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); + {error, _} -> + Txt = <<"The CAPTCHA verification has failed">>, + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)) + end; +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} = IQ) -> + ?INFO_MSG("IQ = ~p", [IQ]), + Txt = <<"No module is handling this query">>, + xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + process(_Handlers, #request{method = 'GET', lang = Lang, path = [_, Id]}) -> @@ -261,12 +282,30 @@ process(_Handlers, process(_Handlers, _Request) -> ejabberd_web:error(not_found). +host_up(Host) -> + IQDisc = gen_iq_handler:iqdisc(Host), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_CAPTCHA, + ?MODULE, process_iq, IQDisc). + +host_down(Host) -> + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_CAPTCHA). + +config_reloaded() -> + gen_server:call(?MODULE, config_reloaded, timer:minutes(1)). + init([]) -> mnesia:delete_table(captcha), - ets:new(captcha, - [named_table, public, {keypos, #captcha.id}]), - check_captcha_setup(), - {ok, #state{}}. + ets:new(captcha, [named_table, public, {keypos, #captcha.id}]), + case check_captcha_setup() of + true -> + register_handlers(), + ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 50), + {ok, #state{enabled = true}}; + false -> + {ok, #state{enabled = false}}; + {error, Reason} -> + {stop, Reason} + end. handle_call({is_limited, Limiter, RateLimit}, _From, State) -> @@ -285,6 +324,23 @@ handle_call({is_limited, Limiter, RateLimit}, _From, Limits), {reply, false, State#state{limits = NewLimits}} end; +handle_call(config_reloaded, _From, #state{enabled = Enabled} = State) -> + State1 = case is_feature_available() of + true when not Enabled -> + case check_captcha_setup() of + true -> + register_handlers(), + State#state{enabled = true}; + _ -> + State + end; + false when Enabled -> + unregister_handlers(), + State#state{enabled = false}; + _ -> + State + end, + {reply, ok, State1}; handle_call(_Request, _From, State) -> {reply, bad_request, State}. @@ -293,17 +349,29 @@ handle_cast(_Msg, State) -> {noreply, State}. handle_info({remove_id, Id}, State) -> ?DEBUG("captcha ~p timed out", [Id]), case ets:lookup(captcha, Id) of - [#captcha{args = Args, pid = Pid}] -> - if is_pid(Pid) -> Pid ! {captcha_failed, Args}; - true -> ok - end, - ets:delete(captcha, Id); - _ -> ok + [#captcha{args = Args, pid = Pid}] -> + callback(captcha_failed, Pid, Args), + ets:delete(captcha, Id); + _ -> ok end, {noreply, State}; handle_info(_Info, State) -> {noreply, State}. -terminate(_Reason, _State) -> ok. +terminate(_Reason, #state{enabled = Enabled}) -> + if Enabled -> unregister_handlers(); + true -> ok + end, + ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 50). + +register_handlers() -> + ejabberd_hooks:add(host_up, ?MODULE, host_up, 50), + ejabberd_hooks:add(host_down, ?MODULE, host_down, 50), + lists:foreach(fun host_up/1, ?MYHOSTS). + +unregister_handlers() -> + ejabberd_hooks:delete(host_up, ?MODULE, host_up, 50), + ejabberd_hooks:delete(host_down, ?MODULE, host_down, 50), + lists:foreach(fun host_down/1, ?MYHOSTS). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -467,16 +535,18 @@ is_feature_available() -> check_captcha_setup() -> case is_feature_available() of - true -> - case create_image() of - {ok, _, _, _} -> ok; - _Err -> - ?CRITICAL_MSG("Captcha is enabled in the option captcha_cmd, " - "but it can't generate images.", - []), - throw({error, captcha_cmd_enabled_but_fails}) - end; - false -> ok + true -> + case create_image() of + {ok, _, _, _} -> + true; + Err -> + ?CRITICAL_MSG("Captcha is enabled in the option captcha_cmd, " + "but it can't generate images.", + []), + Err + end; + false -> + false end. lookup_captcha(Id) -> @@ -491,22 +561,18 @@ lookup_captcha(Id) -> check_captcha(Id, ProvidedKey) -> case ets:lookup(captcha, Id) of - [#captcha{pid = Pid, args = Args, key = ValidKey, - tref = Tref}] -> - ets:delete(captcha, Id), - erlang:cancel_timer(Tref), - if ValidKey == ProvidedKey -> - if is_pid(Pid) -> Pid ! {captcha_succeed, Args}; - true -> ok - end, - captcha_valid; - true -> - if is_pid(Pid) -> Pid ! {captcha_failed, Args}; - true -> ok - end, - captcha_non_valid - end; - _ -> captcha_not_found + [#captcha{pid = Pid, args = Args, key = ValidKey, tref = Tref}] -> + ets:delete(captcha, Id), + erlang:cancel_timer(Tref), + if ValidKey == ProvidedKey -> + callback(captcha_succeed, Pid, Args), + captcha_valid; + true -> + callback(captcha_failed, Pid, Args), + captcha_non_valid + end; + _ -> + captcha_not_found end. clean_treap(Treap, CleanPriority) -> @@ -520,6 +586,14 @@ clean_treap(Treap, CleanPriority) -> end end. +-spec callback(captcha_succeed | captcha_failed, pid(), term()) -> any(). +callback(Result, _Pid, F) when is_function(F) -> + F(Result); +callback(Result, Pid, Args) when is_pid(Pid) -> + Pid ! {Result, Args}; +callback(_, _, _) -> + ok. + now_priority() -> -p1_time_compat:system_time(micro_seconds). diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl index 64c9e5f77..54f248f5e 100644 --- a/src/ejabberd_sm.erl +++ b/src/ejabberd_sm.erl @@ -43,7 +43,7 @@ open_session/5, open_session/6, close_session/4, - check_in_subscription/6, + check_in_subscription/2, bounce_offline_message/1, disconnect_removed_user/2, get_user_resources/2, @@ -183,10 +183,9 @@ close_session(SID, User, Server, Resource) -> ejabberd_hooks:run(sm_remove_connection_hook, JID#jid.lserver, [SID, JID, Info]). --spec check_in_subscription(boolean(), binary(), binary(), jid(), - subscribe | subscribed | unsubscribe | unsubscribed, - binary()) -> boolean() | {stop, false}. -check_in_subscription(Acc, User, Server, _JID, _Type, _Reason) -> +-spec check_in_subscription(boolean(), presence()) -> boolean() | {stop, false}. +check_in_subscription(Acc, #presence{to = To}) -> + #jid{user = User, server = Server} = To, case ejabberd_auth:user_exists(User, Server) of true -> Acc; false -> {stop, false} @@ -627,19 +626,14 @@ do_route(To, Term) -> end. -spec do_route(stanza()) -> any(). -do_route(#presence{from = From, to = To, type = T, status = Status} = Packet) +do_route(#presence{to = To, type = T} = Packet) when T == subscribe; T == subscribed; T == unsubscribe; T == unsubscribed -> ?DEBUG("processing subscription:~n~s", [xmpp:pp(Packet)]), - #jid{user = User, server = Server, - luser = LUser, lserver = LServer} = To, - Reason = if T == subscribe -> xmpp:get_text(Status); - true -> <<"">> - end, + #jid{luser = LUser, lserver = LServer} = To, case is_privacy_allow(Packet) andalso ejabberd_hooks:run_fold( roster_in_subscription, - LServer, false, - [User, Server, From, T, Reason]) of + LServer, false, [Packet]) of true -> Mod = get_sm_backend(LServer), lists:foreach( diff --git a/src/ejabberd_sup.erl b/src/ejabberd_sup.erl index cea0c279b..5980937fe 100644 --- a/src/ejabberd_sup.erl +++ b/src/ejabberd_sup.erl @@ -166,7 +166,6 @@ init([]) -> ACME, Listener, S2S, - Captcha, S2SInSupervisor, S2SOutSupervisor, ServiceSupervisor, @@ -182,6 +181,7 @@ init([]) -> RouterMulticast, Local, SM, + Captcha, ExtMod, GenModSupervisor, Auth, diff --git a/src/mod_block_strangers.erl b/src/mod_block_strangers.erl index e865754e2..bd933c7a1 100644 --- a/src/mod_block_strangers.erl +++ b/src/mod_block_strangers.erl @@ -32,7 +32,7 @@ -export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1, mod_options/1]). --export([filter_packet/1, filter_offline_msg/1]). +-export([filter_packet/1, filter_offline_msg/1, filter_subscription/2]). -include("xmpp.hrl"). -include("ejabberd.hrl"). @@ -40,15 +40,22 @@ -define(SETS, gb_sets). +%%%=================================================================== +%%% Callbacks and hooks +%%%=================================================================== start(Host, _Opts) -> ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, filter_packet, 25), + ejabberd_hooks:add(roster_in_subscription, Host, + ?MODULE, filter_subscription, 25), ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, filter_offline_msg, 25). stop(Host) -> ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, filter_packet, 25), + ejabberd_hooks:delete(roster_in_subscription, Host, + ?MODULE, filter_subscription, 25), ejabberd_hooks:delete(offline_message_hook, Host, ?MODULE, filter_offline_msg, 25). @@ -79,17 +86,72 @@ filter_offline_msg({_Action, #message{} = Msg} = Acc) -> deny -> {stop, {drop, Msg}} end. +filter_subscription(Acc, #presence{meta = #{captcha := passed}}) -> + Acc; +filter_subscription(Acc, #presence{from = From, to = To, lang = Lang, + id = SID, type = subscribe} = Pres) -> + LServer = To#jid.lserver, + case gen_mod:get_module_opt(LServer, ?MODULE, drop) andalso + gen_mod:get_module_opt(LServer, ?MODULE, captcha) andalso + need_check(Pres) of + true -> + case check_subscription(From, To) of + false -> + BFrom = jid:remove_resource(From), + BTo = jid:remove_resource(To), + Limiter = jid:tolower(BFrom), + case ejabberd_captcha:create_captcha( + SID, BFrom, BTo, Lang, Limiter, + fun(Res) -> handle_captcha_result(Res, Pres) end) of + {ok, ID, Body, CaptchaEls} -> + Msg = #message{from = BTo, to = From, + id = ID, body = Body, + sub_els = CaptchaEls}, + case gen_mod:get_module_opt(LServer, ?MODULE, log) of + true -> + ?INFO_MSG("Challenge subscription request " + "from stranger ~s to ~s with " + "CAPTCHA", + [jid:encode(From), jid:encode(To)]); + false -> + ok + end, + ejabberd_router:route(Msg); + {error, limit} -> + ErrText = <<"Too many CAPTCHA requests">>, + Err = xmpp:err_resource_constraint(ErrText, Lang), + ejabberd_router:route_error(Pres, Err); + _ -> + ErrText = <<"Unable to generate a CAPTCHA">>, + Err = xmpp:err_internal_server_error(ErrText, Lang), + ejabberd_router:route_error(Pres, Err) + end, + {stop, false}; + true -> + Acc + end; + false -> + Acc + end; +filter_subscription(Acc, _) -> + Acc. + +handle_captcha_result(captcha_succeed, Pres) -> + Pres1 = xmpp:put_meta(Pres, captcha, passed), + ejabberd_router:route(Pres1); +handle_captcha_result(captcha_failed, #presence{lang = Lang} = Pres) -> + Txt = <<"The CAPTCHA verification has failed">>, + ejabberd_router:route_error(Pres, xmpp:err_not_allowed(Txt, Lang)). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== check_message(#message{from = From, to = To, lang = Lang} = Msg) -> LServer = To#jid.lserver, - AllowLocalUsers = - gen_mod:get_module_opt(LServer, ?MODULE, allow_local_users), - case (Msg#message.body == [] andalso - Msg#message.subject == []) - orelse ((AllowLocalUsers orelse From#jid.luser == <<"">>) andalso - ejabberd_router:is_my_host(From#jid.lserver)) of - false -> + case need_check(Msg) of + true -> case check_subscription(From, To) of - none -> + false -> Drop = gen_mod:get_module_opt(LServer, ?MODULE, drop), Log = gen_mod:get_module_opt(LServer, ?MODULE, log), if @@ -112,10 +174,10 @@ check_message(#message{from = From, to = To, lang = Lang} = Msg) -> true -> allow end; - some -> + true -> allow end; - true -> + false -> allow end. @@ -125,34 +187,46 @@ maybe_adjust_from(#message{type = groupchat, from = From} = Msg) -> maybe_adjust_from(#message{} = Msg) -> Msg. --spec check_subscription(jid(), jid()) -> none | some. +-spec need_check(presence() | message()) -> boolean(). +need_check(Pkt) -> + To = xmpp:get_to(Pkt), + From = xmpp:get_from(Pkt), + LServer = To#jid.lserver, + IsEmpty = case Pkt of + #message{body = [], subject = []} -> + true; + _ -> + false + end, + AllowLocalUsers = gen_mod:get_module_opt(LServer, ?MODULE, allow_local_users), + not (IsEmpty orelse ((AllowLocalUsers orelse From#jid.luser == <<"">>) + andalso ejabberd_router:is_my_host(From#jid.lserver))). + +-spec check_subscription(jid(), jid()) -> boolean(). check_subscription(From, To) -> {LocalUser, LocalServer, _} = jid:tolower(To), {RemoteUser, RemoteServer, _} = jid:tolower(From), - case ejabberd_hooks:run_fold( - roster_get_jid_info, LocalServer, - {none, []}, [LocalUser, LocalServer, From]) of - {none, _} when RemoteUser == <<"">> -> - none; - {none, _} -> - case gen_mod:get_module_opt(LocalServer, ?MODULE, - allow_transports) of - true -> - %% Check if the contact's server is in the roster - case ejabberd_hooks:run_fold( - roster_get_jid_info, LocalServer, - {none, []}, - [LocalUser, LocalServer, jid:make(RemoteServer)]) of - {none, _} -> none; - _ -> some - end; - false -> - none - end; - _ -> - some + case has_subscription(LocalUser, LocalServer, From) of + false when RemoteUser == <<"">> -> + false; + false -> + %% Check if the contact's server is in the roster + gen_mod:get_module_opt(LocalServer, ?MODULE, allow_transports) + andalso has_subscription(LocalUser, LocalServer, + jid:make(RemoteServer)); + true -> + true end. +-spec has_subscription(binary(), binary(), jid()) -> boolean(). +has_subscription(User, Server, JID) -> + {Sub, Ask, _} = ejabberd_hooks:run_fold( + roster_get_jid_info, Server, + {none, none, []}, + [User, Server, JID]), + (Sub /= none) orelse (Ask == subscribe) + orelse (Ask == out) orelse (Ask == both). + sets_bare_member({U, S, <<"">>} = LBJID, Set) -> case ?SETS:next(sets_iterator_from(LBJID, Set)) of {{U, S, _}, _} -> true; @@ -189,10 +263,13 @@ mod_opt_type(log) -> mod_opt_type(allow_local_users) -> fun (B) when is_boolean(B) -> B end; mod_opt_type(allow_transports) -> + fun (B) when is_boolean(B) -> B end; +mod_opt_type(captcha) -> fun (B) when is_boolean(B) -> B end. mod_options(_) -> [{drop, true}, {log, false}, + {captcha, false}, {allow_local_users, true}, {allow_transports, true}]. diff --git a/src/mod_caps.erl b/src/mod_caps.erl index 6b23b193c..7d26178fa 100644 --- a/src/mod_caps.erl +++ b/src/mod_caps.erl @@ -203,9 +203,10 @@ disco_info(Acc, _, _, _Node, _Lang) -> -spec c2s_presence_in(ejabberd_c2s:state(), presence()) -> ejabberd_c2s:state(). c2s_presence_in(C2SState, #presence{from = From, to = To, type = Type} = Presence) -> - {Subscription, _} = ejabberd_hooks:run_fold( - roster_get_jid_info, To#jid.lserver, - {none, []}, [To#jid.luser, To#jid.lserver, From]), + {Subscription, _, _} = ejabberd_hooks:run_fold( + roster_get_jid_info, To#jid.lserver, + {none, none, []}, + [To#jid.luser, To#jid.lserver, From]), ToSelf = (From#jid.luser == To#jid.luser) and (From#jid.lserver == To#jid.lserver), Insert = (Type == available) diff --git a/src/mod_last.erl b/src/mod_last.erl index f9edb91f4..d69a5acac 100644 --- a/src/mod_last.erl +++ b/src/mod_last.erl @@ -142,9 +142,9 @@ process_sm_iq(#iq{type = set, lang = Lang} = IQ) -> process_sm_iq(#iq{from = From, to = To, lang = Lang} = IQ) -> User = To#jid.luser, Server = To#jid.lserver, - {Subscription, _Groups} = + {Subscription, _Ask, _Groups} = ejabberd_hooks:run_fold(roster_get_jid_info, Server, - {none, []}, [User, Server, From]), + {none, none, []}, [User, Server, From]), if (Subscription == both) or (Subscription == from) or (From#jid.luser == To#jid.luser) and (From#jid.lserver == To#jid.lserver) -> diff --git a/src/mod_mam.erl b/src/mod_mam.erl index b6f80be67..8e8f57171 100644 --- a/src/mod_mam.erl +++ b/src/mod_mam.erl @@ -672,10 +672,10 @@ should_archive_peer(LUser, LServer, always -> true; never -> false; roster -> - {Sub, _} = ejabberd_hooks:run_fold( - roster_get_jid_info, - LServer, {none, []}, - [LUser, LServer, Peer]), + {Sub, _, _} = ejabberd_hooks:run_fold( + roster_get_jid_info, + LServer, {none, none, []}, + [LUser, LServer, Peer]), Sub == both orelse Sub == from orelse Sub == to end end diff --git a/src/mod_privacy.erl b/src/mod_privacy.erl index 888a231ed..5dcd69720 100644 --- a/src/mod_privacy.erl +++ b/src/mod_privacy.erl @@ -589,9 +589,10 @@ do_check_packet(#jid{luser = LUser, lserver = LServer}, List, Packet, Dir) -> in -> jid:tolower(From); out -> jid:tolower(To) end, - {Subscription, Groups} = ejabberd_hooks:run_fold( - roster_get_jid_info, LServer, - {none, []}, [LUser, LServer, LJID]), + {Subscription, _Ask, Groups} = ejabberd_hooks:run_fold( + roster_get_jid_info, LServer, + {none, none, []}, + [LUser, LServer, LJID]), check_packet_aux(List, PType2, LJID, Subscription, Groups) end. diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl index 465d15a26..436ed1f3e 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, + in_subscription/2, out_subscription/1, on_self_presence/1, on_user_offline/2, remove_user/2, disco_local_identity/5, disco_local_features/5, disco_local_items/5, disco_sm_identity/5, @@ -605,22 +605,17 @@ on_user_offline(C2SState, _Reason) -> %% subscription hooks handling functions %% --spec out_subscription( - binary(), binary(), jid(), - subscribed | unsubscribed | subscribe | unsubscribe) -> boolean(). -out_subscription(User, Server, To, subscribed) -> - send_last_pep(jid:make(User, Server), To), - true; -out_subscription(_, _, _, _) -> - true. +-spec out_subscription(presence()) -> any(). +out_subscription(#presence{type = subscribed, from = From, to = To}) -> + send_last_pep(jid:remove_resource(From), To); +out_subscription(_) -> + ok. --spec in_subscription(boolean(), binary(), binary(), jid(), - subscribe | subscribed | unsubscribe | unsubscribed, - binary()) -> true. -in_subscription(_, User, Server, Owner, unsubscribed, _) -> - unsubscribe_user(jid:make(User, Server), Owner), +-spec in_subscription(boolean(), presence()) -> true. +in_subscription(_, #presence{to = To, from = Owner, type = unsubscribed}) -> + unsubscribe_user(jid:remove_resource(To), Owner), true; -in_subscription(_, _, _, _, _, _) -> +in_subscription(_, _) -> true. unsubscribe_user(Entity, Owner) -> @@ -2513,8 +2508,8 @@ get_roster_info(_, _, {<<>>, <<>>, _}, _) -> {false, false}; get_roster_info(OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, _}, AllowedGroups) -> LJID = {SubscriberUser, SubscriberServer, <<>>}, - {Subscription, Groups} = ejabberd_hooks:run_fold(roster_get_jid_info, - OwnerServer, {none, []}, + {Subscription, _Ask, Groups} = ejabberd_hooks:run_fold(roster_get_jid_info, + OwnerServer, {none, none, []}, [OwnerUser, OwnerServer, LJID]), PresenceSubscription = Subscription == both orelse Subscription == from orelse diff --git a/src/mod_roster.erl b/src/mod_roster.erl index f9779c47f..68d987e0e 100644 --- a/src/mod_roster.erl +++ b/src/mod_roster.erl @@ -44,8 +44,8 @@ import_info/0, process_local_iq/1, get_user_roster/2, import/5, get_roster/2, import_start/2, import_stop/2, - c2s_self_presence/1, in_subscription/6, - out_subscription/4, set_items/3, remove_user/2, + c2s_self_presence/1, in_subscription/2, + out_subscription/1, set_items/3, remove_user/2, get_jid_info/4, encode_item/1, webadmin_page/3, webadmin_user/4, get_versioning_feature/2, roster_versioning_enabled/1, roster_version/2, @@ -76,7 +76,7 @@ -callback get_roster(binary(), binary()) -> {ok, [#roster{}]} | error. -callback get_roster_item(binary(), binary(), ljid()) -> {ok, #roster{}} | error. -callback read_subscription_and_groups(binary(), binary(), ljid()) - -> {ok, {subscription(), [binary()]}} | error. + -> {ok, {subscription(), ask(), [binary()]}} | error. -callback roster_subscribe(binary(), binary(), ljid(), #roster{}) -> any(). -callback transaction(binary(), function()) -> {atomic, any()} | {aborted, any()}. -callback remove_user(binary(), binary()) -> any(). @@ -383,20 +383,28 @@ get_subscription_and_groups(LUser, LServer, LJID) -> fun() -> Items = get_roster(LUser, LServer), case lists:keyfind(LBJID, #roster.jid, Items) of - #roster{subscription = Sub, groups = Groups} -> - {ok, {Sub, Groups}}; + #roster{subscription = Sub, + ask = Ask, + groups = Groups} -> + {ok, {Sub, Ask, Groups}}; false -> error end end); false -> - Mod:read_subscription_and_groups(LUser, LServer, LBJID) + case Mod:read_subscription_and_groups(LUser, LServer, LBJID) of + {ok, {Sub, Groups}} -> + %% Backward compatibility for third-party backends + {ok, {Sub, none, Groups}}; + Other -> + Other + end end, case Res of {ok, SubAndGroups} -> SubAndGroups; error -> - {none, []} + {none, none, []} end. set_roster(#roster{us = {LUser, LServer}, jid = LJID} = Item) -> @@ -555,17 +563,19 @@ transaction(LUser, LServer, LJIDs, F) -> Err end. --spec in_subscription(boolean(), binary(), binary(), jid(), - subscribe | subscribed | unsubscribe | unsubscribed, - binary()) -> boolean(). -in_subscription(_, User, Server, JID, Type, Reason) -> +-spec in_subscription(boolean(), presence()) -> boolean(). +in_subscription(_, #presence{from = JID, to = To, + type = Type, status = Status}) -> + #jid{user = User, server = Server} = To, + Reason = if Type == subscribe -> xmpp:get_text(Status); + true -> <<"">> + end, process_subscription(in, User, Server, JID, Type, Reason). --spec out_subscription( - binary(), binary(), jid(), - subscribed | unsubscribed | subscribe | unsubscribe) -> boolean(). -out_subscription(User, Server, JID, Type) -> +-spec out_subscription(presence()) -> boolean(). +out_subscription(#presence{from = From, to = JID, type = Type}) -> + #jid{user = User, server = Server} = From, process_subscription(out, User, Server, JID, Type, <<"">>). process_subscription(Direction, User, Server, JID1, @@ -878,8 +888,8 @@ get_priority_from_presence(#presence{priority = Prio}) -> end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% --spec get_jid_info({subscription(), [binary()]}, binary(), binary(), jid()) - -> {subscription(), [binary()]}. +-spec get_jid_info({subscription(), ask(), [binary()]}, binary(), binary(), jid()) + -> {subscription(), ask(), [binary()]}. get_jid_info(_, User, Server, JID) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), @@ -1029,9 +1039,10 @@ user_roster_parse_query(User, Server, Items, Query) -> end. user_roster_subscribe_jid(User, Server, JID) -> - out_subscription(User, Server, JID, subscribe), UJID = jid:make(User, Server), - ejabberd_router:route(#presence{from = UJID, to = JID, type = subscribe}). + Presence = #presence{from = UJID, to = JID, type = subscribe}, + out_subscription(Presence), + ejabberd_router:route(Presence). user_roster_item_parse_query(User, Server, Items, Query) -> @@ -1043,12 +1054,11 @@ user_roster_item_parse_query(User, Server, Items, of {value, _} -> JID1 = jid:make(JID), - out_subscription(User, Server, JID1, - subscribed), UJID = jid:make(User, Server), - ejabberd_router:route( - #presence{from = UJID, to = JID1, - type = subscribed}), + Pres = #presence{from = UJID, to = JID1, + type = subscribed}, + out_subscription(Pres), + ejabberd_router:route(Pres), throw(submitted); false -> case lists:keysearch(<<"remove", diff --git a/src/mod_roster_mnesia.erl b/src/mod_roster_mnesia.erl index 67d302f8a..01e671b43 100644 --- a/src/mod_roster_mnesia.erl +++ b/src/mod_roster_mnesia.erl @@ -102,8 +102,8 @@ del_roster(LUser, LServer, LJID) -> read_subscription_and_groups(LUser, LServer, LJID) -> case mnesia:dirty_read(roster, {LUser, LServer, LJID}) of - [#roster{subscription = Subscription, groups = Groups}] -> - {ok, {Subscription, Groups}}; + [#roster{subscription = Subscription, ask = Ask, groups = Groups}] -> + {ok, {Subscription, Ask, Groups}}; _ -> error end. diff --git a/src/mod_roster_riak.erl b/src/mod_roster_riak.erl index 86fc02cf7..1f65d384c 100644 --- a/src/mod_roster_riak.erl +++ b/src/mod_roster_riak.erl @@ -88,8 +88,9 @@ del_roster(LUser, LServer, LJID) -> read_subscription_and_groups(LUser, LServer, LJID) -> case ejabberd_riak:get(roster, roster_schema(), {LUser, LServer, LJID}) of {ok, #roster{subscription = Subscription, + ask = Ask, groups = Groups}} -> - {ok, {Subscription, Groups}}; + {ok, {Subscription, Ask, Groups}}; _ -> error end. diff --git a/src/mod_roster_sql.erl b/src/mod_roster_sql.erl index c943fc446..85019e21d 100644 --- a/src/mod_roster_sql.erl +++ b/src/mod_roster_sql.erl @@ -171,25 +171,15 @@ del_roster(LUser, LServer, LJID) -> read_subscription_and_groups(LUser, LServer, LJID) -> SJID = jid:encode(LJID), case get_subscription(LServer, LUser, SJID) of - {selected, [{SSubscription}]} -> - Subscription = case SSubscription of - <<"B">> -> both; - <<"T">> -> to; - <<"F">> -> from; - <<"N">> -> none; - <<"">> -> none; - _ -> - ?ERROR_MSG("~s", [format_row_error( - LUser, LServer, - {subscription, SSubscription})]), - none - end, + {selected, [{SSubscription, SAsk}]} -> + Subscription = decode_subscription(LUser, LServer, SSubscription), + Ask = decode_ask(LUser, LServer, SAsk), Groups = case get_rostergroup_by_jid(LServer, LUser, SJID) of {selected, JGrps} when is_list(JGrps) -> [JGrp || {JGrp} <- JGrps]; _ -> [] end, - {ok, {Subscription, Groups}}; + {ok, {Subscription, Ask, Groups}}; _ -> error end. @@ -272,7 +262,7 @@ get_rostergroup_by_jid(LServer, LUser, SJID) -> get_subscription(LServer, LUser, SJID) -> ejabberd_sql:sql_query( LServer, - ?SQL("select @(subscription)s from rosterusers " + ?SQL("select @(subscription)s, @(ask)s from rosterusers " "where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")). update_roster_sql({LUser, LServer, SJID, Name, SSubscription, SAsk, AskMessage}, @@ -320,30 +310,8 @@ raw_to_record(LServer, try jid:decode(SJID) of JID -> LJID = jid:tolower(JID), - Subscription = case SSubscription of - <<"B">> -> both; - <<"T">> -> to; - <<"F">> -> from; - <<"N">> -> none; - <<"">> -> none; - _ -> - ?ERROR_MSG("~s", [format_row_error( - User, LServer, - {subscription, SSubscription})]), - none - end, - Ask = case SAsk of - <<"S">> -> subscribe; - <<"U">> -> unsubscribe; - <<"B">> -> both; - <<"O">> -> out; - <<"I">> -> in; - <<"N">> -> none; - <<"">> -> none; - _ -> - ?ERROR_MSG("~s", [format_row_error(User, LServer, {ask, SAsk})]), - none - end, + Subscription = decode_subscription(User, LServer, SSubscription), + Ask = decode_ask(User, LServer, SAsk), #roster{usj = {User, LServer, LJID}, us = {User, LServer}, jid = LJID, name = Nick, subscription = Subscription, ask = Ask, @@ -374,6 +342,32 @@ record_to_row( end, {LUser, LServer, SJID, Name, SSubscription, SAsk, AskMessage}. +decode_subscription(User, Server, S) -> + case S of + <<"B">> -> both; + <<"T">> -> to; + <<"F">> -> from; + <<"N">> -> none; + <<"">> -> none; + _ -> + ?ERROR_MSG("~s", [format_row_error(User, Server, {subscription, S})]), + none + end. + +decode_ask(User, Server, A) -> + case A of + <<"S">> -> subscribe; + <<"U">> -> unsubscribe; + <<"B">> -> both; + <<"O">> -> out; + <<"I">> -> in; + <<"N">> -> none; + <<"">> -> none; + _ -> + ?ERROR_MSG("~s", [format_row_error(User, Server, {ask, A})]), + none + end. + format_row_error(User, Server, Why) -> [case Why of {jid, JID} -> ["Malformed 'jid' field with value '", JID, "'"]; diff --git a/src/mod_shared_roster.erl b/src/mod_shared_roster.erl index fffd68d60..a1fda9438 100644 --- a/src/mod_shared_roster.erl +++ b/src/mod_shared_roster.erl @@ -33,7 +33,7 @@ import_info/0, webadmin_menu/3, webadmin_page/3, get_user_roster/2, get_jid_info/4, import/5, process_item/2, import_start/2, - in_subscription/6, out_subscription/4, c2s_self_presence/1, + in_subscription/2, out_subscription/1, c2s_self_presence/1, unset_presence/4, register_user/2, remove_user/2, list_groups/1, create_group/2, create_group/3, delete_group/2, get_group_opts/2, set_group_opts/3, @@ -235,12 +235,11 @@ process_item(RosterItem, Host) -> %% If it doesn't, then remove this user from any %% existing roster groups. [] -> - mod_roster:out_subscription(UserTo, ServerTo, - jid:make(UserFrom, ServerFrom), - unsubscribe), - mod_roster:in_subscription(false, UserFrom, ServerFrom, - jid:make(UserTo, ServerTo), - unsubscribe, <<"">>), + Pres = #presence{from = jid:make(UserTo, ServerTo), + to = jid:make(UserFrom, ServerFrom), + type = unsubscribe}, + mod_roster:out_subscription(Pres), + mod_roster:in_subscription(false, Pres), RosterItem#roster{subscription = both, ask = none}; %% If so, it means the user wants to add that contact %% to his personal roster @@ -268,22 +267,22 @@ set_new_rosteritems(UserFrom, ServerFrom, UserTo, RITo = build_roster_record(UserTo, ServerTo, UserFrom, ServerFrom, UserFrom, []), set_item(UserTo, ServerTo, <<"">>, RITo), - mod_roster:out_subscription(UserFrom, ServerFrom, JIDTo, - subscribe), - mod_roster:in_subscription(false, UserTo, ServerTo, - JIDFrom, subscribe, <<"">>), - mod_roster:out_subscription(UserTo, ServerTo, JIDFrom, - subscribed), - mod_roster:in_subscription(false, UserFrom, ServerFrom, - JIDTo, subscribed, <<"">>), - mod_roster:out_subscription(UserTo, ServerTo, JIDFrom, - subscribe), - mod_roster:in_subscription(false, UserFrom, ServerFrom, - JIDTo, subscribe, <<"">>), - mod_roster:out_subscription(UserFrom, ServerFrom, JIDTo, - subscribed), - mod_roster:in_subscription(false, UserTo, ServerTo, - JIDFrom, subscribed, <<"">>), + mod_roster:out_subscription( + #presence{from = JIDFrom, to = JIDTo, type = subscribe}), + mod_roster:in_subscription( + false, #presence{to = JIDTo, from = JIDFrom, type = subscribe}), + mod_roster:out_subscription( + #presence{from = JIDTo, to = JIDFrom, type = subscribed}), + mod_roster:in_subscription( + false, #presence{to = JIDFrom, from = JIDTo, type = subscribed}), + mod_roster:out_subscription( + #presence{from = JIDTo, to = JIDFrom, type = subscribe}), + mod_roster:in_subscription( + false, #presence{to = JIDFrom, from = JIDTo, type = subscribe}), + mod_roster:out_subscription( + #presence{from = JIDFrom, to = JIDTo, type = subscribed}), + mod_roster:in_subscription( + false, #presence{to = JIDTo, from = JIDFrom, type = subscribed}), RIFrom. set_item(User, Server, Resource, Item) -> @@ -294,9 +293,9 @@ set_item(User, Server, Resource, Item) -> items = [mod_roster:encode_item(Item)]}]}, ejabberd_router:route(ResIQ). --spec get_jid_info({subscription(), [binary()]}, binary(), binary(), jid()) - -> {subscription(), [binary()]}. -get_jid_info({Subscription, Groups}, User, Server, +-spec get_jid_info({subscription(), ask(), [binary()]}, binary(), binary(), jid()) + -> {subscription(), ask(), [binary()]}. +get_jid_info({Subscription, Ask, Groups}, User, Server, JID) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), @@ -320,33 +319,26 @@ get_jid_info({Subscription, Groups}, User, Server, NewGroups = if Groups == [] -> GroupNames; true -> Groups end, - {both, NewGroups}; - error -> {Subscription, Groups} + {both, none, NewGroups}; + error -> {Subscription, Ask, Groups} end. --spec in_subscription(boolean(), binary(), binary(), jid(), - subscribe | subscribed | unsubscribe | unsubscribed, - binary()) -> boolean(). -in_subscription(Acc, User, Server, JID, Type, - _Reason) -> +-spec in_subscription(boolean(), presence()) -> boolean(). +in_subscription(Acc, #presence{to = To, from = JID, type = Type}) -> + #jid{user = User, server = Server} = To, process_subscription(in, User, Server, JID, Type, Acc). --spec out_subscription( - binary(), binary(), jid(), - subscribed | unsubscribed | subscribe | unsubscribe) -> boolean(). -out_subscription(UserFrom, ServerFrom, JIDTo, - unsubscribed) -> - #jid{luser = UserTo, lserver = ServerTo} = JIDTo, - JIDFrom = jid:make(UserFrom, ServerFrom), - mod_roster:out_subscription(UserTo, ServerTo, JIDFrom, - unsubscribe), - mod_roster:in_subscription(false, UserFrom, ServerFrom, - JIDTo, unsubscribe, <<"">>), - process_subscription(out, UserFrom, ServerFrom, JIDTo, - unsubscribed, false); -out_subscription(User, Server, JID, Type) -> - process_subscription(out, User, Server, JID, Type, - false). +-spec out_subscription(presence()) -> boolean(). +out_subscription(#presence{from = From, to = To, type = unsubscribed} = Pres) -> + #jid{user = User, server = Server} = From, + mod_roster:out_subscription(Pres#presence{type = unsubscribe}), + mod_roster:in_subscription(false, xmpp:set_from_to( + Pres#presence{type = unsubscribe}, + To, From)), + process_subscription(out, User, Server, To, unsubscribed, false); +out_subscription(#presence{from = From, to = To, type = Type}) -> + #jid{user = User, server = Server} = From, + process_subscription(out, User, Server, To, Type, false). process_subscription(Direction, User, Server, JID, _Type, Acc) -> diff --git a/src/mod_shared_roster_ldap.erl b/src/mod_shared_roster_ldap.erl index 82db781e6..745430cad 100644 --- a/src/mod_shared_roster_ldap.erl +++ b/src/mod_shared_roster_ldap.erl @@ -40,8 +40,8 @@ handle_info/2, terminate/2, code_change/3]). -export([get_user_roster/2, - get_jid_info/4, process_item/2, in_subscription/6, - out_subscription/4, mod_opt_type/1, mod_options/1, + get_jid_info/4, process_item/2, in_subscription/2, + out_subscription/1, mod_opt_type/1, mod_options/1, opt_type/1, depends/2, transform_module_options/1]). -include("ejabberd.hrl"). @@ -159,9 +159,9 @@ process_item(RosterItem, _Host) -> _ -> RosterItem#roster{subscription = both, ask = none} end. --spec get_jid_info({subscription(), [binary()]}, binary(), binary(), jid()) - -> {subscription(), [binary()]}. -get_jid_info({Subscription, Groups}, User, Server, +-spec get_jid_info({subscription(), ask(), [binary()]}, binary(), binary(), jid()) + -> {subscription(), ask(), [binary()]}. +get_jid_info({Subscription, Ask, Groups}, User, Server, JID) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), @@ -174,23 +174,19 @@ get_jid_info({Subscription, Groups}, User, Server, NewGroups = if Groups == [] -> GroupNames; true -> Groups end, - {both, NewGroups}; - error -> {Subscription, Groups} + {both, none, NewGroups}; + error -> {Subscription, Ask, Groups} end. --spec in_subscription(boolean(), binary(), binary(), jid(), - subscribe | subscribed | unsubscribe | unsubscribed, - binary()) -> boolean(). -in_subscription(Acc, User, Server, JID, Type, - _Reason) -> +-spec in_subscription(boolean(), presence()) -> boolean(). +in_subscription(Acc, #presence{to = To, from = JID, type = Type}) -> + #jid{user = User, server = Server} = To, process_subscription(in, User, Server, JID, Type, Acc). --spec out_subscription( - binary(), binary(), jid(), - subscribed | unsubscribed | subscribe | unsubscribe) -> boolean(). -out_subscription(User, Server, JID, Type) -> - process_subscription(out, User, Server, JID, Type, - false). +-spec out_subscription(presence()) -> boolean(). +out_subscription(#presence{from = From, to = JID, type = Type}) -> + #jid{user = User, server = Server} = From, + process_subscription(out, User, Server, JID, Type, false). process_subscription(Direction, User, Server, JID, _Type, Acc) ->