diff --git a/include/mod_muc_room.hrl b/include/mod_muc_room.hrl index c192f1bd8..b0691d084 100644 --- a/include/mod_muc_room.hrl +++ b/include/mod_muc_room.hrl @@ -81,7 +81,8 @@ role :: role(), %%is_subscriber = false :: boolean(), %%subscriptions = [] :: [binary()], - last_presence :: presence() | undefined + last_presence :: presence() | undefined, + occupant_id :: binary() }). -record(subscriber, {jid :: jid(), @@ -132,7 +133,8 @@ activity = treap:empty() :: treap:treap(), room_shaper = none :: ejabberd_shaper:shaper(), room_queue :: p1_queue:queue({message | presence, jid()}) | undefined, - hibernate_timer = none :: reference() | none | hibernating + hibernate_timer = none :: reference() | none | hibernating, + salt = <<>> :: binary() }). -type users() :: #{ljid() => #user{}}. diff --git a/src/ejabberd_config_transformer.erl b/src/ejabberd_config_transformer.erl index 4d62dff93..1b480aa52 100644 --- a/src/ejabberd_config_transformer.erl +++ b/src/ejabberd_config_transformer.erl @@ -73,7 +73,7 @@ transform(Host, Y, Acc) -> transform(Host, modules, ModOpts, Acc) -> {ModOpts1, Acc2} = - lists:mapfoldr( + filtermapfoldr( fun({Mod, Opts}, Acc1) -> Opts1 = transform_module_options(Opts), transform_module(Host, Mod, Opts1, Acc1) @@ -446,7 +446,7 @@ transform_module(_Host, mod_blocking, Opts, Acc) -> (_) -> true end, Opts), - {{mod_blocking, Opts1}, Acc}; + {{true, {mod_blocking, Opts1}}, Acc}; transform_module(_Host, mod_carboncopy, Opts, Acc) -> Opts1 = lists:filter( fun({Opt, _}) when Opt == ram_db_type; @@ -459,7 +459,7 @@ transform_module(_Host, mod_carboncopy, Opts, Acc) -> (_) -> true end, Opts), - {{mod_carboncopy, Opts1}, Acc}; + {{true, {mod_carboncopy, Opts1}}, Acc}; transform_module(_Host, mod_http_api, Opts, Acc) -> Opts1 = lists:filter( fun({admin_ip_access, _}) -> @@ -468,7 +468,7 @@ transform_module(_Host, mod_http_api, Opts, Acc) -> (_) -> true end, Opts), - {{mod_http_api, Opts1}, Acc}; + {{true, {mod_http_api, Opts1}}, Acc}; transform_module(_Host, mod_http_upload, Opts, Acc) -> Opts1 = lists:filter( fun({service_url, _}) -> @@ -477,7 +477,7 @@ transform_module(_Host, mod_http_upload, Opts, Acc) -> (_) -> true end, Opts), - {{mod_http_upload, Opts1}, Acc}; + {{true, {mod_http_upload, Opts1}}, Acc}; transform_module(_Host, mod_pubsub, Opts, Acc) -> Opts1 = lists:map( fun({plugins, Plugins}) -> @@ -505,9 +505,11 @@ transform_module(_Host, mod_pubsub, Opts, Acc) -> (Opt) -> Opt end, Opts), - {{mod_pubsub, Opts1}, Acc}; + {{true, {mod_pubsub, Opts1}}, Acc}; +transform_module(_Host, mod_muc_occupantid, _Opts, Acc) -> + {false, Acc}; transform_module(_Host, Mod, Opts, Acc) -> - {{Mod, Opts}, Acc}. + {{true, {Mod, Opts}}, Acc}. strip_odbc_suffix(M) -> [_|T] = lists:reverse(string:tokens(atom_to_list(M), "_")), diff --git a/src/mod_muc.erl b/src/mod_muc.erl index 6931a546d..954487568 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -27,6 +27,7 @@ -protocol({xep, 45, '1.35.3', '0.5.0', "complete", ""}). -protocol({xep, 249, '1.2', '0.5.0', "complete", ""}). -protocol({xep, 486, '0.1.0', '24.07', "complete", ""}). +-protocol({xep, 421, '1.0.1', '23.10', "complete", ""}). -ifndef(GEN_SERVER). -define(GEN_SERVER, gen_server). -endif. @@ -729,10 +730,6 @@ process_disco_info(#iq{type = get, from = From, to = To, lang = Lang, true -> [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2]; false -> [] end, - OccupantIdFeatures = case gen_mod:is_loaded(ServerHost, mod_muc_occupantid) of - true -> [?NS_OCCUPANT_ID]; - false -> [] - end, RSMFeatures = case RMod:rsm_supported() of true -> [?NS_RSM]; false -> [] @@ -743,8 +740,8 @@ process_disco_info(#iq{type = get, from = From, to = To, lang = Lang, end, Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, ?NS_MUC, ?NS_VCARD, ?NS_MUCSUB, ?NS_MUC_UNIQUE, - ?NS_MUC_STABLE_ID - | RegisterFeatures ++ RSMFeatures ++ MAMFeatures ++ OccupantIdFeatures], + ?NS_MUC_STABLE_ID, ?NS_OCCUPANT_ID + | RegisterFeatures ++ RSMFeatures ++ MAMFeatures], Name = mod_muc_opt:name(ServerHost), Identity = #identity{category = <<"conference">>, type = <<"text">>, diff --git a/src/mod_muc_occupantid.erl b/src/mod_muc_occupantid.erl deleted file mode 100644 index 6227672a6..000000000 --- a/src/mod_muc_occupantid.erl +++ /dev/null @@ -1,162 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : mod_muc_occupantid.erl -%%% Author : Badlop -%%% Purpose : Add Occupant Ids to stanzas in anonymous MUC rooms (XEP-0421) -%%% Created : -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2026 ProcessOne -%%% -%%% This program is free software; you can redistribute it and/or -%%% modify it under the terms of the GNU General Public License as -%%% published by the Free Software Foundation; either version 2 of the -%%% License, or (at your option) any later version. -%%% -%%% This program is distributed in the hope that it will be useful, -%%% but WITHOUT ANY WARRANTY; without even the implied warranty of -%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -%%% General Public License for more details. -%%% -%%% You should have received a copy of the GNU General Public License along -%%% with this program; if not, write to the Free Software Foundation, Inc., -%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -%%% -%%%---------------------------------------------------------------------- - --module(mod_muc_occupantid). - --author('badlop@process-one.net'). - --protocol({xep, 421, '1.0.1', '23.10', "complete", ""}). - --behaviour(gen_mod). - --include_lib("xmpp/include/xmpp.hrl"). --include("logger.hrl"). --include("translate.hrl"). --include("mod_muc_room.hrl"). - --export([start/2, stop/1, - mod_options/1, mod_doc/0, depends/2]). --export([filter_packet/3, clean_obsolete_salts/0]). - -%%% -%%% gen_mod -%%% - -start(_Host, _Opts) -> - prepare_table(), - {ok, [{hook, muc_filter_presence, filter_packet, 10}, - {hook, muc_filter_message, filter_packet, 10}, - {hook, config_reloaded, clean_obsolete_salts, 90, global}]}. - -stop(_Host) -> - ok. - -%%% -%%% Hooks -%%% - -filter_packet(Packet, State, _Nick) -> - add_occupantid_packet(Packet, State#state.jid). - -%%% -%%% XEP-0421 Occupant-id -%%% - -add_occupantid_packet(Packet, RoomJid) -> - From = xmpp:get_from(Packet), - OccupantId = calculate_occupantid(From, RoomJid), - OccupantElement = #occupant_id{id = OccupantId}, - xmpp:append_subtags(xmpp:remove_subtag(Packet, OccupantElement), [OccupantElement]). - -calculate_occupantid(From, RoomJid) -> - Term = {get_salt(RoomJid#jid.lserver), RoomJid, jid:remove_resource(From)}, - misc:term_to_base64(crypto:hash(sha256, io_lib:format("~p", [Term]))). - -%%% -%%% Table storing rooms' salt -%%% - --record(muc_occupant_id, {service_jid, salt}). - -prepare_table() -> - try - mnesia:table_info(muc_occupant_id, attributes), - %% Wait some time to ensure mod_muc is started in all hosts - timer:apply_after(60000, ?MODULE, clean_obsolete_salts, []) - catch - exit:{aborted, {no_exists, _, _}} -> - create_table() - end. - - -create_table() -> - ejabberd_mnesia:create(?MODULE, muc_occupant_id, - [{disc_copies, [node()]}, - {attributes, record_info(fields, muc_occupant_id)}, - {type, set}]). - - -get_salt(ServiceJid) -> - case mnesia:dirty_read(muc_occupant_id, ServiceJid) of - [] -> - Salt = p1_rand:get_string(), - ok = write_salt(ServiceJid, Salt), - Salt; - [#muc_occupant_id{salt = Salt}] -> - Salt - end. - -write_salt(ServiceJid, Salt) -> - mnesia:dirty_write(#muc_occupant_id{service_jid = ServiceJid, salt = Salt}). - -%% @format-begin -clean_obsolete_salts() -> - Hosts = ejabberd_option:hosts(), - MucHosts = - lists:foldl(fun(Host, Acc) -> gen_mod:get_module_opt_hosts(Host, mod_muc) ++ Acc end, - [], - Hosts), - - F = fun() -> - mnesia:write_lock_table(muc_occupant_id), - Ft = fun(#muc_occupant_id{service_jid = Service, salt = _Salt} = Rec, Acc) -> - case lists:member(Service, MucHosts) of - true -> - Acc; - false -> - mnesia:delete_object(Rec), - [Service | Acc] - end - end, - mnesia:foldl(Ft, [], muc_occupant_id) - end, - case mnesia:transaction(F) of - {atomic, Res} -> - ?DEBUG("Deleted Salts for occupant-id of MUC services: ~p", [Res]), - ok; - _ -> - ok - end. -%% @format-end - -%%% -%%% Doc -%%% - -mod_options(_Host) -> - []. - -mod_doc() -> - #{desc => - [?T("This module implements " - "https://xmpp.org/extensions/xep-0421.html" - "[XEP-0421: Anonymous unique occupant identifiers for MUCs]."), "", - ?T("When the module is enabled, the feature is enabled " - "in all semi-anonymous rooms.")], - note => "added in 23.10" - }. - -depends(_, _) -> - [{mod_muc, hard}]. diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 4db4363f9..192f9b960 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -310,6 +310,7 @@ init([Host, ServerHost, Access, Room, HistorySize, history = lqueue_new(HistorySize, QueueType), jid = jid:make(Room, Host), just_created = true, + salt = p1_rand:get_string(), room_queue = RoomQueue, room_shaper = Shaper}), State1 = set_affiliation(Creator, owner, State), @@ -334,6 +335,7 @@ init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType]) room = Room, history = lqueue_new(HistorySize, QueueType), jid = Jid, + salt = p1_rand:get_string(), room_queue = RoomQueue, room_shaper = Shaper}), add_to_log(room_existence, started, State), @@ -638,11 +640,9 @@ normal_state({route, ToNick, FromNick), X = #muc_user{}, Packet2 = xmpp:set_subtag(Packet, X), - case ejabberd_hooks:run_fold(muc_filter_message, - StateData#state.server_host, - xmpp:put_meta(Packet2, mam_ignore, true), - [StateData, FromNick]) of - drop -> + case filter_message_hook(StateData, FromNick, + xmpp:put_meta(Packet2, mam_ignore, true)) of + drop -> ok; Packet3 -> PrivMsg = xmpp:set_from(xmpp:del_meta(Packet3, mam_ignore), FromNickJID), @@ -1092,12 +1092,8 @@ process_groupchat_message(#message{from = From, lang = Lang} = Packet, StateData end, case IsAllowed of true -> - case - ejabberd_hooks:run_fold(muc_filter_message, - StateData#state.server_host, - Packet, - [StateData, FromNick]) - of + case filter_message_hook(StateData, FromNick, + Packet) of drop -> {next_state, normal_state, StateData}; NewPacket1 -> @@ -1401,10 +1397,7 @@ process_presence(Nick, #presence{from = From, type = Type0} = Packet0, StateData IsOnline = is_user_online(From, StateData), if Type0 == available; IsOnline and ((Type0 == unavailable) or (Type0 == error)) -> - case ejabberd_hooks:run_fold(muc_filter_presence, - StateData#state.server_host, - Packet0, - [StateData, Nick]) of + case filter_presence_hook(StateData, Nick, Packet0) of drop -> {next_state, normal_state, StateData}; #presence{} = Packet -> @@ -1561,7 +1554,8 @@ get_users_and_subscribers_aux(Subscribers, StateData) -> #user{jid = jid:make(LBareJID), nick = Nick, role = none, - last_presence = undefined}, + last_presence = undefined, + occupant_id = <<>>}, Acc); true -> Acc @@ -2130,10 +2124,44 @@ set_subscriber(JID, Nick, Nodes, end, NewStateData. +-spec calculate_occupant_id(jid(), state()) -> binary(). +calculate_occupant_id(Jid, #state{salt = Salt, jid = RoomJid}) -> + JidS = jid:encode(jid:remove_resource(Jid)), + RoomJidS = jid:encode(RoomJid), + Term = <>, + misc:term_to_base64(crypto:hash(sha256, Term)). + +-spec filter_message_hook(state(), binary(), #message{}) -> drop | #message{}. +filter_message_hook(#state{users = Users} = StateData, Nick, #message{from = From} = Message) -> + OccupantId = case maps:find(jid:tolower(From), Users) of + {ok, #user{occupant_id = Id}} -> Id; + _ -> calculate_occupant_id(From, StateData) + end, + Message2 = xmpp:append_subtags(xmpp:remove_subtag(Message, #occupant_id{}), + [#occupant_id{id = OccupantId}]), + ejabberd_hooks:run_fold(muc_filter_message, + StateData#state.server_host, + Message2, + [StateData, Nick]). + +-spec filter_presence_hook(state(), binary(), #presence{}) -> drop | #presence{}. +filter_presence_hook(#state{users = Users} = StateData, Nick, #presence{from = From} = Pres) -> + OccupantId = case maps:find(jid:tolower(From), Users) of + {ok, #user{occupant_id = Id}} -> Id; + _ -> calculate_occupant_id(From, StateData) + end, + Pres2 = xmpp:append_subtags(xmpp:remove_subtag(Pres, #occupant_id{}), + [#occupant_id{id = OccupantId}]), + ejabberd_hooks:run_fold(muc_filter_message, + StateData#state.server_host, + Pres2, + [StateData, Nick]). + + -spec add_online_user(jid(), binary(), role(), state()) -> state(). add_online_user(JID, Nick, Role, StateData) -> tab_add_online_user(JID, StateData), - User = #user{jid = JID, nick = Nick, role = Role}, + User = #user{jid = JID, nick = Nick, role = Role, occupant_id = calculate_occupant_id(JID, StateData)}, reset_hibernate_timer(update_online_user(JID, User, StateData)). -spec remove_online_user(jid(), state()) -> state(). @@ -3043,10 +3071,8 @@ send_subject(JID, #state{subject_author = {Nick, AuthorJID}} = StateData) -> end, Packet = #message{from = AuthorJID, to = JID, type = groupchat, subject = Subject}, - case ejabberd_hooks:run_fold(muc_filter_message, - StateData#state.server_host, - xmpp:put_meta(Packet, mam_ignore, true), - [StateData, Nick]) of + case filter_message_hook(StateData, Nick, + xmpp:put_meta(Packet, mam_ignore, true)) of drop -> ok; NewPacket1 -> @@ -4271,6 +4297,8 @@ set_opts2([{Opt, Val} | Opts], StateData) -> hats_users -> StateData#state{hats_users = maps:from_list(Val)}; hibernation_time -> StateData; + salt -> + StateData#state{salt = Val}; Other -> ?INFO_MSG("Unknown MUC room option, will be discarded: ~p", [Other]), StateData @@ -4353,6 +4381,7 @@ make_opts(StateData, Hibernation) -> {hats_defs, maps:to_list(StateData#state.hats_defs)}, {hats_users, maps:to_list(StateData#state.hats_users)}, {hibernation_time, if Hibernation -> erlang:system_time(microsecond); true -> undefined end}, + {salt, StateData#state.salt}, {subscribers, Subscribers}]. expand_opts(CompactOpts) -> @@ -4377,14 +4406,16 @@ expand_opts(CompactOpts) -> Subject = proplists:get_value(subject, CompactOpts, <<"">>), Subscribers = proplists:get_value(subscribers, CompactOpts, []), HibernationTime = proplists:get_value(hibernation_time, CompactOpts, 0), + Salt = proplists:get_value(hibernation_time, CompactOpts, <<>>), [{subject, Subject}, {subject_author, SubjectAuthor}, {subscribers, Subscribers}, - {hibernation_time, HibernationTime} + {hibernation_time, HibernationTime}, + {salt, Salt} | lists:reverse(Opts1)]. config_fields() -> - [subject, subject_author, subscribers, hibernate_time | record_info(fields, config)]. + [subject, subject_author, subscribers, hibernate_time, salt | record_info(fields, config)]. -spec destroy_room(muc_destroy(), state()) -> {result, undefined, stop}. destroy_room(DEl, StateData) -> @@ -4447,7 +4478,7 @@ make_disco_info(From, StateData) -> ?NS_DISCO_INFO, ?NS_DISCO_ITEMS, ?NS_COMMANDS, ?NS_MESSAGE_MODERATE_0, ?NS_MESSAGE_MODERATE_1, - ?NS_MESSAGE_RETRACT, + ?NS_MESSAGE_RETRACT, ?NS_OCCUPANT_ID, ?CONFIG_OPT_TO_FEATURE((Config#config.public), <<"muc_public">>, <<"muc_hidden">>), ?CONFIG_OPT_TO_FEATURE((Config#config.persistent), @@ -4472,12 +4503,6 @@ make_disco_info(From, StateData) -> true -> [?NS_HATS]; false -> [] end - ++ case gen_mod:is_loaded(StateData#state.server_host, mod_muc_occupantid) of - true -> - [?NS_OCCUPANT_ID]; - _ -> - [] - end ++ case {gen_mod:is_loaded(StateData#state.server_host, mod_mam), Config#config.mam} of {true, true} -> @@ -5423,10 +5448,8 @@ process_iq_moderate(From, #iq{type = set, lang = Lang}, Id, Reason, from = From, sub_els = SubEl}, {FromNick, _Role} = get_participant_data(From, StateData), - Packet = ejabberd_hooks:run_fold(muc_filter_message, - StateData#state.server_host, - xmpp:put_meta(Packet0, mam_ignore, true), - [StateData, FromNick]), + Packet = filter_message_hook(StateData, FromNick, + xmpp:put_meta(Packet0, mam_ignore, true)), send_wrapped_multiple(JID, get_users_and_subscribers_with_node(?NS_MUCSUB_NODES_MESSAGES, StateData), Packet, ?NS_MUCSUB_NODES_MESSAGES, StateData),