]> granicus.if.org Git - ejabberd/commitdiff
Let a MUC room to route presences from its bare JID
authorEvgeniy Khramtsov <ekhramtsov@process-one.net>
Mon, 12 Feb 2018 14:37:36 +0000 (17:37 +0300)
committerEvgeniy Khramtsov <ekhramtsov@process-one.net>
Mon, 12 Feb 2018 14:37:36 +0000 (17:37 +0300)
The goal for this is to provide entity capabilities (XEP-0115) and
vCard-based avatar hash (XEP-0153)

include/mod_muc_room.hrl
src/mod_caps.erl
src/mod_muc_room.erl
src/mod_vcard_xupdate.erl
test/muc_tests.erl

index aea5432e68b83d977eb18b35788cef9b7b823334..b224192c8f7736dfabbcef128ff903eed0a7100d 100644 (file)
@@ -63,6 +63,7 @@
     max_users                            = ?MAX_USERS_DEFAULT :: non_neg_integer() | none,
     logging                              = false :: boolean(),
     vcard                                = <<"">> :: binary(),
+    vcard_xupdate                        = undefined :: undefined | external | binary(),
     captcha_whitelist                    = (?SETS):empty() :: ?TGB_SET,
     mam                                  = false :: boolean(),
     pubsub                               = <<"">> :: binary()
index 995e8b487e4b6298cad87ecb74b4a3096dd14d9a..19838cc8a09158420c3568205cee3c9d77f574b4 100644 (file)
@@ -38,7 +38,8 @@
 -export([read_caps/1, list_features/1, caps_stream_features/2,
         disco_features/5, disco_identity/5, disco_info/5,
         get_features/2, export/1, import_info/0, import/5,
-         get_user_caps/2, import_start/2, import_stop/2]).
+         get_user_caps/2, import_start/2, import_stop/2,
+        compute_disco_hash/2, is_valid_node/1]).
 
 %% gen_mod callbacks
 -export([start/2, stop/1, reload/3, depends/2]).
@@ -412,13 +413,13 @@ make_my_disco_hash(Host) ->
          DiscoInfo = #disco_info{identities = Identities,
                                  features = Feats,
                                  xdata = Info},
-         make_disco_hash(DiscoInfo, sha);
+         compute_disco_hash(DiscoInfo, sha);
       _Err -> <<"">>
     end.
 
 -type digest_type() :: md5 | sha | sha224 | sha256 | sha384 | sha512.
