Add support to register nick in a room (#3455)

Registering a nick in the MUC service or in a room is mutually exclusive:
- A nick that is registered in the service cannot be registered in any
  room, not even the original owner can register it.
- Similarly, a nick registered in any room cannot be registered in the
  service.
This commit is contained in:
Badlop 2023-10-10 13:08:13 +02:00
parent 9534ca2da1
commit 10245b40ee
4 changed files with 118 additions and 43 deletions

View File

@ -51,6 +51,7 @@
process_disco_items/1,
process_vcard/1,
process_register/1,
process_iq_register/1,
process_muc_unique/1,
process_mucsub/1,
broadcast_service_message/3,
@ -675,29 +676,33 @@ process_vcard(#iq{lang = Lang} = IQ) ->
xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
-spec process_register(iq()) -> iq().
process_register(#iq{type = Type, from = From, to = To, lang = Lang,
sub_els = [El = #register{}]} = IQ) ->
process_register(IQ) ->
case process_iq_register(IQ) of
{result, Result} ->
xmpp:make_iq_result(IQ, Result);
{error, Err} ->
xmpp:make_error(IQ, Err)
end.
-spec process_iq_register(iq()) -> {result, register()} | {error, stanza_error()}.
process_iq_register(#iq{type = Type, from = From, to = To, lang = Lang,
sub_els = [El = #register{}]}) ->
Host = To#jid.lserver,
RegisterDestination = jid:encode(To),
ServerHost = ejabberd_router:host_of_route(Host),
AccessRegister = mod_muc_opt:access_register(ServerHost),
case acl:match_rule(ServerHost, AccessRegister, From) of
allow ->
case Type of
get ->
xmpp:make_iq_result(
IQ, iq_get_register_info(ServerHost, Host, From, Lang));
{result, iq_get_register_info(ServerHost, RegisterDestination, From, Lang)};
set ->
case process_iq_register_set(ServerHost, Host, From, El, Lang) of
{result, Result} ->
xmpp:make_iq_result(IQ, Result);
{error, Err} ->
xmpp:make_error(IQ, Err)
end
process_iq_register_set(ServerHost, RegisterDestination, From, El, Lang)
end;
deny ->
ErrText = ?T("Access denied by service policy"),
Err = xmpp:err_forbidden(ErrText, Lang),
xmpp:make_error(IQ, Err)
{error, Err}
end.
-spec process_disco_info(iq()) -> iq().
@ -1416,6 +1421,11 @@ mod_doc() ->
"nobody else can use that nickname in any room in the MUC "
"service. To register a nickname, open the Service Discovery in "
"your XMPP client and register in the MUC service."), "",
?T("It is also possible to register a nickname in a room, so "
"nobody else can use that nickname in that room. If a nick is "
"registered in the MUC service, that nick cannot be registered in "
"any room, and vice versa: a nick that is registered in a room "
"cannot be registered at the MUC service."), "",
?T("This module supports clustering and load balancing. One module "
"can be started per cluster node. Rooms are distributed at "
"creation time on all available MUC module instances. The "
@ -1461,11 +1471,12 @@ mod_doc() ->
"modify that option.")}},
{access_register,
#{value => ?T("AccessName"),
note => "improved in 23.xx",
desc =>
?T("This option specifies who is allowed to register nickname "
"within the Multi-User Chat service. The default is 'all' for "
"within the Multi-User Chat service and rooms. The default is 'all' for "
"backward compatibility, which means that any user is allowed "
"to register any free nick.")}},
"to register any free nick in the MUC service and in the rooms.")}},
{db_type,
#{value => "mnesia | sql",
desc =>

View File

@ -82,13 +82,19 @@ forget_room(_LServer, Host, Name) ->
end,
mnesia:transaction(F).
can_use_nick(_LServer, Host, JID, Nick) ->
can_use_nick(_LServer, ServiceOrRoom, JID, Nick) ->
{LUser, LServer, _} = jid:tolower(JID),
LUS = {LUser, LServer},
MatchSpec = case (jid:decode(ServiceOrRoom))#jid.lserver of
ServiceOrRoom -> [{'==', {element, 2, '$1'}, ServiceOrRoom}];
Service -> [{'orelse',
{'==', {element, 2, '$1'}, Service},
{'==', {element, 2, '$1'}, ServiceOrRoom} }]
end,
case catch mnesia:dirty_select(muc_registered,
[{#muc_registered{us_host = '$1',
nick = Nick, _ = '_'},
[{'==', {element, 2, '$1'}, Host}],
MatchSpec,
['$_']}])
of
{'EXIT', _Reason} -> true;
@ -110,31 +116,46 @@ get_nick(_LServer, Host, From) ->
[#muc_registered{nick = Nick}] -> Nick
end.
set_nick(_LServer, Host, From, Nick) ->
set_nick(_LServer, ServiceOrRoom, From, Nick) ->
{LUser, LServer, _} = jid:tolower(From),
LUS = {LUser, LServer},
F = fun () ->
case Nick of
<<"">> ->
mnesia:delete({muc_registered, {LUS, Host}}),
mnesia:delete({muc_registered, {LUS, ServiceOrRoom}}),
ok;
_ ->
Service = (jid:decode(ServiceOrRoom))#jid.lserver,
MatchSpec = case (ServiceOrRoom == Service) of
true -> [{'==', {element, 2, '$1'}, ServiceOrRoom}];
false -> [{'orelse',
{'==', {element, 2, '$1'}, Service},
{'==', {element, 2, '$1'}, ServiceOrRoom} }]
end,
Allow = case mnesia:select(
muc_registered,
[{#muc_registered{us_host =
'$1',
nick = Nick,
_ = '_'},
[{'==', {element, 2, '$1'},
Host}],
[{#muc_registered{us_host = '$1', nick = Nick, _ = '_'},
MatchSpec,
['$_']}]) of
[] when (ServiceOrRoom == Service) ->
NickRegistrations = mnesia:select(
muc_registered,
[{#muc_registered{us_host = '$1', nick = Nick, _ = '_'},
[],
['$_']}]),
not lists:any(fun({_, {_NRUS, NRServiceOrRoom}, _Nick}) ->
Service == (jid:decode(NRServiceOrRoom))#jid.lserver end,
NickRegistrations);
[] -> true;
[#muc_registered{us_host = {_U, Host}}]
when (Host == Service) and (ServiceOrRoom /= Service) ->
false;
[#muc_registered{us_host = {U, _Host}}] ->
U == LUS
end,
if Allow ->
mnesia:write(#muc_registered{
us_host = {LUS, Host},
us_host = {LUS, ServiceOrRoom},
nick = Nick}),
ok;
true ->

View File

@ -501,6 +501,8 @@ normal_state({route, <<"">>,
process_iq_captcha(From, IQ, StateData);
#adhoc_command{} ->
process_iq_adhoc(From, IQ, StateData);
#register{} ->
mod_muc:process_iq_register(IQ);
#fasten_apply_to{} = ApplyTo ->
case xmpp:get_subtag(ApplyTo, #message_moderate{}) of
#message_moderate{} = Moderate ->
@ -1406,7 +1408,7 @@ do_process_presence(Nick, #presence{from = From, type = available, lang = Lang}
true ->
case {nick_collision(From, Nick, StateData),
mod_muc:can_use_nick(StateData#state.server_host,
StateData#state.host,
jid:encode(StateData#state.jid),
From, Nick),
{(StateData#state.config)#config.allow_visitor_nickchange,
is_visitor(From, StateData)}} of
@ -2290,7 +2292,7 @@ add_new_user(From, Nick, Packet, StateData) ->
andalso NConferences < MaxConferences),
Collision,
mod_muc:can_use_nick(StateData#state.server_host,
StateData#state.host, From, Nick),
jid:encode(StateData#state.jid), From, Nick),
get_occupant_initial_role(From, Affiliation, StateData)}
of
{false, _, _, _} when NUsers >= MaxUsers orelse NUsers >= MaxAdminUsers ->
@ -4385,8 +4387,10 @@ maybe_forget_room(StateData) ->
end).
-spec make_disco_info(jid(), state()) -> disco_info().
make_disco_info(_From, StateData) ->
make_disco_info(From, StateData) ->
Config = StateData#state.config,
ServerHost = StateData#state.server_host,
AccessRegister = mod_muc_opt:access_register(ServerHost),
Feats = [?NS_VCARD, ?NS_MUC, ?NS_DISCO_INFO, ?NS_DISCO_ITEMS,
?NS_COMMANDS, ?NS_MESSAGE_MODERATE, ?NS_MESSAGE_RETRACT,
?CONFIG_OPT_TO_FEATURE((Config#config.public),
@ -4401,6 +4405,10 @@ make_disco_info(_From, StateData) ->
<<"muc_moderated">>, <<"muc_unmoderated">>),
?CONFIG_OPT_TO_FEATURE((Config#config.password_protected),
<<"muc_passwordprotected">>, <<"muc_unsecured">>)]
++ case acl:match_rule(ServerHost, AccessRegister, From) of
allow -> [?NS_REGISTER];
deny -> []
end
++ case Config#config.allow_subscription of
true -> [?NS_MUCSUB];
false -> []
@ -4703,7 +4711,7 @@ process_iq_mucsub(From,
{error, xmpp:err_conflict(ErrText, Lang)};
false ->
case mod_muc:can_use_nick(StateData#state.server_host,
StateData#state.host,
jid:encode(StateData#state.jid),
From, Nick) of
false ->
Err = case Nick of

View File

@ -159,13 +159,19 @@ forget_room(LServer, Host, Name) ->
end,
ejabberd_sql:sql_transaction(LServer, F).
can_use_nick(LServer, Host, JID, Nick) ->
can_use_nick(LServer, ServiceOrRoom, JID, Nick) ->
SJID = jid:encode(jid:tolower(jid:remove_resource(JID))),
case catch ejabberd_sql:sql_query(
LServer,
?SQL("select @(jid)s from muc_registered "
"where nick=%(Nick)s"
" and host=%(Host)s")) of
SqlQuery = case (jid:decode(ServiceOrRoom))#jid.lserver of
ServiceOrRoom ->
?SQL("select @(jid)s from muc_registered "
"where nick=%(Nick)s"
" and host=%(ServiceOrRoom)s");
Service ->
?SQL("select @(jid)s from muc_registered "
"where nick=%(Nick)s"
" and (host=%(ServiceOrRoom)s or host=%(Service)s)")
end,
case catch ejabberd_sql:sql_query(LServer, SqlQuery) of
{selected, [{SJID1}]} -> SJID == SJID1;
_ -> true
end.
@ -258,28 +264,57 @@ get_nick(LServer, Host, From) ->
_ -> error
end.
set_nick(LServer, Host, From, Nick) ->
set_nick(LServer, ServiceOrRoom, From, Nick) ->
JID = jid:encode(jid:tolower(jid:remove_resource(From))),
F = fun () ->
case Nick of
<<"">> ->
ejabberd_sql:sql_query_t(
?SQL("delete from muc_registered where"
" jid=%(JID)s and host=%(Host)s")),
" jid=%(JID)s and host=%(ServiceOrRoom)s")),
ok;
_ ->
Allow = case ejabberd_sql:sql_query_t(
?SQL("select @(jid)s from muc_registered"
" where nick=%(Nick)s"
" and host=%(Host)s")) of
{selected, [{J}]} -> J == JID;
_ -> true
Service = (jid:decode(ServiceOrRoom))#jid.lserver,
SqlQuery = case (ServiceOrRoom == Service) of
true ->
?SQL("select @(jid)s, @(host)s from muc_registered "
"where nick=%(Nick)s"
" and host=%(ServiceOrRoom)s");
false ->
?SQL("select @(jid)s, @(host)s from muc_registered "
"where nick=%(Nick)s"
" and (host=%(ServiceOrRoom)s or host=%(Service)s)")
end,
Allow = case ejabberd_sql:sql_query_t(SqlQuery) of
{selected, []}
when (ServiceOrRoom == Service) ->
%% Registering in the service...
%% check if nick is registered for some room in this service
{selected, NickRegistrations} =
ejabberd_sql:sql_query_t(
?SQL("select @(jid)s, @(host)s from muc_registered "
"where nick=%(Nick)s")),
not lists:any(fun({_NRJid, NRServiceOrRoom}) ->
Service == (jid:decode(NRServiceOrRoom))#jid.lserver end,
NickRegistrations);
{selected, []} ->
%% Nick not registered in any service or room
true;
{selected, [{_J, Host}]}
when (Host == Service) and (ServiceOrRoom /= Service) ->
%% Registering in a room, but the nick is already registered in the service
false;
{selected, [{J, _Host}]} ->
%% Registering in room (or service) a nick that is
%% already registered in this room (or service)
%% Only the owner of this registration can use the nick
J == JID
end,
if Allow ->
?SQL_UPSERT_T(
"muc_registered",
["!jid=%(JID)s",
"!host=%(Host)s",
"!host=%(ServiceOrRoom)s",
"server_host=%(LServer)s",
"nick=%(Nick)s"]),
ok;