diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl index f6d0e765d..6f6a196e5 100644 --- a/src/ejabberd_sm.erl +++ b/src/ejabberd_sm.erl @@ -442,135 +442,96 @@ online(Sessions) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec do_route(jid(), jid(), stanza() | broadcast()) -> any(). +do_route(From, #jid{lresource = <<"">>} = To, {broadcast, _} = Packet) -> + ?DEBUG("processing broadcast to bare JID: ~p", [Packet]), + lists:foreach( + fun(R) -> + do_route(From, jid:replace_resource(To, R), Packet) + end, get_user_resources(To#jid.user, To#jid.server)); do_route(From, To, {broadcast, _} = Packet) -> - case To#jid.lresource of - <<"">> -> - lists:foreach(fun(R) -> - do_route(From, - jid:replace_resource(To, R), - Packet) - end, - get_user_resources(To#jid.user, To#jid.server)); - _ -> - {U, S, R} = jid:tolower(To), - Mod = get_sm_backend(S), - case online(Mod:get_sessions(U, S, R)) of - [] -> - ?DEBUG("packet dropped~n", []); - Ss -> - Session = lists:max(Ss), - Pid = element(2, Session#session.sid), - ?DEBUG("sending to process ~p~n", [Pid]), - Pid ! {route, From, To, Packet} - end + ?DEBUG("processing broadcast to full JID: ~p", [Packet]), + {U, S, R} = jid:tolower(To), + Mod = get_sm_backend(S), + case online(Mod:get_sessions(U, S, R)) of + [] -> + ?DEBUG("dropping broadcast to unavailable resourse: ~p", [Packet]); + Ss -> + Session = lists:max(Ss), + Pid = element(2, Session#session.sid), + ?DEBUG("sending to process ~p: ~p", [Pid, Packet]), + Pid ! {route, From, To, Packet} end; -do_route(From, To, Packet) -> - ?DEBUG("session manager~n\tfrom ~p~n\tto ~p~n\tpacket " - "~P~n", - [From, To, Packet, 8]), +do_route(From, To, #presence{type = T, status = Status} = 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, lresource = LResource} = To, - Lang = xmpp:get_lang(Packet), - case LResource of - <<"">> -> - case Packet of - #presence{type = T, status = Status} -> - {Pass, _Subsc} = case T of - subscribe -> - Reason = xmpp:get_text(Status), - {is_privacy_allow(From, To, Packet) - andalso - ejabberd_hooks:run_fold(roster_in_subscription, - LServer, - false, - [User, Server, - From, - subscribe, - Reason]), - true}; - subscribed -> - {is_privacy_allow(From, To, Packet) - andalso - ejabberd_hooks:run_fold(roster_in_subscription, - LServer, - false, - [User, Server, - From, - subscribed, - <<"">>]), - true}; - unsubscribe -> - {is_privacy_allow(From, To, Packet) - andalso - ejabberd_hooks:run_fold(roster_in_subscription, - LServer, - false, - [User, Server, - From, - unsubscribe, - <<"">>]), - true}; - unsubscribed -> - {is_privacy_allow(From, To, Packet) - andalso - ejabberd_hooks:run_fold(roster_in_subscription, - LServer, - false, - [User, Server, - From, - unsubscribed, - <<"">>]), - true}; - _ -> {true, false} - end, - if Pass -> - PResources = get_user_present_resources(LUser, LServer), - lists:foreach(fun ({_, R}) -> - do_route(From, - jid:replace_resource(To, - R), - Packet) - end, - PResources); - true -> ok - end; - #message{type = T} when T == chat; T == headline; T == normal -> - route_message(From, To, Packet, T); - #message{type = groupchat} -> - ErrTxt = <<"User session not found">>, - Err = xmpp:make_error( - Packet, xmpp:err_service_unavailable(ErrTxt, Lang)), - ejabberd_router:route(To, From, Err); - #iq{} -> process_iq(From, To, Packet); - _ -> ok - end; - _ -> - Mod = get_sm_backend(LServer), - case online(Mod:get_sessions(LUser, LServer, LResource)) of - [] -> - case Packet of - #message{type = T} when T == chat; T == normal -> - route_message(From, To, Packet, T); - #message{type = groupchat} -> - ErrTxt = <<"User session not found">>, - Err = xmpp:make_error( - Packet, - xmpp:err_service_unavailable(ErrTxt, Lang)), - ejabberd_router:route(To, From, Err); - #iq{type = T} when T == get; T == set -> - ErrTxt = <<"User session not found">>, - Err = xmpp:make_error( - Packet, - xmpp:err_service_unavailable(ErrTxt, Lang)), - ejabberd_router:route(To, From, Err); - _ -> ?DEBUG("packet dropped~n", []) - end; - Ss -> - Session = lists:max(Ss), - Pid = element(2, Session#session.sid), - ?DEBUG("sending to process ~p~n", [Pid]), - Pid ! {route, From, To, Packet} - end + luser = LUser, lserver = LServer} = To, + Reason = if T == subscribe -> xmpp:get_text(Status); + true -> <<"">> + end, + case is_privacy_allow(From, To, Packet) andalso + ejabberd_hooks:run_fold( + roster_in_subscription, + LServer, false, + [User, Server, From, T, Reason]) of + true -> + Mod = get_sm_backend(LServer), + lists:foreach( + fun(#session{sid = SID, usr = {_, _, R}, + priority = Prio}) when is_integer(Prio) -> + Pid = element(2, SID), + ?DEBUG("sending to process ~p:~n~s", + [Pid, xmpp:pp(Packet)]), + Pid ! {route, From, jid:replace_resource(To, R), Packet}; + (_) -> + ok + end, online(Mod:get_sessions(LUser, LServer))); + false -> + ok + end; +do_route(From, #jid{lresource = <<"">>} = To, #presence{} = Packet) -> + ?DEBUG("processing presence to bare JID:~n~s", [xmpp:pp(Packet)]), + {LUser, LServer, _} = jid:tolower(To), + lists:foreach( + fun({_, R}) -> + do_route(From, jid:replace_resource(To, R), Packet) + end, get_user_present_resources(LUser, LServer)); +do_route(From, #jid{lresource = <<"">>} = To, #message{type = T} = Packet) -> + ?DEBUG("processing message to bare JID:~n~s", [xmpp:pp(Packet)]), + if T == chat; T == headline; T == normal -> + route_message(From, To, Packet, T); + true -> + Lang = xmpp:get_lang(Packet), + ErrTxt = <<"User session not found">>, + Err = xmpp:err_service_unavailable(ErrTxt, Lang), + ejabberd_router:route_error(To, From, Packet, Err) + end; +do_route(From, #jid{lresource = <<"">>} = To, #iq{} = Packet) -> + ?DEBUG("processing IQ to bare JID:~n~s", [xmpp:pp(Packet)]), + process_iq(From, To, Packet); +do_route(From, To, Packet) -> + ?DEBUG("processing packet to full JID:~n~s", [xmpp:pp(Packet)]), + {LUser, LServer, LResource} = jid:tolower(To), + Mod = get_sm_backend(LServer), + case online(Mod:get_sessions(LUser, LServer, LResource)) of + [] -> + case Packet of + #message{type = T} when T == chat; T == normal -> + route_message(From, To, Packet, T); + #presence{} -> + ?DEBUG("dropping presence to unavalable resource:~n~s", + [xmpp:pp(Packet)]); + _ -> + Lang = xmpp:get_lang(Packet), + ErrTxt = <<"User session not found">>, + Err = xmpp:err_service_unavailable(ErrTxt, Lang), + ejabberd_router:route_error(To, From, Packet, Err) + end; + Ss -> + Session = lists:max(Ss), + Pid = element(2, Session#session.sid), + ?DEBUG("sending to process ~p:~n~s", [Pid, xmpp:pp(Packet)]), + Pid ! {route, From, To, Packet} end. %% The default list applies to the user as a whole, diff --git a/src/mod_roster.erl b/src/mod_roster.erl index 423fe9e0e..fa27f866c 100644 --- a/src/mod_roster.erl +++ b/src/mod_roster.erl @@ -142,21 +142,54 @@ depends(_Host, _Opts) -> process_iq(#iq{from = #jid{luser = <<"">>}, to = #jid{resource = <<"">>}} = IQ) -> process_iq_manager(IQ); +process_iq(#iq{from = #jid{luser = U, lserver = S}, + to = #jid{luser = U, lserver = S}} = IQ) -> + process_local_iq(IQ); +process_iq(#iq{lang = Lang} = IQ) -> + Txt = <<"Query to another users is forbidden">>, + xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)). -process_iq(#iq{from = From, lang = Lang} = IQ) -> - #jid{lserver = LServer} = From, - case lists:member(LServer, ?MYHOSTS) of - true -> process_local_iq(IQ); - _ -> - Txt = <<"The query is only allowed from local users">>, - xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)) - end. - -process_local_iq(#iq{type = Type} = IQ) -> - case Type of - set -> try_process_iq_set(IQ); - get -> process_iq_get(IQ) - end. +process_local_iq(#iq{type = set,lang = Lang, + sub_els = [#roster_query{ + items = [#roster_item{ask = Ask}]}]} = IQ) + when Ask /= undefined -> + Txt = <<"Possessing 'ask' attribute is not allowed by RFC6121">>, + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); +process_local_iq(#iq{type = set, from = From, lang = Lang, + sub_els = [#roster_query{ + items = [#roster_item{} = Item]}]} = IQ) -> + case has_duplicated_groups(Item#roster_item.groups) of + true -> + Txt = <<"Duplicated groups are not allowed by RFC6121">>, + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); + false -> + #jid{server = Server} = From, + Access = gen_mod:get_module_opt(Server, ?MODULE, + access, fun(A) -> A end, all), + case acl:match_rule(Server, Access, From) of + deny -> + Txt = <<"Denied by ACL">>, + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); + allow -> + process_iq_set(IQ) + end + end; +process_local_iq(#iq{type = set, lang = Lang, + sub_els = [#roster_query{items = [_|_]}]} = IQ) -> + Txt = <<"Multiple elements are not allowed by RFC6121">>, + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); +process_local_iq(#iq{type = get, lang = Lang, + sub_els = [#roster_query{items = Items}]} = IQ) -> + case Items of + [] -> + process_iq_get(IQ); + [_|_] -> + Txt = <<"The query must not contain elements">>, + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)) + end; +process_local_iq(#iq{lang = Lang} = IQ) -> + Txt = <<"No module is handling this query">>, + xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). roster_hash(Items) -> p1_sha:sha(term_to_binary(lists:sort([R#roster{groups = @@ -315,11 +348,18 @@ encode_item(Item) -> end, groups = Item#roster.groups}. +decode_item(#roster_item{subscription = remove} = Item, R, _) -> + R#roster{jid = jid:tolower(Item#roster_item.jid), + name = <<"">>, + subscription = remove, + ask = none, + groups = [], + askmessage = <<"">>, + xs = []}; decode_item(Item, R, Managed) -> R#roster{jid = jid:tolower(Item#roster_item.jid), name = Item#roster_item.name, subscription = case Item#roster_item.subscription of - remove -> remove; Sub when Managed -> Sub; _ -> R#roster.subscription end, @@ -329,17 +369,6 @@ get_roster_by_jid_t(LUser, LServer, LJID) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:get_roster_by_jid(LUser, LServer, LJID). -try_process_iq_set(#iq{from = From, lang = Lang} = IQ) -> - #jid{server = Server} = From, - Access = gen_mod:get_module_opt(Server, ?MODULE, access, fun(A) -> A end, all), - case acl:match_rule(Server, Access, From) of - deny -> - Txt = <<"Denied by ACL">>, - xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); - allow -> - process_iq_set(IQ) - end. - process_iq_set(#iq{from = From, to = To, id = Id, sub_els = [#roster_query{items = QueryItems}]} = IQ) -> Managed = is_managed_from_id(Id), @@ -515,8 +544,7 @@ process_subscription(Direction, User, Server, JID1, {Subscription, Pending} -> NewItem = Item#roster{subscription = Subscription, ask = Pending, - askmessage = - iolist_to_binary(AskMessage)}, + askmessage = AskMessage}, roster_subscribe_t(LUser, LServer, LJID, NewItem), case roster_version_on_db(LServer) of true -> write_roster_version_t(LUser, LServer); @@ -730,10 +758,8 @@ del_roster_t(LUser, LServer, LJID) -> Mod:del_roster(LUser, LServer, LJID). process_item_set_t(LUser, LServer, #roster_item{jid = JID1} = QueryItem) -> - JID = {JID1#jid.user, JID1#jid.server, - JID1#jid.resource}, - LJID = {JID1#jid.luser, JID1#jid.lserver, - JID1#jid.lresource}, + JID = {JID1#jid.user, JID1#jid.server, <<>>}, + LJID = {JID1#jid.luser, JID1#jid.lserver, <<>>}, Item = #roster{usj = {LUser, LServer, LJID}, us = {LUser, LServer}, jid = JID}, Item2 = decode_item(QueryItem, Item, _Managed = true), @@ -1046,6 +1072,10 @@ is_managed_from_id(<<"roster-remotely-managed">>) -> is_managed_from_id(_Id) -> false. +has_duplicated_groups(Groups) -> + GroupsPrep = lists:usort([jid:resourceprep(G) || G <- Groups]), + not (length(GroupsPrep) == length(Groups)). + export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:export(LServer). diff --git a/test/ejabberd_SUITE.erl b/test/ejabberd_SUITE.erl index 59936352b..a13d801ea 100644 --- a/test/ejabberd_SUITE.erl +++ b/test/ejabberd_SUITE.erl @@ -394,7 +394,7 @@ db_tests(riak) -> auth_md5, presence_broadcast, last, - roster_get, + roster_tests:single_cases(), private, privacy_tests:single_cases(), vcard, @@ -402,9 +402,7 @@ db_tests(riak) -> test_unregister]}, muc_tests:master_slave_cases(), privacy_tests:master_slave_cases(), - {test_roster_subscribe, [parallel], - [roster_subscribe_master, - roster_subscribe_slave]}, + roster_tests:master_slave_cases(), {test_flex_offline, [sequence], [flex_offline_master, flex_offline_slave]}, {test_offline, [sequence], @@ -412,10 +410,7 @@ db_tests(riak) -> {test_announce, [sequence], [announce_master, announce_slave]}, {test_vcard_xupdate, [parallel], - [vcard_xupdate_master, vcard_xupdate_slave]}, - {test_roster_remove, [parallel], - [roster_remove_master, - roster_remove_slave]}]; + [vcard_xupdate_master, vcard_xupdate_slave]}]; db_tests(DB) when DB == mnesia; DB == redis -> [{single_user, [sequence], [test_register, @@ -424,8 +419,7 @@ db_tests(DB) when DB == mnesia; DB == redis -> auth_md5, presence_broadcast, last, - roster_get, - roster_ver, + roster_tests:single_cases(), private, privacy_tests:single_cases(), vcard, @@ -435,11 +429,9 @@ db_tests(DB) when DB == mnesia; DB == redis -> muc_tests:master_slave_cases(), privacy_tests:master_slave_cases(), pubsub_multiple_tests(), + roster_tests:master_slave_cases(), {test_mix, [parallel], [mix_master, mix_slave]}, - {test_roster_subscribe, [parallel], - [roster_subscribe_master, - roster_subscribe_slave]}, {test_flex_offline, [sequence], [flex_offline_master, flex_offline_slave]}, {test_offline, [sequence], @@ -457,10 +449,7 @@ db_tests(DB) when DB == mnesia; DB == redis -> {test_announce, [sequence], [announce_master, announce_slave]}, {test_vcard_xupdate, [parallel], - [vcard_xupdate_master, vcard_xupdate_slave]}, - {test_roster_remove, [parallel], - [roster_remove_master, - roster_remove_slave]}]; + [vcard_xupdate_master, vcard_xupdate_slave]}]; db_tests(_) -> %% No support for carboncopy [{single_user, [sequence], @@ -470,8 +459,7 @@ db_tests(_) -> auth_md5, presence_broadcast, last, - roster_get, - roster_ver, + roster_tests:single_cases(), private, privacy_tests:single_cases(), vcard, @@ -481,11 +469,9 @@ db_tests(_) -> muc_tests:master_slave_cases(), privacy_tests:master_slave_cases(), pubsub_multiple_tests(), + roster_tests:master_slave_cases(), {test_mix, [parallel], [mix_master, mix_slave]}, - {test_roster_subscribe, [parallel], - [roster_subscribe_master, - roster_subscribe_slave]}, {test_flex_offline, [sequence], [flex_offline_master, flex_offline_slave]}, {test_offline, [sequence], @@ -499,10 +485,7 @@ db_tests(_) -> {test_announce, [sequence], [announce_master, announce_slave]}, {test_vcard_xupdate, [parallel], - [vcard_xupdate_master, vcard_xupdate_slave]}, - {test_roster_remove, [parallel], - [roster_remove_master, - roster_remove_slave]}]. + [vcard_xupdate_master, vcard_xupdate_slave]}]. ldap_tests() -> [{ldap_tests, [sequence], @@ -862,33 +845,26 @@ test_bind(Config) -> test_open_session(Config) -> disconnect(open_session(Config, true)). -roster_get(Config) -> - #iq{type = result, sub_els = [#roster_query{items = []}]} = - send_recv(Config, #iq{type = get, sub_els = [#roster_query{}]}), - disconnect(Config). - -roster_ver(Config) -> - %% Get initial "ver" - #iq{type = result, sub_els = [#roster_query{ver = Ver1, items = []}]} = - send_recv(Config, #iq{type = get, - sub_els = [#roster_query{ver = <<"">>}]}), - %% Should receive empty IQ-result - #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = get, - sub_els = [#roster_query{ver = Ver1}]}), - %% Attempting to subscribe to server's JID - send(Config, #presence{type = subscribe, to = server_jid(Config)}), - %% Receive a single roster push with the new "ver" - #iq{type = set, sub_els = [#roster_query{ver = Ver2}]} = recv_iq(Config), - %% Requesting roster with the previous "ver". Should receive Ver2 again - #iq{type = result, sub_els = [#roster_query{ver = Ver2}]} = - send_recv(Config, #iq{type = get, - sub_els = [#roster_query{ver = Ver1}]}), - %% Now requesting roster with the newest "ver". Should receive empty IQ. - #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = get, - sub_els = [#roster_query{ver = Ver2}]}), - disconnect(Config). +roster_feature_enabled(Config) -> + roster_tests:feature_enabled(Config). +roster_iq_set_many_items(Config) -> + roster_tests:iq_set_many_items(Config). +roster_iq_set_duplicated_groups(Config) -> + roster_tests:iq_set_duplicated_groups(Config). +roster_iq_set_ask(Config) -> + roster_tests:iq_set_ask(Config). +roster_iq_get_item(Config) -> + roster_tests:iq_get_item(Config). +roster_iq_unexpected_element(Config) -> + roster_tests:iq_unexpected_element(Config). +roster_set_item(Config) -> + roster_tests:set_item(Config). +roster_version(Config) -> + roster_tests:version(Config). +roster_subscribe_master(Config) -> + roster_tests:subscribe_master(Config). +roster_subscribe_slave(Config) -> + roster_tests:subscribe_slave(Config). codec_failure(Config) -> JID = my_jid(Config), @@ -2043,148 +2019,6 @@ mix_slave(Config) -> disconnect = get_event(Config), disconnect(Config). -roster_subscribe_master(Config) -> - #presence{} = send_recv(Config, #presence{}), - wait_for_slave(Config), - Peer = ?config(peer, Config), - LPeer = jid:remove_resource(Peer), - send(Config, #presence{type = subscribe, to = LPeer}), - Push1 = #iq{type = set, - sub_els = [#roster_query{items = [#roster_item{ - ask = subscribe, - subscription = none, - jid = LPeer}]}]} = - recv_iq(Config), - send(Config, make_iq_result(Push1)), - #presence{type = subscribed, from = LPeer} = recv_presence(Config), - Push2 = #iq{type = set, - sub_els = [#roster_query{items = [#roster_item{ - subscription = to, - jid = LPeer}]}]} = - recv_iq(Config), - send(Config, make_iq_result(Push2)), - #presence{type = available, from = Peer} = recv_presence(Config), - %% BUG: ejabberd sends previous push again. Is it ok? - Push3 = #iq{type = set, - sub_els = [#roster_query{items = [#roster_item{ - subscription = to, - jid = LPeer}]}]} = - recv_iq(Config), - send(Config, make_iq_result(Push3)), - #presence{type = subscribe, from = LPeer} = recv_presence(Config), - send(Config, #presence{type = subscribed, to = LPeer}), - Push4 = #iq{type = set, - sub_els = [#roster_query{items = [#roster_item{ - subscription = both, - jid = LPeer}]}]} = - recv_iq(Config), - send(Config, make_iq_result(Push4)), - %% Move into a group - Groups = [<<"A">>, <<"B">>], - Item = #roster_item{jid = LPeer, groups = Groups}, - #iq{type = result, sub_els = []} = - send_recv(Config, - #iq{type = set, sub_els = [#roster_query{items = [Item]}]}), - Push5 = #iq{type = set, - sub_els = - [#roster_query{items = [#roster_item{ - jid = LPeer, - subscription = both}]}]} = - recv_iq(Config), - send(Config, make_iq_result(Push5)), - #iq{sub_els = [#roster_query{items = [#roster_item{groups = G1}]}]} = Push5, - Groups = lists:sort(G1), - wait_for_slave(Config), - #presence{type = unavailable, from = Peer} = recv_presence(Config), - disconnect(Config). - -roster_subscribe_slave(Config) -> - #presence{} = send_recv(Config, #presence{}), - wait_for_master(Config), - Peer = ?config(master, Config), - LPeer = jid:remove_resource(Peer), - #presence{type = subscribe, from = LPeer} = recv_presence(Config), - send(Config, #presence{type = subscribed, to = LPeer}), - Push1 = #iq{type = set, - sub_els = [#roster_query{items = [#roster_item{ - subscription = from, - jid = LPeer}]}]} = - recv_iq(Config), - send(Config, make_iq_result(Push1)), - send(Config, #presence{type = subscribe, to = LPeer}), - Push2 = #iq{type = set, - sub_els = [#roster_query{items = [#roster_item{ - ask = subscribe, - subscription = from, - jid = LPeer}]}]} = - recv_iq(Config), - send(Config, make_iq_result(Push2)), - #presence{type = subscribed, from = LPeer} = recv_presence(Config), - Push3 = #iq{type = set, - sub_els = [#roster_query{items = [#roster_item{ - subscription = both, - jid = LPeer}]}]} = - recv_iq(Config), - send(Config, make_iq_result(Push3)), - #presence{type = available, from = Peer} = recv_presence(Config), - wait_for_master(Config), - disconnect(Config). - -roster_remove_master(Config) -> - MyJID = my_jid(Config), - Peer = ?config(slave, Config), - LPeer = jid:remove_resource(Peer), - Groups = [<<"A">>, <<"B">>], - wait_for_slave(Config), - #presence{from = MyJID, type = available} = send_recv(Config, #presence{}), - #presence{from = Peer, type = available} = recv_presence(Config), - %% The peer removed us from its roster. - {Push1, Push2, _, _, _} = - ?recv5( - %% TODO: I guess this can be optimized, we don't need - %% to send transient roster push with subscription = 'to'. - #iq{type = set, - sub_els = - [#roster_query{items = [#roster_item{ - jid = LPeer, - subscription = to}]}]}, - #iq{type = set, - sub_els = - [#roster_query{items = [#roster_item{ - jid = LPeer, - subscription = none}]}]}, - #presence{type = unsubscribe, from = LPeer}, - #presence{type = unsubscribed, from = LPeer}, - #presence{type = unavailable, from = Peer}), - send(Config, make_iq_result(Push1)), - send(Config, make_iq_result(Push2)), - #iq{sub_els = [#roster_query{items = [#roster_item{groups = G1}]}]} = Push1, - #iq{sub_els = [#roster_query{items = [#roster_item{groups = G2}]}]} = Push2, - Groups = lists:sort(G1), Groups = lists:sort(G2), - disconnect(Config). - -roster_remove_slave(Config) -> - MyJID = my_jid(Config), - Peer = ?config(master, Config), - LPeer = jid:remove_resource(Peer), - #presence{from = MyJID, type = available} = send_recv(Config, #presence{}), - wait_for_master(Config), - #presence{from = Peer, type = available} = recv_presence(Config), - %% Remove the peer from roster. - Item = #roster_item{jid = LPeer, subscription = remove}, - #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = set, - sub_els = [#roster_query{items = [Item]}]}), - Push = #iq{type = set, - sub_els = - [#roster_query{items = [#roster_item{ - jid = LPeer, - subscription = remove}]}]} = - recv_iq(Config), - #presence{type = unavailable, from = Peer} = recv_presence(Config), - send(Config, make_iq_result(Push)), - disconnect(Config). - proxy65_master(Config) -> Proxy = proxy_jid(Config), MyJID = my_jid(Config), diff --git a/test/roster_tests.erl b/test/roster_tests.erl new file mode 100644 index 000000000..2d05709ab --- /dev/null +++ b/test/roster_tests.erl @@ -0,0 +1,527 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 22 Oct 2016 by Evgeny Khramtsov +%%%------------------------------------------------------------------- +-module(roster_tests). + +%% API +-compile(export_all). +-import(suite, [send_recv/2, recv_iq/1, send/2, disconnect/1, del_roster/1, + del_roster/2, make_iq_result/1, wait_for_slave/1, + wait_for_master/1, recv_presence/1, self_presence/2, + put_event/2, get_event/1, match_failure/2, get_roster/1, + is_feature_advertised/2]). +-include("suite.hrl"). +-include("mod_roster.hrl"). + +-record(state, {subscription = none :: none | from | to | both, + peer_available = false, + pending_in = false :: boolean(), + pending_out = false :: boolean()}). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_TestCase, Config) -> + Config. + +stop(_TestCase, Config) -> + Config. + +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {roster_single, [sequence], + [single_test(feature_enabled), + single_test(iq_set_many_items), + single_test(iq_set_duplicated_groups), + single_test(iq_get_item), + single_test(iq_unexpected_element), + single_test(iq_set_ask), + single_test(set_item), + single_test(version)]}. + +feature_enabled(Config) -> + ct:comment("Checking if roster versioning stream feature is set"), + true = ?config(rosterver, Config), + disconnect(Config). + +set_item(Config) -> + JID = jid:from_string(<<"nurse@example.com">>), + Item = #roster_item{jid = JID}, + {V1, Item} = set_items(Config, [Item]), + {V1, [Item]} = get_items(Config), + ItemWithGroups = Item#roster_item{groups = [<<"G1">>, <<"G2">>]}, + {V2, ItemWithGroups} = set_items(Config, [ItemWithGroups]), + {V2, [ItemWithGroups]} = get_items(Config), + {V3, Item} = set_items(Config, [Item]), + {V3, [Item]} = get_items(Config), + ItemWithName = Item#roster_item{name = <<"some name">>}, + {V4, ItemWithName} = set_items(Config, [ItemWithName]), + {V4, [ItemWithName]} = get_items(Config), + ItemRemoved = Item#roster_item{subscription = remove}, + {V5, ItemRemoved} = set_items(Config, [ItemRemoved]), + {V5, []} = get_items(Config), + del_roster(disconnect(Config), JID). + +iq_set_many_items(Config) -> + J1 = jid:from_string(<<"nurse1@example.com">>), + J2 = jid:from_string(<<"nurse2@example.com">>), + ct:comment("Trying to send roster-set with many elements"), + Items = [#roster_item{jid = J1}, #roster_item{jid = J2}], + #stanza_error{reason = 'bad-request'} = set_items(Config, Items), + disconnect(Config). + +iq_set_duplicated_groups(Config) -> + JID = jid:from_string(<<"nurse@example.com">>), + G = randoms:get_string(), + ct:comment("Trying to send roster-set with duplicated groups"), + Item = #roster_item{jid = JID, groups = [G, G]}, + #stanza_error{reason = 'bad-request'} = set_items(Config, [Item]), + disconnect(Config). + +iq_set_ask(Config) -> + JID = jid:from_string(<<"nurse@example.com">>), + ct:comment("Trying to send roster-set with 'ask' included"), + Item = #roster_item{jid = JID, ask = subscribe}, + #stanza_error{reason = 'bad-request'} = set_items(Config, [Item]), + disconnect(Config). + +iq_get_item(Config) -> + JID = jid:from_string(<<"nurse@example.com">>), + ct:comment("Trying to send roster-get with element"), + #iq{type = error} = Err3 = + send_recv(Config, #iq{type = get, + sub_els = [#roster_query{ + items = [#roster_item{jid = JID}]}]}), + #stanza_error{reason = 'bad-request'} = xmpp:get_error(Err3), + disconnect(Config). + +iq_unexpected_element(Config) -> + JID = jid:from_string(<<"nurse@example.com">>), + ct:comment("Trying to send IQs with unexpected element"), + lists:foreach( + fun(Type) -> + #iq{type = error} = Err4 = + send_recv(Config, #iq{type = Type, + sub_els = [#roster_item{jid = JID}]}), + #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err4) + end, [get, set]), + disconnect(Config). + +version(Config) -> + JID = jid:from_string(<<"nurse@example.com">>), + ct:comment("Requesting roster"), + {InitialVersion, _} = get_items(Config, <<"">>), + ct:comment("Requesting roster with initial version"), + {empty, []} = get_items(Config, InitialVersion), + ct:comment("Adding JID to the roster"), + {NewVersion, _} = set_items(Config, [#roster_item{jid = JID}]), + ct:comment("Requesting roster with initial version"), + {NewVersion, _} = get_items(Config, InitialVersion), + ct:comment("Requesting roster with new version"), + {empty, []} = get_items(Config, NewVersion), + del_roster(disconnect(Config), JID). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {roster_master_slave, [parallel], + [master_slave_test(subscribe)]}. + +subscribe_master(Config) -> + Actions = actions(), + process_subscriptions_master(Config, Actions), + del_roster(disconnect(Config)). + +subscribe_slave(Config) -> + process_subscriptions_slave(Config), + del_roster(disconnect(Config)). + +process_subscriptions_master(Config, Actions) -> + EnumeratedActions = lists:zip(lists:seq(1, length(Actions)), Actions), + self_presence(Config, available), + lists:foldl( + fun({N, {Dir, Type}}, State) -> + if Dir == out -> put_event(Config, {N, in, Type}); + Dir == in -> put_event(Config, {N, out, Type}) + end, + wait_for_slave(Config), + ct:pal("Performing ~s-~s (#~p) " + "in state:~n~s~nwith roster:~n~s", + [Dir, Type, N, pp(State), + pp(get_roster(Config))]), + transition(Config, Dir, Type, State) + end, #state{}, EnumeratedActions), + put_event(Config, done), + wait_for_slave(Config), + Config. + +process_subscriptions_slave(Config) -> + self_presence(Config, available), + process_subscriptions_slave(Config, get_event(Config), #state{}). + +process_subscriptions_slave(Config, done, _State) -> + wait_for_master(Config), + Config; +process_subscriptions_slave(Config, {N, Dir, Type}, State) -> + wait_for_master(Config), + ct:pal("Performing ~s-~s (#~p) " + "in state:~n~s~nwith roster:~n~s", + [Dir, Type, N, pp(State), pp(get_roster(Config))]), + NewState = transition(Config, Dir, Type, State), + process_subscriptions_slave(Config, get_event(Config), NewState). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("roster_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("roster_" ++ atom_to_list(T)), [parallel], + [list_to_atom("roster_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("roster_" ++ atom_to_list(T) ++ "_slave")]}. + +get_items(Config) -> + get_items(Config, <<"">>). + +get_items(Config, Version) -> + case send_recv(Config, #iq{type = get, + sub_els = [#roster_query{ver = Version}]}) of + #iq{type = result, + sub_els = [#roster_query{ver = NewVersion, items = Items}]} -> + {NewVersion, Items}; + #iq{type = result, sub_els = []} -> + {empty, []}; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +get_item(Config, JID) -> + case get_items(Config) of + {_Ver, Items} when is_list(Items) -> + lists:keyfind(JID, #roster_item.jid, Items); + _ -> + false + end. + +set_items(Config, Items) -> + case send_recv(Config, #iq{type = set, + sub_els = [#roster_query{items = Items}]}) of + #iq{type = result, sub_els = []} -> + recv_push(Config); + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +recv_push(Config) -> + ct:comment("Receiving roster push"), + Push = #iq{type = set, + sub_els = [#roster_query{ver = Ver, items = [PushItem]}]} + = recv_iq(Config), + send(Config, make_iq_result(Push)), + {Ver, PushItem}. + +recv_push(Config, Subscription, Ask) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + Match = #roster_item{jid = PeerBareJID, + subscription = Subscription, + ask = Ask, + groups = [], + name = <<"">>}, + ct:comment("Receiving roster push"), + Push = #iq{type = set, sub_els = [#roster_query{items = [Item]}]} = + recv_iq(Config), + case Item of + Match -> send(Config, make_iq_result(Push)); + _ -> match_failure(Item, Match) + end. + +recv_presence(Config, Type) -> + PeerJID = ?config(peer, Config), + case recv_presence(Config) of + #presence{from = PeerJID, type = Type} -> ok; + Pres -> match_failure(Pres, #presence{from = PeerJID, type = Type}) + end. + +recv_subscription(Config, Type) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + case recv_presence(Config) of + #presence{from = PeerBareJID, type = Type} -> ok; + Pres -> match_failure(Pres, #presence{from = PeerBareJID, type = Type}) + end. + +pp(Term) -> + io_lib_pretty:print(Term, fun pp/2). + +pp(state, N) -> + Fs = record_info(fields, state), + try N = length(Fs), Fs + catch _:_ -> no end; +pp(roster, N) -> + Fs = record_info(fields, roster), + try N = length(Fs), Fs + catch _:_ -> no end; +pp(_, _) -> no. + +%% RFC6121, A.2.1 +transition(Config, out, subscribe, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + send(Config, #presence{to = PeerBareJID, type = subscribe}), + case {Sub, Out, In} of + {none, false, _} -> + recv_push(Config, none, subscribe), + State#state{pending_out = true}; + {none, true, false} -> + %% BUG: we should not receive roster push here + recv_push(Config, none, subscribe), + State; + {from, false, false} -> + recv_push(Config, from, subscribe), + State#state{pending_out = true}; + _ -> + State + end; +%% RFC6121, A.2.2 +transition(Config, out, unsubscribe, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + send(Config, #presence{to = PeerBareJID, type = unsubscribe}), + case {Sub, Out, In} of + {none, true, _} -> + recv_push(Config, none, undefined), + State#state{pending_out = false}; + {to, false, _} -> + recv_push(Config, none, undefined), + recv_presence(Config, unavailable), + State#state{subscription = none, peer_available = false}; + {from, true, false} -> + recv_push(Config, from, undefined), + State#state{pending_out = false}; + {both, false, false} -> + recv_push(Config, from, undefined), + recv_presence(Config, unavailable), + State#state{subscription = from, peer_available = false}; + _ -> + State + end; +%% RFC6121, A.2.3 +transition(Config, out, subscribed, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + send(Config, #presence{to = PeerBareJID, type = subscribed}), + case {Sub, Out, In} of + {none, false, true} -> + recv_push(Config, from, undefined), + State#state{subscription = from, pending_in = false}; + {none, true, true} -> + recv_push(Config, from, subscribe), + State#state{subscription = from, pending_in = false}; + {to, false, true} -> + recv_push(Config, both, undefined), + State#state{subscription = both, pending_in = false}; + {to, false, _} -> + %% BUG: we should not transition to 'both' state + recv_push(Config, both, undefined), + State#state{subscription = both}; + _ -> + State + end; +%% RFC6121, A.2.4 +transition(Config, out, unsubscribed, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + send(Config, #presence{to = PeerBareJID, type = unsubscribed}), + case {Sub, Out, In} of + {none, false, true} -> + State#state{subscription = none, pending_in = false}; + {none, true, true} -> + recv_push(Config, none, subscribe), + State#state{subscription = none, pending_in = false}; + {to, _, true} -> + State#state{pending_in = false}; + {from, false, _} -> + recv_push(Config, none, undefined), + State#state{subscription = none}; + {from, true, _} -> + recv_push(Config, none, subscribe), + State#state{subscription = none}; + {both, _, _} -> + recv_push(Config, to, undefined), + State#state{subscription = to}; + _ -> + State + end; +%% RFC6121, A.3.1 +transition(Config, in, subscribe = Type, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + case {Sub, Out, In} of + {none, false, false} -> + recv_subscription(Config, Type), + State#state{pending_in = true}; + {none, true, false} -> + recv_push(Config, none, subscribe), + recv_subscription(Config, Type), + State#state{pending_in = true}; + {to, false, false} -> + %% BUG: we should not receive roster push in this state! + recv_push(Config, to, undefined), + recv_subscription(Config, Type), + State#state{pending_in = true}; + _ -> + State + end; +%% RFC6121, A.3.2 +transition(Config, in, unsubscribe = Type, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + case {Sub, Out, In} of + {none, _, true} -> + State#state{pending_in = false}; + {to, _, true} -> + recv_push(Config, to, undefined), + recv_subscription(Config, Type), + State#state{pending_in = false}; + {from, false, _} -> + recv_push(Config, none, undefined), + recv_subscription(Config, Type), + State#state{subscription = none}; + {from, true, _} -> + recv_push(Config, none, subscribe), + recv_subscription(Config, Type), + State#state{subscription = none}; + {both, _, _} -> + recv_push(Config, to, undefined), + recv_subscription(Config, Type), + State#state{subscription = to}; + _ -> + State + end; +%% RFC6121, A.3.3 +transition(Config, in, subscribed = Type, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + case {Sub, Out, In} of + {none, true, _} -> + recv_push(Config, to, undefined), + recv_subscription(Config, Type), + recv_presence(Config, available), + State#state{subscription = to, pending_out = false, peer_available = true}; + {from, true, _} -> + recv_push(Config, both, undefined), + recv_subscription(Config, Type), + recv_presence(Config, available), + State#state{subscription = both, pending_out = false, peer_available = true}; + {from, false, _} -> + %% BUG: we should not transition to 'both' in this state + recv_push(Config, both, undefined), + recv_subscription(Config, Type), + recv_presence(Config, available), + State#state{subscription = both, pending_out = false, peer_available = true}; + _ -> + State + end; +%% RFC6121, A.3.4 +transition(Config, in, unsubscribed = Type, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + case {Sub, Out, In} of + {none, true, true} -> + %% BUG: we should receive roster push in this state! + recv_subscription(Config, Type), + State#state{subscription = none, pending_out = false}; + {none, true, false} -> + recv_push(Config, none, undefined), + recv_subscription(Config, Type), + State#state{subscription = none, pending_out = false}; + {none, false, false} -> + State; + {to, false, _} -> + recv_push(Config, none, undefined), + recv_subscription(Config, Type), + recv_presence(Config, unavailable), + State#state{subscription = none, peer_available = false}; + {from, true, false} -> + recv_push(Config, from, undefined), + recv_subscription(Config, Type), + State#state{subscription = from, pending_out = false}; + {both, _, _} -> + recv_push(Config, from, undefined), + recv_subscription(Config, Type), + recv_presence(Config, unavailable), + State#state{subscription = from, peer_available = false}; + _ -> + State + end; +%% Outgoing roster remove +transition(Config, out, remove, + #state{subscription = Sub, pending_in = In, pending_out = Out}) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + Item = #roster_item{jid = PeerBareJID, subscription = remove}, + #iq{type = result, sub_els = []} = + send_recv(Config, #iq{type = set, + sub_els = [#roster_query{items = [Item]}]}), + recv_push(Config, remove, undefined), + case {Sub, Out, In} of + {to, _, _} -> + recv_presence(Config, unavailable); + {both, _, _} -> + recv_presence(Config, unavailable); + _ -> + ok + end, + #state{}; +%% Incoming roster remove +transition(Config, in, remove, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + case {Sub, Out, In} of + {none, true, _} -> + ok; + {from, false, _} -> + recv_push(Config, none, undefined), + recv_subscription(Config, unsubscribe); + {from, true, _} -> + recv_push(Config, none, subscribe), + recv_subscription(Config, unsubscribe); + {to, false, _} -> + %% BUG: we should receive push here + %% recv_push(Config, none, undefined), + recv_presence(Config, unavailable), + recv_subscription(Config, unsubscribed); + {both, _, _} -> + recv_presence(Config, unavailable), + recv_push(Config, to, undefined), + recv_subscription(Config, unsubscribe), + recv_push(Config, none, undefined), + recv_subscription(Config, unsubscribed); + _ -> + ok + end, + State#state{subscription = none}. + +actions() -> + States = [{Dir, Type} || Dir <- [out, in], + Type <- [subscribe, subscribed, + unsubscribe, unsubscribed, + remove]], + Actions = lists:flatten([[X, Y] || X <- States, Y <- States]), + remove_dups(Actions, []). + +remove_dups([X|T], [X,X|_] = Acc) -> + remove_dups(T, Acc); +remove_dups([X|T], Acc) -> + remove_dups(T, [X|Acc]); +remove_dups([], Acc) -> + lists:reverse(Acc). diff --git a/test/suite.erl b/test/suite.erl index 3bed62562..f88ac5a5e 100644 --- a/test/suite.erl +++ b/test/suite.erl @@ -86,6 +86,7 @@ init_config(Config) -> {stream_from, <<"">>}, {db_xmlns, <<"">>}, {mechs, []}, + {rosterver, false}, {lang, <<"en">>}, {base_dir, BaseDir}, {socket, undefined}, @@ -421,6 +422,8 @@ wait_auth_SASL_result(Config, ShouldFail) -> set_opt(sm, true, ConfigAcc); (#feature_csi{}, ConfigAcc) -> set_opt(csi, true, ConfigAcc); + (#rosterver_feature{}, ConfigAcc) -> + set_opt(rosterver, true, ConfigAcc); (_, ConfigAcc) -> ConfigAcc end, Config, Fs); @@ -674,26 +677,32 @@ set_opt(Opt, Val, Config) -> [{Opt, Val}|lists:keydelete(Opt, 1, Config)]. wait_for_master(Config) -> - put_event(Config, slave_ready), + put_event(Config, peer_ready), case get_event(Config) of - master_ready -> + peer_ready -> ok; Other -> - suite:match_failure([Other], [master_ready]) + suite:match_failure(Other, peer_ready) end. wait_for_slave(Config) -> - put_event(Config, master_ready), + put_event(Config, peer_ready), case get_event(Config) of - slave_ready -> + peer_ready -> ok; Other -> - suite:match_failure([Other], [slave_ready]) + suite:match_failure(Other, peer_ready) end. make_iq_result(#iq{from = From} = IQ) -> IQ#iq{type = result, to = From, from = undefined, sub_els = []}. +self_presence(Config, Type) -> + MyJID = my_jid(Config), + ct:comment("Sending self-presence"), + #presence{type = Type, from = MyJID} = + send_recv(Config, #presence{type = Type}). + set_roster(Config, Subscription, Groups) -> MyJID = my_jid(Config), {U, S, _} = jid:tolower(MyJID), @@ -710,15 +719,21 @@ set_roster(Config, Subscription, Groups) -> Config. del_roster(Config) -> + del_roster(Config, ?config(peer, Config)). + +del_roster(Config, PeerJID) -> MyJID = my_jid(Config), {U, S, _} = jid:tolower(MyJID), - PeerJID = ?config(peer, Config), PeerBareJID = jid:remove_resource(PeerJID), PeerLJID = jid:tolower(PeerBareJID), ct:comment("Removing ~s from roster", [jid:to_string(PeerBareJID)]), {atomic, _} = mod_roster:del_roster(U, S, PeerLJID), Config. +get_roster(Config) -> + {LUser, LServer, _} = jid:tolower(my_jid(Config)), + mod_roster:get_roster(LUser, LServer). + receiver(NS, Owner) -> MRef = erlang:monitor(process, Owner), receiver(NS, Owner, MRef).