--spec make_disco_hash(disco_info(), digest_type()) -> binary().
-make_disco_hash(DiscoInfo, Algo) ->
+-spec compute_disco_hash(disco_info(), digest_type()) -> binary().
+compute_disco_hash(DiscoInfo, Algo) ->
     Concat = list_to_binary([concat_identities(DiscoInfo),
                              concat_features(DiscoInfo), concat_info(DiscoInfo)]),
     base64:encode(case Algo of
@@ -434,17 +435,17 @@ make_disco_hash(DiscoInfo, Algo) ->
 check_hash(Caps, DiscoInfo) ->
     case Caps#caps.hash of
       <<"md5">> ->
-         Caps#caps.version == make_disco_hash(DiscoInfo, md5);
+         Caps#caps.version == compute_disco_hash(DiscoInfo, md5);
       <<"sha-1">> ->
-         Caps#caps.version == make_disco_hash(DiscoInfo, sha);
+         Caps#caps.version == compute_disco_hash(DiscoInfo, sha);
       <<"sha-224">> ->
-         Caps#caps.version == make_disco_hash(DiscoInfo, sha224);
+         Caps#caps.version == compute_disco_hash(DiscoInfo, sha224);
       <<"sha-256">> ->
-         Caps#caps.version == make_disco_hash(DiscoInfo, sha256);
+         Caps#caps.version == compute_disco_hash(DiscoInfo, sha256);
       <<"sha-384">> ->
-         Caps#caps.version == make_disco_hash(DiscoInfo, sha384);
+         Caps#caps.version == compute_disco_hash(DiscoInfo, sha384);
       <<"sha-512">> ->
-         Caps#caps.version == make_disco_hash(DiscoInfo, sha512);
+         Caps#caps.version == compute_disco_hash(DiscoInfo, sha512);
       _ -> true
     end.
 
index 646d0fdd76b1d62a01ac3578ebad9e2fc087e0d7..71dd09084907bf6047f5dde603b19efe64b96e7e 100644 (file)
@@ -288,7 +288,7 @@ normal_state({route, <<"">>,
                               process_iq_admin(From, IQ, StateData);
                           ?NS_MUC_OWNER ->
                               process_iq_owner(From, IQ, StateData);
-                          ?NS_DISCO_INFO when SubEl#disco_info.node == <<>> ->
+                          ?NS_DISCO_INFO ->
                               process_iq_disco_info(From, IQ, StateData);
                           ?NS_DISCO_ITEMS ->
                               process_iq_disco_items(From, IQ, StateData);
@@ -2066,12 +2066,30 @@ presence_broadcast_allowed(JID, StateData) ->
 -spec send_initial_presences_and_messages(
        jid(), binary(), presence(), state(), state()) -> ok.
 send_initial_presences_and_messages(From, Nick, Presence, NewState, OldState) ->
+    send_self_presence(From, NewState),
     send_existing_presences(From, NewState),
     send_initial_presence(From, NewState, OldState),
     History = get_history(Nick, Presence, NewState),
     send_history(From, History, NewState),
     send_subject(From, OldState).
 
+-spec send_self_presence(jid(), state()) -> ok.
+send_self_presence(JID, State) ->
+    AvatarHash = (State#state.config)#config.vcard_xupdate,
+    DiscoInfo = make_disco_info(JID, State),
+    DiscoHash = mod_caps:compute_disco_hash(DiscoInfo, sha),
+    Els1 = [#caps{hash = <<"sha-1">>,
+                 node = ?EJABBERD_URI,
+                 version = DiscoHash}],
+    Els2 = if is_binary(AvatarHash) ->
+                  [#vcard_xupdate{hash = AvatarHash}|Els1];
+             true ->
+                  Els1
+          end,
+    ejabberd_router:route(#presence{from = State#state.jid, to = JID,
+                                   id = randoms:get_string(),
+                                   sub_els = Els2}).
+
 -spec send_initial_presence(jid(), state(), state()) -> ok.
 send_initial_presence(NJID, StateData, OldStateData) ->
     send_new_presence1(NJID, <<"">>, true, StateData, OldStateData).
@@ -3342,12 +3360,15 @@ send_config_change_info(New, #state{config = Old} = StateData) ->
              end
                ++
                case Old#config{anonymous = New#config.anonymous,
-                               vcard = New#config.vcard,
                                logging = New#config.logging} of
                  New -> [];
                  _ -> [104]
                end,
     if Codes /= [] ->
+           lists:foreach(
+             fun({_LJID, #user{jid = JID}}) ->
+                     send_self_presence(JID, StateData#state{config = New})
+             end, ?DICT:to_list(StateData#state.users)),
            Message = #message{type = groupchat,
                               id = randoms:get_string(),
                               sub_els = [#muc_user{status_codes = Codes}]},
@@ -3375,7 +3396,8 @@ remove_nonmembers(StateData) ->
                StateData, (?DICT):to_list(get_users_and_subscribers(StateData))).
 
 -spec set_opts([{atom(), any()}], state()) -> state().
-set_opts([], StateData) -> StateData;
+set_opts([], StateData) ->
+    set_vcard_xupdate(StateData);
 set_opts([{Opt, Val} | Opts], StateData) ->
     NSD = case Opt of
            title ->
@@ -3490,6 +3512,10 @@ set_opts([{Opt, Val} | Opts], StateData) ->
                StateData#state{config =
                                    (StateData#state.config)#config{vcard =
                                                                        Val}};
+           vcard_xupdate ->
+               StateData#state{config =
+                                   (StateData#state.config)#config{vcard_xupdate =
+                                                                       Val}};
            pubsub ->
                StateData#state{config =
                                    (StateData#state.config)#config{pubsub = Val}};
@@ -3524,6 +3550,20 @@ set_opts([{Opt, Val} | Opts], StateData) ->
          end,
     set_opts(Opts, NSD).
 
+set_vcard_xupdate(#state{config =
+                            #config{vcard = VCardRaw,
+                                    vcard_xupdate = undefined} = Config} = State)
+  when VCardRaw /= <<"">> ->
+    case fxml_stream:parse_element(VCardRaw) of
+       {error, _} ->
+           State;
+       El ->
+           Hash = mod_vcard_xupdate:compute_hash(El),
+           State#state{config = Config#config{vcard_xupdate = Hash}}
+    end;
+set_vcard_xupdate(State) ->
+    State.
+
 -define(MAKE_CONFIG_OPT(Opt),
        {get_config_opt_name(Opt), element(Opt, Config)}).
 
@@ -3559,6 +3599,7 @@ make_opts(StateData) ->
      ?MAKE_CONFIG_OPT(#config.presence_broadcast),
      ?MAKE_CONFIG_OPT(#config.voice_request_min_interval),
      ?MAKE_CONFIG_OPT(#config.vcard),
+     ?MAKE_CONFIG_OPT(#config.vcard_xupdate),
      ?MAKE_CONFIG_OPT(#config.pubsub),
      {captcha_whitelist,
       (?SETS):to_list((StateData#state.config)#config.captcha_whitelist)},
@@ -3602,12 +3643,8 @@ destroy_room(DEl, StateData) ->
          false -> Fiffalse
        end).
 
--spec process_iq_disco_info(jid(), iq(), state()) ->
-                                  {result, disco_info()} | {error, stanza_error()}.
-process_iq_disco_info(_From, #iq{type = set, lang = Lang}, _StateData) ->
-    Txt = <<"Value 'set' of 'type' attribute is not allowed">>,
-    {error, xmpp:err_not_allowed(Txt, Lang)};
-process_iq_disco_info(_From, #iq{type = get, lang = Lang}, StateData) ->
+-spec make_disco_info(jid(), state()) -> disco_info().
+make_disco_info(_From, StateData) ->
     Config = StateData#state.config,
     Feats = [?NS_VCARD, ?NS_MUC,
             ?CONFIG_OPT_TO_FEATURE((Config#config.public),
@@ -3633,11 +3670,35 @@ process_iq_disco_info(_From, #iq{type = get, lang = Lang}, StateData) ->
               _ ->
                   []
           end,
-    {result, #disco_info{xdata = [iq_disco_info_extras(Lang, StateData)],
-                        identities = [#identity{category = <<"conference">>,
-                                                type = <<"text">>,
-                                                name = get_title(StateData)}],
-                        features = Feats}}.
+    #disco_info{identities = [#identity{category = <<"conference">>,
+                                       type = <<"text">>,
+                                       name = get_title(StateData)}],
+               features = Feats}.
+
+-spec process_iq_disco_info(jid(), iq(), state()) ->
+                                  {result, disco_info()} | {error, stanza_error()}.
+process_iq_disco_info(_From, #iq{type = set, lang = Lang}, _StateData) ->
+    Txt = <<"Value 'set' of 'type' attribute is not allowed">>,
+    {error, xmpp:err_not_allowed(Txt, Lang)};
+process_iq_disco_info(From, #iq{type = get, lang = Lang,
+                               sub_els = [#disco_info{node = <<>>}]},
+                     StateData) ->
+    DiscoInfo = make_disco_info(From, StateData),
+    Extras = iq_disco_info_extras(Lang, StateData),
+    {result, DiscoInfo#disco_info{xdata = [Extras]}};
+process_iq_disco_info(From, #iq{type = get, lang = Lang,
+                               sub_els = [#disco_info{node = Node}]},
+                     StateData) ->
+    try
+       true = mod_caps:is_valid_node(Node),
+       DiscoInfo = make_disco_info(From, StateData),
+       Hash = mod_caps:compute_disco_hash(DiscoInfo, sha),
+       Node = <<(?EJABBERD_URI)/binary, $#, Hash/binary>>,
+       {result, DiscoInfo#disco_info{node = Node}}
+    catch _:{badmatch, _} ->
+           Txt = <<"Invalid node name">>,
+           {error, xmpp:err_item_not_found(Txt, Lang)}
+    end.
 
 -spec iq_disco_info_extras(binary(), state()) -> xdata().
 iq_disco_info_extras(Lang, StateData) ->
@@ -3703,13 +3764,15 @@ process_iq_vcard(_From, #iq{type = get}, StateData) ->
        {error, _} ->
            {error, xmpp:err_item_not_found()}
     end;
-process_iq_vcard(From, #iq{type = set, lang = Lang, sub_els = [SubEl]},
+process_iq_vcard(From, #iq{type = set, lang = Lang, sub_els = [Pkt]},
                 StateData) ->
     case get_affiliation(From, StateData) of
        owner ->
-           VCardRaw = fxml:element_to_binary(xmpp:encode(SubEl)),
+           SubEl = xmpp:encode(Pkt),
+           VCardRaw = fxml:element_to_binary(SubEl),
+           Hash = mod_vcard_xupdate:compute_hash(SubEl),
            Config = StateData#state.config,
-           NewConfig = Config#config{vcard = VCardRaw},
+           NewConfig = Config#config{vcard = VCardRaw, vcard_xupdate = Hash},
            change_config(NewConfig, StateData);
        _ ->
            ErrText = <<"Owner privileges required">>,
@@ -4133,6 +4196,28 @@ send_wrapped(From, To, Packet, Node, State) ->
                    ok
            end;
        true ->
+           case Packet of
+               #presence{type = unavailable} ->
+                   case xmpp:get_subtag(Packet, #muc_user{}) of
+                       #muc_user{destroy = Destroy,
+                                 status_codes = Codes} ->
+                           case Destroy /= undefined orelse
+                                (lists:member(110,Codes) andalso
+                                 not lists:member(303, Codes)) of
+                               true ->
+                                   ejabberd_router:route(
+                                     #presence{from = State#state.jid, to = To,
+                                               id = randoms:get_string(),
+                                               type = unavailable});
+                               false ->
+                                   ok
+                           end;
+                       _ ->
+                           false
+                   end;
+               _ ->
+                   ok
+           end,
            ejabberd_router:route(xmpp:set_from_to(Packet, From, To))
     end.
 
index 597ff41a84c5ecda8395fa42768be4ef3e9ac019..a44b8ced8b2c78b94bb046848c46b7ebda47026f 100644 (file)
@@ -32,6 +32,8 @@
 
 -export([update_presence/1, vcard_set/1, remove_user/2,
         user_send_packet/1, mod_opt_type/1, mod_options/1, depends/2]).
+%% API
+-export([compute_hash/1]).
 
 -include("ejabberd.hrl").
 -include("logger.hrl").
index bcceb69386b672d58164631e5dfef048c86342ad..ae6e2e9a7420d9b1567be0d270d74f0b2d92951c 100644 (file)
@@ -800,6 +800,11 @@ change_affiliation_slave(Config, {Aff, Role, Status, Reason}) ->
     MyNick = ?config(nick, Config),
     MyNickJID = jid:replace_resource(Room, MyNick),
     ct:comment("Receiving affiliation change to ~s", [Aff]),
+    if Aff == outcast ->
+           #presence{from = Room, type = unavailable} = recv_presence(Config);
+       true ->
+           ok
+    end,
     #muc_user{status_codes = Codes,
              items = [#muc_item{role = Role,
                                 actor = Actor,
@@ -858,6 +863,7 @@ kick_slave(Config) ->
     wait_for_master(Config),
     {[], _, _} = join(Config),
     ct:comment("Receiving role change to 'none'"),
+    #presence{from = Room, type = unavailable} = recv_presence(Config),
     #muc_user{status_codes = Codes,
              items = [#muc_item{role = none,
                                 affiliation = none,
@@ -889,6 +895,7 @@ destroy_master(Config) ->
     wait_for_slave(Config),
     ok = destroy(Config, Reason),
     ct:comment("Receiving destruction presence"),
+    #presence{from = Room, type = unavailable} = recv_presence(Config),
     #muc_user{items = [#muc_item{role = none,
                                 affiliation = none}],
              destroy = #muc_destroy{jid = AltRoom,
@@ -907,6 +914,7 @@ destroy_slave(Config) ->
     #stanza_error{reason = 'forbidden'} = destroy(Config, Reason),
     wait_for_master(Config),
     ct:comment("Receiving destruction presence"),
+    #presence{from = Room, type = unavailable} = recv_presence(Config),
     #muc_user{items = [#muc_item{role = none,
                                 affiliation = none}],
              destroy = #muc_destroy{jid = AltRoom,
@@ -938,6 +946,7 @@ vcard_master(Config) ->
 vcard_slave(Config) ->
     wait_for_master(Config),
     {[], _, _} = join(Config),
+    [104] = recv_config_change_message(Config),
     VCard = get_event(Config),
     VCard = get_vcard(Config),
     #stanza_error{reason = 'forbidden'} = set_vcard(Config, VCard),
@@ -1150,11 +1159,13 @@ config_members_only_master(Config) ->
     disconnect(Config).
 
 config_members_only_slave(Config) ->
+    Room = muc_room_jid(Config),
     MyJID = my_jid(Config),
     MyNickJID = my_muc_jid(Config),
     {[], _, _} = slave_join(Config),
     [104] = recv_config_change_message(Config),
     ct:comment("Getting kicked because the room has become members-only"),
+    #presence{from = Room, type = unavailable} = recv_presence(Config),
     #muc_user{status_codes = Codes,
              items = [#muc_item{jid = MyJID,
                                 role = none,
@@ -1171,6 +1182,7 @@ config_members_only_slave(Config) ->
     ct:comment("Waiting for the peer to ask for join"),
     join = get_event(Config),
     {[], _, _} = join(Config, participant, member),
+    #presence{from = Room, type = unavailable} = recv_presence(Config),
     #muc_user{status_codes = NewCodes,
              items = [#muc_item{jid = MyJID,
                                 role = none,
@@ -1555,8 +1567,9 @@ join_new(Config, Room) ->
     MyJID = my_jid(Config),
     MyNick = ?config(nick, Config),
     MyNickJID = jid:replace_resource(Room, MyNick),
-    ct:comment("Joining new room"),
+    ct:comment("Joining new room ~p", [Room]),
     send(Config, #presence{to = MyNickJID, sub_els = [#muc{}]}),
+    #presence{from = Room, type = available} = recv_presence(Config),
     %% As per XEP-0045 we MUST receive stanzas in the following order:
     %% 1. In-room presence from other occupants
     %% 2. In-room presence from the joining entity itself (so-called "self-presence")
@@ -1625,30 +1638,33 @@ join(Config, Role, Aff, SubEl) ->
     case recv_presence(Config) of
        #presence{type = error, from = MyNickJID} = Err ->
            xmpp:get_subtag(Err, #stanza_error{});
-       #presence{type = available, from = PeerNickJID} = Pres ->
-           #muc_user{items = [#muc_item{role = moderator,
-                                        affiliation = owner}]} =
-               xmpp:get_subtag(Pres, #muc_user{}),
-           ct:comment("Receiving initial self-presence"),
-           #muc_user{status_codes = Codes,
-                     items = [#muc_item{role = Role,
-                                        jid = MyJID,
-                                        affiliation = Aff}]} =
-               recv_muc_presence(Config, MyNickJID, available),
-           ct:comment("Checking if code '110' (self-presence) is set"),
-           true = lists:member(110, Codes),
-           {History, Subj} = recv_history_and_subject(Config),
-           {History, Subj, Codes};
-       #presence{type = available, from = MyNickJID} = Pres ->
-           #muc_user{status_codes = Codes,
-                     items = [#muc_item{role = Role,
-                                        jid = MyJID,
-                                        affiliation = Aff}]} =
-               xmpp:get_subtag(Pres, #muc_user{}),
-           ct:comment("Checking if code '110' (self-presence) is set"),
-           true = lists:member(110, Codes),
-           {History, Subj} = recv_history_and_subject(Config),
-           {empty, History, Subj, Codes}
+       #presence{from = Room, type = available} ->
+           case recv_presence(Config) of
+               #presence{type = available, from = PeerNickJID} = Pres ->
+                   #muc_user{items = [#muc_item{role = moderator,
+                                                affiliation = owner}]} =
+                       xmpp:get_subtag(Pres, #muc_user{}),
+                   ct:comment("Receiving initial self-presence"),
+                   #muc_user{status_codes = Codes,
+                             items = [#muc_item{role = Role,
+                                                jid = MyJID,
+                                                affiliation = Aff}]} =
+                       recv_muc_presence(Config, MyNickJID, available),
+                   ct:comment("Checking if code '110' (self-presence) is set"),
+                   true = lists:member(110, Codes),
+                   {History, Subj} = recv_history_and_subject(Config),
+                   {History, Subj, Codes};
+               #presence{type = available, from = MyNickJID} = Pres ->
+                   #muc_user{status_codes = Codes,
+                             items = [#muc_item{role = Role,
+                                                jid = MyJID,
+                                                affiliation = Aff}]} =
+                       xmpp:get_subtag(Pres, #muc_user{}),
+                   ct:comment("Checking if code '110' (self-presence) is set"),
+                   true = lists:member(110, Codes),
+                   {History, Subj} = recv_history_and_subject(Config),
+                   {empty, History, Subj, Codes}
+           end
     end.
 
 leave(Config) ->
@@ -1667,6 +1683,7 @@ leave(Config, Room) ->
     end,
     ct:comment("Leaving the room"),
     send(Config, #presence{to = MyNickJID, type = unavailable}),
+    #presence{from = Room, type = unavailable} = recv_presence(Config),
     #muc_user{
        status_codes = Codes,
        items = [#muc_item{role = none, jid = MyJID}]} =
@@ -1702,6 +1719,7 @@ set_config(Config, RoomConfig, Room) ->
                       sub_els = [#muc_owner{config = #xdata{type = submit,
                                                             fields = Fs}}]}) of
        #iq{type = result, sub_els = []} ->
+           #presence{from = Room, type = available} = recv_presence(Config),
            #message{from = Room, type = groupchat} = Msg = recv_message(Config),
            #muc_user{status_codes = Codes} = xmpp:get_subtag(Msg, #muc_user{}),
            lists:sort(Codes);
@@ -1846,6 +1864,7 @@ set_vcard(Config, VCard) ->
     case send_recv(Config, #iq{type = set, to = Room,
                               sub_els = [VCard]}) of
        #iq{type = result, sub_els = []} ->
+           [104] = recv_config_change_message(Config),
            ok;
        #iq{type = error} = Err ->
            xmpp:get_subtag(Err, #stanza_error{})
@@ -1865,6 +1884,7 @@ get_vcard(Config) ->
 recv_config_change_message(Config) ->
     ct:comment("Receiving configuration change notification message"),
     Room = muc_room_jid(Config),
+    #presence{from = Room, type = available} = recv_presence(Config),
     #message{type = groupchat, from = Room} = Msg = recv_message(Config),
     #muc_user{status_codes = Codes} = xmpp:get_subtag(Msg, #muc_user{}),
     lists:sort(Codes).