diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 11c10d861..d97b04559 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -40,6 +40,11 @@ send_element/2, socket_type/0, get_presence/1, + get_aux_field/2, + set_aux_field/3, + del_aux_field/2, + get_subscription/2, + broadcast/4, get_subscribed/1]). %% gen_fsm callbacks @@ -96,6 +101,7 @@ conn = unknown, auth_module = unknown, ip, + aux_fields = [], lang}). %-define(DBGFSM, true). @@ -154,6 +160,39 @@ socket_type() -> get_presence(FsmRef) -> ?GEN_FSM:sync_send_all_state_event(FsmRef, {get_presence}, 1000). +get_aux_field(Key, #state{aux_fields = Opts}) -> + case lists:keysearch(Key, 1, Opts) of + {value, {_, Val}} -> + {ok, Val}; + _ -> + error + end. + +set_aux_field(Key, Val, #state{aux_fields = Opts} = State) -> + Opts1 = lists:keydelete(Key, 1, Opts), + State#state{aux_fields = [{Key, Val}|Opts1]}. + +del_aux_field(Key, #state{aux_fields = Opts} = State) -> + Opts1 = lists:keydelete(Key, 1, Opts), + State#state{aux_fields = Opts1}. + +get_subscription(From = #jid{}, StateData) -> + get_subscription(jlib:jid_tolower(From), StateData); +get_subscription(LFrom, StateData) -> + LBFrom = setelement(3, LFrom, ""), + F = ?SETS:is_element(LFrom, StateData#state.pres_f) orelse + ?SETS:is_element(LBFrom, StateData#state.pres_f), + T = ?SETS:is_element(LFrom, StateData#state.pres_t) orelse + ?SETS:is_element(LBFrom, StateData#state.pres_t), + if F and T -> both; + F -> from; + T -> to; + true -> none + end. + +broadcast(FsmRef, Type, From, Packet) -> + FsmRef ! {broadcast, Type, From, Packet}. + stop(FsmRef) -> ?GEN_FSM:send_event(FsmRef, closed). @@ -1114,35 +1153,39 @@ handle_info({route, From, To, Packet}, StateName, StateData) -> {Pass, NewAttrs, NewState} = case Name of "presence" -> + State = ejabberd_hooks:run_fold( + c2s_presence_in, StateData#state.server, + StateData, + [{From, To, Packet}]), case xml:get_attr_s("type", Attrs) of "probe" -> LFrom = jlib:jid_tolower(From), LBFrom = jlib:jid_remove_resource(LFrom), NewStateData = case ?SETS:is_element( - LFrom, StateData#state.pres_a) orelse + LFrom, State#state.pres_a) orelse ?SETS:is_element( - LBFrom, StateData#state.pres_a) of + LBFrom, State#state.pres_a) of true -> - StateData; + State; false -> case ?SETS:is_element( - LFrom, StateData#state.pres_f) of + LFrom, State#state.pres_f) of true -> A = ?SETS:add_element( LFrom, - StateData#state.pres_a), - StateData#state{pres_a = A}; + State#state.pres_a), + State#state{pres_a = A}; false -> case ?SETS:is_element( - LBFrom, StateData#state.pres_f) of + LBFrom, State#state.pres_f) of true -> A = ?SETS:add_element( LBFrom, - StateData#state.pres_a), - StateData#state{pres_a = A}; + State#state.pres_a), + State#state{pres_a = A}; false -> - StateData + State end end end, @@ -1150,59 +1193,59 @@ handle_info({route, From, To, Packet}, StateName, StateData) -> {false, Attrs, NewStateData}; "error" -> NewA = remove_element(jlib:jid_tolower(From), - StateData#state.pres_a), - {true, Attrs, StateData#state{pres_a = NewA}}; + State#state.pres_a), + {true, Attrs, State#state{pres_a = NewA}}; "invisible" -> Attrs1 = lists:keydelete("type", 1, Attrs), - {true, [{"type", "unavailable"} | Attrs1], StateData}; + {true, [{"type", "unavailable"} | Attrs1], State}; "subscribe" -> - SRes = is_privacy_allow(StateData, From, To, Packet, in), - {SRes, Attrs, StateData}; + SRes = is_privacy_allow(State, From, To, Packet, in), + {SRes, Attrs, State}; "subscribed" -> - SRes = is_privacy_allow(StateData, From, To, Packet, in), - {SRes, Attrs, StateData}; + SRes = is_privacy_allow(State, From, To, Packet, in), + {SRes, Attrs, State}; "unsubscribe" -> - SRes = is_privacy_allow(StateData, From, To, Packet, in), - {SRes, Attrs, StateData}; + SRes = is_privacy_allow(State, From, To, Packet, in), + {SRes, Attrs, State}; "unsubscribed" -> - SRes = is_privacy_allow(StateData, From, To, Packet, in), - {SRes, Attrs, StateData}; + SRes = is_privacy_allow(State, From, To, Packet, in), + {SRes, Attrs, State}; _ -> - case privacy_check_packet(StateData, From, To, Packet, in) of + case privacy_check_packet(State, From, To, Packet, in) of allow -> LFrom = jlib:jid_tolower(From), LBFrom = jlib:jid_remove_resource(LFrom), case ?SETS:is_element( - LFrom, StateData#state.pres_a) orelse + LFrom, State#state.pres_a) orelse ?SETS:is_element( - LBFrom, StateData#state.pres_a) of + LBFrom, State#state.pres_a) of true -> - {true, Attrs, StateData}; + {true, Attrs, State}; false -> case ?SETS:is_element( - LFrom, StateData#state.pres_f) of + LFrom, State#state.pres_f) of true -> A = ?SETS:add_element( LFrom, - StateData#state.pres_a), + State#state.pres_a), {true, Attrs, - StateData#state{pres_a = A}}; + State#state{pres_a = A}}; false -> case ?SETS:is_element( - LBFrom, StateData#state.pres_f) of + LBFrom, State#state.pres_f) of true -> A = ?SETS:add_element( LBFrom, - StateData#state.pres_a), + State#state.pres_a), {true, Attrs, - StateData#state{pres_a = A}}; + State#state{pres_a = A}}; false -> - {true, Attrs, StateData} + {true, Attrs, State} end end end; deny -> - {false, Attrs, StateData} + {false, Attrs, State} end end; "broadcast" -> @@ -1358,6 +1401,17 @@ handle_info({force_update_presence, LUser}, StateName, StateData end, {next_state, StateName, NewStateData}; +handle_info({broadcast, Type, From, Packet}, StateName, StateData) -> + Recipients = ejabberd_hooks:run_fold( + c2s_broadcast_recipients, StateData#state.server, + [], + [StateData, Type, From, Packet]), + lists:foreach( + fun(USR) -> + ejabberd_router:route( + From, jlib:make_jid(USR), Packet) + end, lists:usort(Recipients)), + fsm_next_state(StateName, StateData); handle_info(Info, StateName, StateData) -> ?ERROR_MSG("Unexpected info: ~p", [Info]), fsm_next_state(StateName, StateData). diff --git a/src/mod_caps.erl b/src/mod_caps.erl index 319e37e6c..15e6b5e7f 100644 --- a/src/mod_caps.erl +++ b/src/mod_caps.erl @@ -53,12 +53,16 @@ ]). %% hook handlers --export([user_send_packet/3]). +-export([user_send_packet/3, + user_receive_packet/4, + c2s_presence_in/2, + c2s_broadcast_recipients/5]). -include("ejabberd.hrl"). -include("jlib.hrl"). -define(PROCNAME, ejabberd_mod_caps). +-define(BAD_HASH_LIFETIME, 600). %% in seconds -record(caps, {node, version, hash, exts}). -record(caps_features, {node_pair, features = []}). @@ -101,10 +105,10 @@ get_features(#caps{node = Node, version = Version, exts = Exts}) -> BinaryNode = node_to_binary(Node, SubNode), case cache_tab:lookup(caps_features, BinaryNode, caps_read_fun(BinaryNode)) of - error -> - Acc; - {ok, Features} -> - binary_to_features(Features) ++ Acc + {ok, Features} when is_list(Features) -> + binary_to_features(Features) ++ Acc; + _ -> + Acc end end, [], SubNodes). @@ -159,6 +163,22 @@ user_send_packet(#jid{luser = User, lserver = Server} = From, user_send_packet(_From, _To, _Packet) -> ok. +user_receive_packet(#jid{lserver = Server}, From, _To, + {xmlelement, "presence", Attrs, Els}) -> + Type = xml:get_attr_s("type", Attrs), + if Type == ""; Type == "available" -> + case read_caps(Els) of + nothing -> + ok; + #caps{version = Version, exts = Exts} = Caps -> + feature_request(Server, From, Caps, [Version | Exts]) + end; + true -> + ok + end; +user_receive_packet(_JID, _From, _To, _Packet) -> + ok. + caps_stream_features(Acc, MyHost) -> case make_my_disco_hash(MyHost) of "" -> @@ -194,6 +214,67 @@ disco_info(_Acc, Host, Module, ?EJABBERD_URI ++ "#" ++ [_|_], Lang) -> disco_info(Acc, _Host, _Module, _Node, _Lang) -> Acc. +c2s_presence_in(C2SState, {From, To, {_, _, Attrs, Els}}) -> + Type = xml:get_attr_s("type", Attrs), + Subscription = ejabberd_c2s:get_subscription(From, C2SState), + Insert = ((Type == "") or (Type == "available")) + and ((Subscription == both) or (Subscription == to)), + Delete = (Type == "unavailable") or (Type == "error") or (Type == "invisible"), + if Insert or Delete -> + LFrom = jlib:jid_tolower(From), + Rs = case ejabberd_c2s:get_aux_field(caps_resources, C2SState) of + {ok, Rs1} -> + Rs1; + error -> + gb_trees:empty() + end, + Caps = read_caps(Els), + {CapsUpdated, NewRs} = + case Caps of + nothing when Insert == true -> + {false, Rs}; + _ when Insert == true -> + case gb_trees:lookup(LFrom, Rs) of + {value, Caps} -> + {false, Rs}; + none -> + {true, gb_trees:insert(LFrom, Caps, Rs)}; + _ -> + {true, gb_trees:update(LFrom, Caps, Rs)} + end; + _ -> + {false, gb_trees:delete_any(LFrom, Rs)} + end, + if CapsUpdated -> + ejabberd_hooks:run(caps_update, To#jid.lserver, + [From, To, get_features(Caps)]); + true -> + ok + end, + ejabberd_c2s:set_aux_field(caps_resources, NewRs, C2SState); + true -> + C2SState + end. + +c2s_broadcast_recipients(InAcc, C2SState, {pep_message, Feature}, + _From, _Packet) -> + case ejabberd_c2s:get_aux_field(caps_resources, C2SState) of + {ok, Rs} -> + gb_trees_fold( + fun(USR, Caps, Acc) -> + case lists:member(Feature, get_features(Caps)) of + true -> + [USR|Acc]; + false -> + Acc + end + end, InAcc, Rs); + _ -> + InAcc + end; +c2s_broadcast_recipients(Acc, _, _, _, _) -> + Acc. + %%==================================================================== %% gen_server callbacks %%==================================================================== @@ -214,8 +295,14 @@ init([Host, Opts]) -> MaxSize = gen_mod:get_opt(cache_size, Opts, 1000), LifeTime = gen_mod:get_opt(cache_life_time, Opts, timer:hours(24) div 1000), cache_tab:new(caps_features, [{max_size, MaxSize}, {life_time, LifeTime}]), + ejabberd_hooks:add(c2s_presence_in, Host, + ?MODULE, c2s_presence_in, 75), + ejabberd_hooks:add(c2s_broadcast_recipients, Host, + ?MODULE, c2s_broadcast_recipients, 75), ejabberd_hooks:add(user_send_packet, Host, ?MODULE, user_send_packet, 75), + ejabberd_hooks:add(user_receive_packet, Host, + ?MODULE, user_receive_packet, 75), ejabberd_hooks:add(c2s_stream_features, Host, ?MODULE, caps_stream_features, 75), ejabberd_hooks:add(s2s_stream_features, Host, @@ -241,8 +328,14 @@ handle_info(_Info, State) -> terminate(_Reason, State) -> Host = State#state.host, + ejabberd_hooks:delete(c2s_presence_in, Host, + ?MODULE, c2s_presence_in, 75), + ejabberd_hooks:delete(c2s_broadcast_recipients, Host, + ?MODULE, c2s_broadcast_recipients, 75), ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, user_send_packet, 75), + ejabberd_hooks:delete(user_receive_packet, Host, + ?MODULE, user_receive_packet, 75), ejabberd_hooks:delete(c2s_stream_features, Host, ?MODULE, caps_stream_features, 75), ejabberd_hooks:delete(s2s_stream_features, Host, @@ -266,21 +359,31 @@ feature_request(Host, From, Caps, [SubNode | Tail] = SubNodes) -> BinaryNode = node_to_binary(Node, SubNode), case cache_tab:lookup(caps_features, BinaryNode, caps_read_fun(BinaryNode)) of - error -> - IQ = #iq{type = get, - xmlns = ?NS_DISCO_INFO, - sub_el = [{xmlelement, "query", - [{"xmlns", ?NS_DISCO_INFO}, - {"node", Node ++ "#" ++ SubNode}], - []}]}, - F = fun(IQReply) -> - feature_response( - IQReply, Host, From, Caps, SubNodes) - end, - ejabberd_local:route_iq( - jlib:make_jid("", Host, ""), From, IQ, F); - _ -> - feature_request(Host, From, Caps, Tail) + {ok, Fs} when is_list(Fs) -> + feature_request(Host, From, Caps, Tail); + Other -> + NeedRequest = case Other of + {ok, TS} -> + now_ts() >= TS + ?BAD_HASH_LIFETIME; + _ -> + true + end, + if NeedRequest -> + IQ = #iq{type = get, + xmlns = ?NS_DISCO_INFO, + sub_el = [{xmlelement, "query", + [{"xmlns", ?NS_DISCO_INFO}, + {"node", Node ++ "#" ++ SubNode}], + []}]}, + F = fun(IQReply) -> + feature_response( + IQReply, Host, From, Caps, SubNodes) + end, + ejabberd_local:route_iq( + jlib:make_jid("", Host, ""), From, IQ, F); + true -> + feature_request(Host, From, Caps, Tail) + end end; feature_request(_Host, _From, _Caps, []) -> ok. @@ -302,18 +405,19 @@ feature_response(#iq{type = result, caps_features, BinaryNode, BinaryFeatures, caps_write_fun(BinaryNode, BinaryFeatures)); false -> - cache_tab:insert(caps_features, BinaryNode, [], - caps_write_fun(BinaryNode, [])) + %% We cache current timestamp and will probe the client + %% after BAD_HASH_LIFETIME seconds. + cache_tab:insert(caps_features, BinaryNode, now_ts(), + caps_write_fun(BinaryNode, now_ts())) end, feature_request(Host, From, Caps, SubNodes); -feature_response(timeout, _Host, _From, _Caps, _SubNodes) -> - ok; feature_response(_IQResult, Host, From, Caps, [SubNode | SubNodes]) -> - %% We got type=error or invalid type=result stanza, so - %% we cache empty feature not to probe the client permanently + %% We got type=error or invalid type=result stanza or timeout, + %% so we cache current timestamp and will probe the client + %% after BAD_HASH_LIFETIME seconds. BinaryNode = node_to_binary(Caps#caps.node, SubNode), - cache_tab:insert(caps_features, BinaryNode, [], - caps_write_fun(BinaryNode, [])), + cache_tab:insert(caps_features, BinaryNode, now_ts(), + caps_write_fun(BinaryNode, now_ts())), feature_request(Host, From, Caps, SubNodes). node_to_binary(Node, SubNode) -> @@ -506,3 +610,20 @@ concat_xdata_fields(Fields) -> Acc end, ["", []], Fields), [Form, $<, lists:sort(Res)]. + +gb_trees_fold(F, Acc, Tree) -> + Iter = gb_trees:iterator(Tree), + gb_trees_fold_iter(F, Acc, Iter). + +gb_trees_fold_iter(F, Acc, Iter) -> + case gb_trees:next(Iter) of + {Key, Val, NewIter} -> + NewAcc = F(Key, Val, Acc), + gb_trees_fold_iter(F, NewAcc, NewIter); + _ -> + Acc + end. + +now_ts() -> + {MegaSecs, Secs, _} = now(), + MegaSecs*1000000 + Secs.