From f8a01ae3b896c5c7be9da050728e07412e3bae2b Mon Sep 17 00:00:00 2001 From: Michael Uvarov Date: Wed, 14 Mar 2018 18:06:55 +0100 Subject: [PATCH] Separated database logic from mod_muc into backend modules #1758 Separated database logic from mod_muc into backend modules Add load_permanent_rooms_at_startup option New result format for some mod_muc functions Document mod_muc.backend option Remove mnesia migrations from mod_muc Replace catch with try..catch Simplify mod_muc Mnesia code Subtruct unset_nick from set_nick function Do not allow to use a nick if DB returns an error Add MUC registration tests Test registration over S2S Move generic s2s code into s2s_helper Use s2s_helper in muc_SUITE Disable fed1 cover for MUC-over-S2S tests Increase timeout for hibernation tests Add muc_SUITE:can_found_in_db_when_stopped testcase/1 Replace issue= with event= (we've decided to use event= everywhere!) Check that Nick is not empty in mod_muc:set_nick Return ok | {error,_} from store_room/4 Return ok | {error,_} from forget_room callback Do not create room if we have any backend issues Return {ok, Rooms}|{error,_} from mod_muc_db:get_rooms/1 Fix muc_SUITE to use updated mod_muc API --- .travis.yml | 4 + doc/modules/mod_muc.md | 2 + include/mod_muc.hrl | 13 + src/mod_muc.erl | 320 +++++++++--------- src/mod_muc_db.erl | 48 +++ src/mod_muc_db_mnesia.erl | 184 ++++++++++ src/mod_muc_room.erl | 34 +- .../ejabberd_tests/tests/muc_SUITE.erl | 219 ++++++++++-- .../ejabberd_tests/tests/muc_helper.erl | 7 + .../ejabberd_tests/tests/s2s_SUITE.erl | 115 +------ .../ejabberd_tests/tests/s2s_helper.erl | 134 ++++++++ 11 files changed, 785 insertions(+), 295 deletions(-) create mode 100644 include/mod_muc.hrl create mode 100644 src/mod_muc_db.erl create mode 100644 src/mod_muc_db_mnesia.erl create mode 100644 test.disabled/ejabberd_tests/tests/s2s_helper.erl diff --git a/.travis.yml b/.travis.yml index 30510cb5727..8c8ee5c9c53 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,10 @@ addons: - slapd - ldap-utils - golang # used for google drive test results upload + hosts: + # travis tries to resolve muc.localhost and fails + # used in MUC + s2s tests combination + - muc.localhost before_install: - tools/configure $REL_CONFIG install: diff --git a/doc/modules/mod_muc.md b/doc/modules/mod_muc.md index 6e123baf0ae..67d8efa4381 100644 --- a/doc/modules/mod_muc.md +++ b/doc/modules/mod_muc.md @@ -8,6 +8,7 @@ Also `mod_muc_log` is a logging submodule. ### Options * `host` (string, default: `"conference.@HOST@"`): Subdomain for MUC service to reside under. `@HOST@` is replaced with each served domain. +* `backend` (atom, default: `mnesia`): Storage backend. Currently only `mnesia` is supported. * `access` (atom, default: `all`): Access Rule to determine who is allowed to use the MUC service. * `access_create` (atom, default: `all`): Who is allowed to create rooms. * `access_admin` (atom, default: `none`): Who is the administrator in all rooms. @@ -27,6 +28,7 @@ Also `mod_muc_log` is a logging submodule. * `user_presence_shaper` (atom, default: `none`): Shaper for user presences processed by a room (global for the room). * `max_user_conferences` (non-negative, default: 10): Specifies the number of rooms that a user can occupy simultaneously. * `http_auth_pool` (atom, default: `none`): If an external HTTP service is chosen to check passwords for password-protected rooms, this option specifies the HTTP pool name to use (see [External HTTP Authentication](#external-http-authentication) below). +* `load_permanent_rooms_at_startup` (boolean, default: false) - Load all rooms at startup (can be unsafe when there are many rooms, that's why disabled). * `hibernate_timeout` (timeout, default: `90000`): Timeout (in milliseconds) defining the inactivity period after which the room's process should be hibernated. * `hibernated_room_check_interval` (timeout, default: `infinity`): Interval defining how often the hibernated rooms will be checked (a timer is global for a node). * `hibernated_room_timeout` (timeout, default: `inifitniy`): A time after which a hibernated room is stopped (deeply hibernated). diff --git a/include/mod_muc.hrl b/include/mod_muc.hrl new file mode 100644 index 00000000000..3fd9eb38153 --- /dev/null +++ b/include/mod_muc.hrl @@ -0,0 +1,13 @@ +-record(muc_room, { + name_host, + opts + }). + +-record(muc_online_room, {name_host, + pid + }). + +-record(muc_registered, { + us_host, + nick + }). diff --git a/src/mod_muc.erl b/src/mod_muc.erl index 68f91ab24ef..3fc6fdd98c9 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -36,13 +36,13 @@ start/2, stop/1, room_destroyed/3, - store_room/3, - restore_room/2, - forget_room/2, + store_room/4, + restore_room/3, + forget_room/3, create_instant_room/5, process_iq_disco_items/4, broadcast_service_message/2, - can_use_nick/3, + can_use_nick/4, room_jid_to_pid/1, default_host/0]). @@ -89,31 +89,18 @@ Packet :: packet()}. -type access() :: {_AccessRoute, _AccessCreate, _AccessAdmin, _AccessPersistent}. --record(muc_room, { - name_host, - opts -}). +-include("mod_muc.hrl"). -type muc_room() :: #muc_room{ name_host :: room_host(), opts :: list() }. --record(muc_online_room, { - name_host, - pid -}). - -type muc_online_room() :: #muc_online_room{ name_host :: room_host(), pid :: pid() }. --record(muc_registered, { - us_host, - nick -}). - -type muc_registered() :: #muc_registered{ us_host :: jid:literal_jid(), nick :: nick() @@ -155,6 +142,9 @@ start_link(Host, Opts) -> {'error', _} | {'ok', 'undefined' | pid()} | {'ok', 'undefined' | pid(), _}. start(Host, Opts) -> ensure_metrics(Host), + TrackedDBFuns = [store_room, restore_room, forget_room, get_rooms, + can_use_nick, get_nick, set_nick, unset_nick], + gen_mod:start_backend_module(mod_muc_db, Opts, TrackedDBFuns), start_supervisor(Host), Proc = gen_mod:get_module_proc(Host, ?PROCNAME), ChildSpec = @@ -204,36 +194,36 @@ create_instant_room(Host, Name, From, Nick, Opts) -> gen_server:call(Proc, {create_instant, Name, From, Nick, Opts}). --spec store_room(jid:server(), room(), list()) - -> {'aborted', _} | {'atomic', _}. -store_room(Host, Name, Opts) -> - F = fun() -> - mnesia:write(#muc_room{name_host = {Name, Host}, - opts = Opts}) - end, - mnesia:transaction(F). - - --spec restore_room(jid:server(), room()) - -> 'error' | 'undefined' | [any()]. -restore_room(Host, Name) -> - case catch mnesia:dirty_read(muc_room, {Name, Host}) of - [#muc_room{opts = Opts}] -> - Opts; +-spec store_room(jid:server(), jid:server(), room(), list()) -> + {error, _} | ok. +store_room(ServerHost, Host, Name, Opts) -> + mod_muc_db_backend:store_room(ServerHost, Host, Name, Opts). + + +-spec restore_room(jid:server(), jid:server(), room()) -> + {error, _} | {ok, _}. +restore_room(ServerHost, Host, Name) -> + mod_muc_db_backend:restore_room(ServerHost, Host, Name). + +-spec forget_room(jid:server(), jid:server(), room()) -> ok | {error, term()}. +forget_room(ServerHost, Host, Name) -> + %% Removes room from DB, even if it's already removed. + Result = mod_muc_db_backend:forget_room(ServerHost, Host, Name), + case Result of + ok -> + %% TODO this hook should be refactored to be executed on ServerHost, not Host. + %% It also should be renamed to forget_room_hook. + %% We also need to think how to remove stopped rooms + %% (i.e. in case we want to expose room removal over REST or SQS). + %% + %% In some _rare_ cases this hook can be called more than once for the same room. + ejabberd_hooks:run(forget_room, Host, [Host, Name]); _ -> - error - end. - - --spec forget_room(jid:server(), room()) -> 'ok'. -forget_room(Host, Name) -> - F = fun() -> - mnesia:delete({muc_room, {Name, Host}}) - end, - mnesia:transaction(F), - ejabberd_hooks:run(forget_room, Host, [Host, Name]), - ok. - + %% Room is not removed or we don't know. + %% XXX Handle this case better. + ok + end, + Result. -spec process_iq_disco_items(Host :: jid:server(), From :: jid:jid(), To :: jid:jid(), jlib:iq()) -> mongoose_acc:t(). @@ -248,26 +238,22 @@ process_iq_disco_items(Host, From, To, #iq{lang = Lang} = IQ) -> jlib:iq_to_xml(Res)). --spec can_use_nick(jid:server(), jid:jid(), nick()) -> boolean(). -can_use_nick(_Host, _JID, <<>>) -> +-spec can_use_nick(jid:server(), jid:server(), jid:jid(), nick()) -> boolean(). +can_use_nick(_ServerHost, _Host, _JID, <<>>) -> false; -can_use_nick(Host, JID, Nick) -> - {LUser, LServer, _} = jid:to_lower(JID), - LUS = {LUser, LServer}, - case catch mnesia:dirty_select( - muc_registered, - [{#muc_registered{us_host = '$1', - nick = Nick, - _ = '_'}, - [{'==', {element, 2, '$1'}, Host}], - ['$_']}]) of - {'EXIT', _Reason} -> - true; - [] -> - true; - [#muc_registered{us_host = {U, _Host}}] -> - U == LUS - end. +can_use_nick(ServerHost, Host, JID, Nick) -> + mod_muc_db_backend:can_use_nick(ServerHost, Host, JID, Nick). + +set_nick(LServer, Host, From, <<>>) -> + {error, should_not_be_empty}; +set_nick(LServer, Host, From, Nick) -> + mod_muc_db_backend:set_nick(LServer, Host, From, Nick). + +unset_nick(LServer, Host, From) -> + mod_muc_db_backend:unset_nick(LServer, Host, From). + +get_nick(LServer, Host, From) -> + mod_muc_db_backend:get_nick(LServer, Host, From). %%==================================================================== %% gen_server callbacks @@ -282,22 +268,14 @@ can_use_nick(Host, JID, Nick) -> %%-------------------------------------------------------------------- -spec init([jid:server() | list(), ...]) -> {'ok', state()}. init([Host, Opts]) -> - mnesia:create_table(muc_room, - [{disc_copies, [node()]}, - {attributes, record_info(fields, muc_room)}]), - mnesia:create_table(muc_registered, - [{disc_copies, [node()]}, - {attributes, record_info(fields, muc_registered)}]), + mod_muc_db_backend:init(Host, Opts), mnesia:create_table(muc_online_room, [{ram_copies, [node()]}, {attributes, record_info(fields, muc_online_room)}]), mnesia:add_table_copy(muc_online_room, node(), ram_copies), - mnesia:add_table_copy(muc_room, node(), disc_copies), - mnesia:add_table_copy(muc_registered, node(), disc_copies), catch ets:new(muc_online_users, [bag, named_table, public, {keypos, 2}]), MyHost = gen_mod:get_opt_subhost(Host, Opts, default_host()), clean_table_from_bad_node(node(), MyHost), - mnesia:add_table_index(muc_registered, nick), mnesia:subscribe(system), Access = gen_mod:get_opt(access, Opts, all), AccessCreate = gen_mod:get_opt(access_create, Opts, all), @@ -330,9 +308,17 @@ init([Host, Opts]) -> ejabberd_router:register_route(MyHost, mongoose_packet_handler:new(?MODULE, State)), mongoose_subhosts:register(Host, MyHost), - load_permanent_rooms(MyHost, Host, - {Access, AccessCreate, AccessAdmin, AccessPersistent}, - HistorySize, RoomShaper, HttpAuthPool), + case gen_mod:get_module_opt(Host, mod_muc, load_permanent_rooms_at_startup, false) of + false -> + ?INFO_MSG("event=load_permanent_rooms_at_startup, skip=true, " + "details=\"each room is loaded when someone access the room\"", []); + true -> + ?INFO_MSG("event=load_permanent_rooms_at_startup, skip=false, " + "details=\"it can take some time\"", []), + load_permanent_rooms(MyHost, Host, + {Access, AccessCreate, AccessAdmin, AccessPersistent}, + HistorySize, RoomShaper, HttpAuthPool) + end, set_persistent_rooms_timer(State), {ok, State}. @@ -549,10 +535,27 @@ get_registered_room_or_route_error_from_presence(Room, From, To, Acc, Packet, http_auth_pool = HttpAuthPool, default_room_opts = DefRoomOpts} = State, {_, _, Nick} = jid:to_lower(To), - {ok, Pid} = start_new_room(Host, ServerHost, Access, Room, + Result = start_new_room(Host, ServerHost, Access, Room, HistorySize, RoomShaper, HttpAuthPool, From, Nick, DefRoomOpts), - register_room_or_stop_if_duplicate(Host, Room, Pid); + case Result of + {ok, Pid} -> + register_room_or_stop_if_duplicate(Host, Room, Pid); + {error, {failed_to_restore, Reason}} -> + %% Notify user about our backend module error + ?WARNING_MSG("event=send_service_unavailable room=~ts reason=~p", + [Room, Reason]), + Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>), + ErrText = <<"Service is temporary unavailable">>, + Err = jlib:make_error_reply( + Packet, mongoose_xmpp_errors:service_unavailable(Lang, ErrText)), + ejabberd_router:route(To, From, Acc, Err), + {route_error, ErrText}; + _ -> + %% Unknown error, most likely a room process failed to start. + %% Do not notify user (we can send "internal server error"). + erlang:error({start_new_room_failed, Room, Result}) + end; false -> Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>), ErrText = <<"Room creation is denied by service policy">>, @@ -562,20 +565,29 @@ get_registered_room_or_route_error_from_presence(Room, From, To, Acc, Packet, {route_error, ErrText} end. -get_registered_room_or_route_error_from_packet(Room, From, To, _Acc, Packet, +get_registered_room_or_route_error_from_packet(Room, From, To, Acc, Packet, #state{server_host = ServerHost, host = Host, access = Access} = State) -> - case restore_room(Host, Room) of - error -> + case restore_room(ServerHost, Host, Room) of + {error, room_not_found} -> Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>), ErrText = <<"Conference room does not exist">>, Err = jlib:make_error_reply( Packet, mongoose_xmpp_errors:item_not_found(Lang, ErrText)), ejabberd_router:route(To, From, Err), {route_error, ErrText}; - Opts -> + {error, Reason} -> + ?WARNING_MSG("event=send_service_unavailable room=~ts reason=~p", + [Room, Reason]), + Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>), + ErrText = <<"Service is temporary unavailable">>, + Err = jlib:make_error_reply( + Packet, mongoose_xmpp_errors:service_unavailable(Lang, ErrText)), + ejabberd_router:route(To, From, Acc, Err), + {route_error, ErrText}; + {ok, Opts} -> ?DEBUG("MUC: restore room '~s'~n", [Room]), #state{history_size = HistorySize, room_shaper = RoomShaper, @@ -618,16 +630,17 @@ route_by_type(<<"iq">>, {From, To, _Acc, Packet}, #state{host = Host} = State) - #iq{type = get, xmlns = ?NS_DISCO_ITEMS} = IQ -> spawn(?MODULE, process_iq_disco_items, [Host, From, To, IQ]); #iq{type = get, xmlns = ?NS_REGISTER = XMLNS, lang = Lang} = IQ -> + Result = iq_get_register_info(ServerHost, Host, From, Lang), Res = IQ#iq{type = result, sub_el = [#xmlel{name = <<"query">>, attrs = [{<<"xmlns">>, XMLNS}], - children = iq_get_register_info(Host, From, Lang)}]}, + children = Result}]}, ejabberd_router:route(To, From, jlib:iq_to_xml(Res)); #iq{type = set, xmlns = ?NS_REGISTER = XMLNS, lang = Lang, sub_el = SubEl} = IQ -> - case process_iq_register_set(Host, From, SubEl, Lang) of + case process_iq_register_set(ServerHost, Host, From, SubEl, Lang) of {result, IQRes} -> Res = IQ#iq{type = result, sub_el = [#xmlel{name = <<"query">>, @@ -651,9 +664,13 @@ route_by_type(<<"iq">>, {From, To, _Acc, Packet}, #state{host = Host} = State) - children = [iq_get_unique(From)]}]}, ejabberd_router:route(To, From, jlib:iq_to_xml(Res)); #iq{} -> + ?INFO_MSG("event=ignore_unknown_iq from=~ts to=~ts packet=~1000p", + [jid:to_binary(From), jid:to_binary(To), exml:to_binary(Packet)]), Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:feature_not_implemented()), ejabberd_router:route(To, From, Err); _ -> + ?INFO_MSG("event=failed_to_parse_iq from=~ts to=~ts packet=~1000p", + [jid:to_binary(From), jid:to_binary(To), exml:to_binary(Packet)]), ok end; route_by_type(<<"message">>, {From, To, _Acc, Packet}, @@ -696,15 +713,13 @@ check_user_can_create_room(ServerHost, AccessCreate, From, RoomID) -> RoomShaper :: shaper:shaper(), HttpAuthPool :: none | mongoose_http_client:pool()) -> 'ok'. load_permanent_rooms(Host, ServerHost, Access, HistorySize, RoomShaper, HttpAuthPool) -> RoomsToLoad = - case catch mnesia:dirty_select( - muc_room, [{#muc_room{name_host = {'_', Host}, _ = '_'}, - [], - ['$_']}]) of - {'EXIT', Reason} -> - ?ERROR_MSG("~p", [Reason]), - []; - Rs -> - Rs + case mod_muc_db_backend:get_rooms(ServerHost, Host) of + {ok, Rs} -> + Rs; + {error, Reason} -> + ?ERROR_MSG("event=get_rooms_failed event=skip_load_permanent_rooms reason=~p", + [Reason]), + [] end, lists:foreach( fun(R) -> @@ -733,14 +748,16 @@ load_permanent_rooms(Host, ServerHost, Access, HistorySize, RoomShaper, HttpAuth start_new_room(Host, ServerHost, Access, Room, HistorySize, RoomShaper, HttpAuthPool, From, Nick, DefRoomOpts) -> - case mnesia:dirty_read(muc_room, {Room, Host}) of - [] -> + case mod_muc_db_backend:restore_room(ServerHost, Host, Room) of + {error, room_not_found} -> ?DEBUG("MUC: open new room '~s'~n", [Room]), mod_muc_room:start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, HttpAuthPool, From, Nick, DefRoomOpts); - [#muc_room{opts = Opts}|_] -> + {error, Reason} -> + {error, {failed_to_restore, Reason}}; + {ok, Opts} -> ?DEBUG("MUC: restore room '~s'~n", [Room]), mod_muc_room:start(Host, ServerHost, Access, Room, HistorySize, @@ -921,21 +938,19 @@ iq_get_unique(From) -> randoms:get_string()]))}. --spec iq_get_register_info('undefined' | jid:server(), +-spec iq_get_register_info(jid:server(), jid:server(), jid:simple_jid() | jid:jid(), ejabberd:lang()) - -> [exml:element(), ...]. -iq_get_register_info(Host, From, Lang) -> - {LUser, LServer, _} = jid:to_lower(From), - LUS = {LUser, LServer}, + -> [jlib:xmlel(), ...]. +iq_get_register_info(ServerHost, Host, From, Lang) -> {Nick, Registered} = - case catch mnesia:dirty_read(muc_registered, {LUS, Host}) of - {'EXIT', _Reason} -> - {<<>>, []}; - [] -> - {<<>>, []}; - [#muc_registered{nick = N}] -> - {N, [#xmlel{name = <<"registered">>}]} - end, + case catch get_nick(ServerHost, Host, From) of + {'EXIT', _Reason} -> + {<<>>, []}; + {error, _} -> + {<<>>, []}; + {ok, N} -> + {N, [#xmlel{name = <<"registered">>}]} + end, ClientReqText = translate:translate( Lang, <<"You need a client that supports x:data to register the nickname">>), ClientReqEl = #xmlel{name = <<"instructions">>, @@ -954,71 +969,66 @@ iq_get_register_info(Host, From, Lang) -> xfield(<<"text-single">>, <<"Nickname">>, <<"nick">>, Nick, Lang)]}]. --spec iq_set_register_info(jid:server(), +-spec iq_set_register_info(jid:server(), jid:server(), jid:simple_jid() | jid:jid(), nick(), ejabberd:lang()) - -> {'error', exml:element()} | {'result', []}. -iq_set_register_info(Host, From, Nick, Lang) -> - {LUser, LServer, _} = jid:to_lower(From), - LUS = {LUser, LServer}, - case mnesia:transaction(iq_set_register_info_t(Host, LUS, Nick)) of - {atomic, ok} -> + -> {'error', jlib:xmlel()} | {'result', []}. +iq_set_register_info(ServerHost, Host, From, Nick, Lang) -> + case set_nick(ServerHost, Host, From, Nick) of + ok -> {result, []}; - {atomic, false} -> + {error, conflict} -> ErrText = <<"That nickname is registered by another person">>, {error, mongoose_xmpp_errors:conflict(Lang, ErrText)}; - _ -> + {error, should_not_be_empty} -> + ErrText = <<"You must fill in field \"Nickname\" in the form">>, + {error, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)}; + {error, ErrorReason} -> + ?ERROR_MSG("event=iq_set_register_info_failed, " + "jid=~ts, nick=~p, reason=~p", + [jid:to_binary(From), Nick, ErrorReason]), {error, mongoose_xmpp_errors:internal_server_error()} end. --spec iq_set_register_info_t(Host :: jid:server(), LUS :: jid:simple_bare_jid(), - Nick :: binary()) -> fun(() -> ok | false). -iq_set_register_info_t(Host, LUS, <<>>) -> - fun() -> - mnesia:delete({muc_registered, {LUS, Host}}), - ok - end; -iq_set_register_info_t(Host, LUS, Nick) -> - Allow = - case mnesia:select(muc_registered, - [{#muc_registered{us_host = '$1', nick = Nick, _ = '_'}, - [{'==', {element, 2, '$1'}, Host}], - ['$_']}]) of - [] -> - true; - [#muc_registered{us_host = {U, _Host}}] -> - U == LUS - end, - case Allow of - true -> - mnesia:write(#muc_registered{us_host = {LUS, Host}, nick = Nick}), - ok; - false -> - false +-spec iq_set_unregister_info(jid:server(), jid:server(), + jid:simple_jid() | jid:jid(), ejabberd:lang()) + -> {'error', jlib:xmlel()} | {'result', []}. +iq_set_unregister_info(ServerHost, Host, From, _Lang) -> + case unset_nick(ServerHost, Host, From) of + ok -> + {result, []}; + {error, ErrorReason} -> + ?ERROR_MSG("event=iq_set_unregister_info_failed, " + "jid=~ts, reason=~p", + [jid:to_binary(From), ErrorReason]), + {error, mongoose_xmpp_errors:internal_server_error()} end. --spec process_iq_register_set(jid:server(),jid:jid(), exml:element(), ejabberd:lang()) -> - {error, exml:element()} | {result, []}. -process_iq_register_set(Host, From, #xmlel{ children = Els } = SubEl, Lang) -> +-spec process_iq_register_set(jid:server(), jid:server(), + jid:jid(), jlib:xmlel(), ejabberd:lang()) + -> {'error', jlib:xmlel()} | {'result', []}. +process_iq_register_set(ServerHost, Host, From, SubEl, Lang) -> + #xmlel{children = Els} = SubEl, case xml:get_subtag(SubEl, <<"remove">>) of false -> case xml:remove_cdata(Els) of [#xmlel{name = <<"x">>} = XEl] -> process_register(xml:get_tag_attr_s(<<"xmlns">>, XEl), xml:get_tag_attr_s(<<"type">>, XEl), - Host, From, Lang, XEl); + ServerHost, Host, From, Lang, XEl); _ -> {error, mongoose_xmpp_errors:bad_request()} end; _ -> - iq_set_register_info(Host, From, <<>>, Lang) + iq_set_unregister_info(ServerHost, Host, From, Lang) end. --spec process_register(XMLNS :: binary(), Type :: binary(), Host :: jid:server(), - From ::jid:jid(), Lang :: ejabberd:lang(), XEl :: exml:element()) -> +-spec process_register(XMLNS :: binary(), Type :: binary(), + ServerHost :: jid:server(), Host :: jid:server(), + From :: jid:jid(), Lang :: ejabberd:lang(), XEl :: exml:element()) -> {error, exml:element()} | {result, []}. -process_register(?NS_XDATA, <<"cancel">>, _Host, _From, _Lang, _XEl) -> +process_register(?NS_XDATA, <<"cancel">>, _ServerHost, _Host, _From, _Lang, _XEl) -> {result, []}; -process_register(?NS_XDATA, <<"submit">>, Host, From, Lang, XEl) -> +process_register(?NS_XDATA, <<"submit">>, ServerHost, Host, From, Lang, XEl) -> XData = jlib:parse_xdata_submit(XEl), case XData of invalid -> @@ -1026,13 +1036,13 @@ process_register(?NS_XDATA, <<"submit">>, Host, From, Lang, XEl) -> _ -> case lists:keysearch(<<"nick">>, 1, XData) of {value, {_, [Nick]}} when Nick /= <<>> -> - iq_set_register_info(Host, From, Nick, Lang); + iq_set_register_info(ServerHost, Host, From, Nick, Lang); _ -> ErrText = <<"You must fill in field \"Nickname\" in the form">>, {error, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)} end end; -process_register(_, _, _Host, _From, _Lang, _XEl) -> +process_register(_, _, _ServerHost, _Host, _From, _Lang, _XEl) -> {error, mongoose_xmpp_errors:bad_request()}. -spec iq_get_vcard(ejabberd:lang()) -> [exml:element(), ...]. diff --git a/src/mod_muc_db.erl b/src/mod_muc_db.erl new file mode 100644 index 00000000000..e644dbfef9e --- /dev/null +++ b/src/mod_muc_db.erl @@ -0,0 +1,48 @@ +-module(mod_muc_db). +-include("mod_muc.hrl"). + +%% Defines which ODBC pool to use +%% Parent host of the MUC service +-type server_host() :: ejabberd:server(). + +%% Host of MUC service +-type muc_host() :: ejabberd:server(). + +%% User's JID. Can be on another domain accessable over FED. +%% Only bare part (user@host) is important. +-type client_jid() :: ejabberd:jid(). + +-type room_opts() :: [{OptionName :: atom(), OptionValue :: term()}]. + + + +%% Called when MUC service starts or restarts for each domain +-callback init(server_host(), ModuleOpts :: list()) -> ok. + +-callback store_room(server_host(), muc_host(), mod_muc:room(), room_opts()) -> + ok | {error, term()}. + +-callback restore_room(server_host(), muc_host(), mod_muc:room()) -> + {ok, room_opts()} | {error, room_not_found} | {error, term()}. + +-callback forget_room(server_host(), muc_host(), mod_muc:room()) -> + ok | {error, term()}. + +-callback can_use_nick(server_host(), muc_host(), + client_jid(), mod_muc:nick()) -> boolean(). + +-callback get_rooms(server_host(), muc_host()) -> + {ok, [#muc_room{}]} | {error, term()}. + +%% Get nick associated with jid client_jid() across muc_host() domain +-callback get_nick(server_host(), muc_host(), client_jid()) -> + {ok, mod_muc:nick()} | {error, not_registered} | {error, term()}. + +%% Register nick +-callback set_nick(server_host(), muc_host(), client_jid(), mod_muc:nick()) -> + ok | {error, conflict} | {error, term()}. + +%% Unregister nick +%% Unregistered nicks can be used by someone else +-callback unset_nick(server_host(), muc_host(), client_jid()) -> + ok | {error, term()}. diff --git a/src/mod_muc_db_mnesia.erl b/src/mod_muc_db_mnesia.erl new file mode 100644 index 00000000000..46b4fa958a3 --- /dev/null +++ b/src/mod_muc_db_mnesia.erl @@ -0,0 +1,184 @@ +%%%---------------------------------------------------------------------- +%%% Based on file mod_muc. +%%% +%%% Original header and copyright notice: +%%% +%%% Author : Alexey Shchepin +%%% Purpose : MUC support (XEP-0045) +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2011 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +-module(mod_muc_db_mnesia). +-behaviour(mod_muc_db). +-export([init/2, + store_room/4, + restore_room/3, + forget_room/3, + get_rooms/2, + can_use_nick/4, + get_nick/3, + set_nick/4, + unset_nick/3]). + +-include("mongoose.hrl"). +-include("jlib.hrl"). +-include("mod_muc.hrl"). + +init(_Host, _Opts) -> + mnesia:create_table(muc_room, + [{disc_copies, [node()]}, + {attributes, record_info(fields, muc_room)}]), + mnesia:create_table(muc_registered, + [{disc_copies, [node()]}, + {attributes, record_info(fields, muc_registered)}]), + mnesia:add_table_copy(muc_room, node(), disc_copies), + mnesia:add_table_copy(muc_registered, node(), disc_copies), + mnesia:add_table_index(muc_registered, nick), + ok. + +-spec store_room(ejabberd:server(), ejabberd:server(), mod_muc:room(), list()) + -> ok | {error, term()}. +store_room(_ServerHost, MucHost, RoomName, Opts) -> + F = fun() -> + mnesia:write(#muc_room{name_host = {RoomName, MucHost}, + opts = Opts}) + end, + Result = mnesia:transaction(F), + case Result of + {atomic, _} -> + ok; + _ -> + ?ERROR_MSG("event=store_room_failed room=~ts reason=~p", + [RoomName, Result]), + {error, Result} + end. + +restore_room(_ServerHost, MucHost, RoomName) -> + try mnesia:dirty_read(muc_room, {RoomName, MucHost}) of + [#muc_room{opts = Opts}] -> + {ok, Opts}; + [] -> + {error, room_not_found}; + Other -> + {error, Other} + catch Class:Reason -> + ?ERROR_MSG("event=restore_room_failed room=~ts reason=~p:~p", + [RoomName, Class, Reason]), + {error, {Class, Reason}} + end. + +-spec forget_room(ejabberd:server(), ejabberd:server(), mod_muc:room()) -> + ok | {error, term()}. +forget_room(_ServerHost, MucHost, RoomName) -> + F = fun() -> + mnesia:delete({muc_room, {RoomName, MucHost}}) + end, + Result = mnesia:transaction(F), + case Result of + {atomic, _} -> + ok; + _ -> + ?ERROR_MSG("event=forget_room_failed room=~ts reason=~p", + [RoomName, Result]), + {error, Result} + end. + +get_rooms(_Lserver, MucHost) -> + Query = [{#muc_room{name_host = {'_', MucHost}, _ = '_'}, + [], + ['$_']}], + try + {ok, mnesia:dirty_select(muc_room, Query)} + catch Class:Reason -> + ?ERROR_MSG("event=get_rooms_failed reason=~p:~p", + [Class, Reason]), + {error, {Class, Reason}} + end. + +-spec can_use_nick(ejabberd:server(), ejabberd:server(), + ejabberd:jid(), mod_muc:nick()) -> boolean(). +can_use_nick(_ServerHost, MucHost, JID, Nick) -> + LUS = jid:to_lus(JID), + can_use_nick_internal(MucHost, Nick, LUS). + +can_use_nick_internal(MucHost, Nick, LUS) -> + Query = [{#muc_registered{us_host = '$1', nick = Nick, _ = '_'}, + [{'==', {element, 2, '$1'}, MucHost}], + ['$_']}], + try mnesia:dirty_select(muc_registered, Query) of + [] -> + true; + [#muc_registered{us_host = {U, _Host}}] -> + U == LUS + catch Class:Reason -> + ?ERROR_MSG("event=can_use_nick_failed jid=~ts nick=~ts reason=~p:~p", + [jid:to_binary(LUS), Nick, Class, Reason]), + false + end. + +get_nick(_ServerHost, MucHost, From) -> + LUS = jid:to_lus(From), + try mnesia:dirty_read(muc_registered, {LUS, MucHost}) of + [] -> + {error, not_registered}; + [#muc_registered{nick = Nick}] -> + {ok, Nick} + catch Class:Reason -> + ?ERROR_MSG("event=get_nick_failed jid=~ts reason=~p:~p", + [jid:to_binary(From), Class, Reason]), + {error, {Class, Reason}} + end. + +set_nick(_ServerHost, MucHost, From, Nick) + when is_binary(Nick), Nick =/= <<>> -> + LUS = jid:to_lus(From), + F = fun () -> + case can_use_nick_internal(MucHost, Nick, LUS) of + true -> + Object = #muc_registered{us_host = {LUS, MucHost}, nick = Nick}, + mnesia:write(Object), + ok; + false -> + {error, conflict} + end + end, + case mnesia:transaction(F) of + {atomic, Result} -> + Result; + ErrorResult -> + ?ERROR_MSG("event=set_nick_failed jid=~ts nick=~ts reason=~1000p", + [jid:to_binary(From), Nick, ErrorResult]), + {error, ErrorResult} + end. + +unset_nick(_ServerHost, MucHost, From) -> + LUS = jid:to_lus(From), + F = fun () -> + mnesia:delete({muc_registered, {LUS, MucHost}}) + end, + case mnesia:transaction(F) of + {atomic, _} -> + ok; + ErrorResult -> + ?ERROR_MSG("event=unset_nick_failed jid=~ts reason=~1000p", + [jid:to_binary(From), ErrorResult]), + {error, ErrorResult} + end. diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index cd0811398d0..839e5acbc52 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -171,19 +171,22 @@ start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, HttpAuthPool, Opts :: list()) -> {'error', _} | {'ok', 'undefined' | pid()} | {'ok', 'undefined' | pid(), _}. -start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, HttpAuthPool, Opts) -> +start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, HttpAuthPool, Opts) + when is_list(Opts) -> Supervisor = gen_mod:get_module_proc(ServerHost, ejabberd_mod_muc_sup), supervisor:start_child(Supervisor, [Host, ServerHost, Access, Room, HistorySize, RoomShaper, HttpAuthPool, Opts]). start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, HttpAuthPool, - Creator, Nick, DefRoomOpts) -> + Creator, Nick, DefRoomOpts) + when is_list(DefRoomOpts) -> gen_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, RoomShaper, HttpAuthPool, Creator, Nick, DefRoomOpts], ?FSMOPTS). -start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, HttpAuthPool, Opts) -> +start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, HttpAuthPool, Opts) + when is_list(Opts) -> gen_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, RoomShaper, HttpAuthPool, Opts], @@ -252,7 +255,7 @@ can_access_identity(RoomJID, UserJID) -> -spec init([any(), ...]) -> {ok, statename(), state()} | {ok, statename(), state(), timeout()}. init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, HttpAuthPool, - Creator, _Nick, DefRoomOpts]) -> + Creator, _Nick, DefRoomOpts]) when is_list(DefRoomOpts) -> process_flag(trap_exit, true), Shaper = shaper:new(RoomShaper), State = set_affiliation(Creator, owner, @@ -922,7 +925,8 @@ change_subject_if_allowed(FromNick, Role, Packet, StateData) -> save_persistent_room_state(StateData) -> case (StateData#state.config)#config.persistent of true -> - mod_muc:store_room(StateData#state.host, + mod_muc:store_room(StateData#state.server_host, + StateData#state.host, StateData#state.room, make_opts(StateData)); _ -> @@ -1072,7 +1076,7 @@ process_presence_unavailable(From, Packet, StateData) -> -> 'allowed' | 'conflict_registered' | 'conflict_use' | 'not_allowed_visitor'. choose_nick_change_strategy(From, Nick, StateData) -> case {is_nick_exists(Nick, StateData), - mod_muc:can_use_nick(StateData#state.host, From, Nick), + mod_muc:can_use_nick(StateData#state.server_host, StateData#state.host, From, Nick), (StateData#state.config)#config.allow_visitor_nickchange, is_visitor(From, StateData)} of {_, _, false, true} -> @@ -1798,7 +1802,7 @@ choose_new_user_strategy(From, Nick, Affiliation, Role, Els, StateData) -> case {is_user_limit_reached(From, Affiliation, StateData), is_nick_exists(Nick, StateData), is_next_session_of_occupant(From, Nick, StateData), - mod_muc:can_use_nick(StateData#state.host, From, Nick), + mod_muc:can_use_nick(StateData#state.server_host, StateData#state.host, From, Nick), Role, Affiliation} of {false, _, _, _, _, _} -> @@ -2782,7 +2786,9 @@ process_admin_items_set(UJID, Items, Lang, StateData) -> process_admin_item_set(ChangedItem, UJID, SD) end, StateData, Res), case (NSD#state.config)#config.persistent of - true -> mod_muc:store_room(NSD#state.host, NSD#state.room, make_opts(NSD)); + true -> + mod_muc:store_room(NSD#state.server_host, + NSD#state.host, NSD#state.room, make_opts(NSD)); _ -> ok end, {result, [], NSD}; @@ -3705,9 +3711,9 @@ change_config(Config, StateData) -> case {(StateData#state.config)#config.persistent, Config#config.persistent} of {_, true} -> - mod_muc:store_room(NSD#state.host, NSD#state.room, make_opts(NSD)); + mod_muc:store_room(NSD#state.server_host, NSD#state.host, NSD#state.room, make_opts(NSD)); {true, false} -> - mod_muc:forget_room(NSD#state.host, NSD#state.room); + mod_muc:forget_room(NSD#state.server_host, NSD#state.host, NSD#state.room); {false, false} -> ok end, @@ -3835,7 +3841,9 @@ destroy_room(DestroyEl, StateData) -> remove_each_occupant_from_room(DestroyEl, StateData), case (StateData#state.config)#config.persistent of true -> - mod_muc:forget_room(StateData#state.host, StateData#state.room); + mod_muc:forget_room(StateData#state.server_host, + StateData#state.host, + StateData#state.room); false -> ok end, @@ -4532,9 +4540,9 @@ route_invitation({ok, _IJIDs}, _From, _Packet, _Lang, StateData0) -> StateData0. -spec store_room_if_persistent(state()) -> any(). -store_room_if_persistent(#state{ host = Host, room = Room, +store_room_if_persistent(#state{ host = Host, room = Room, server_host = ServerHost, config = #config{ persistent = true } } = StateData) -> - mod_muc:store_room(Host, Room, make_opts(StateData)); + mod_muc:store_room(ServerHost, Host, Room, make_opts(StateData)); store_room_if_persistent(_SD) -> ok. diff --git a/test.disabled/ejabberd_tests/tests/muc_SUITE.erl b/test.disabled/ejabberd_tests/tests/muc_SUITE.erl index 9f491dc76c7..10e9c5f7ae1 100644 --- a/test.disabled/ejabberd_tests/tests/muc_SUITE.erl +++ b/test.disabled/ejabberd_tests/tests/muc_SUITE.erl @@ -43,6 +43,7 @@ -define(PASSWORD, <<"pa5sw0rd">>). -define(SUBJECT, <<"subject">>). -define(WAIT_TIME, 1500). +-define(WAIT_TIMEOUT, 10000). -define(FAKEPID, fakepid). @@ -86,8 +87,11 @@ all() -> [ {group, room_management}, {group, http_auth_no_server}, {group, http_auth}, - {group, hibernation} -% {group, room_registration_race_condition} + {group, hibernation}, +% {group, room_registration_race_condition}, + {group, register}, + {group, register_over_s2s}, + {group, room_registration_race_condition} ]. groups() -> [ @@ -102,7 +106,8 @@ groups() -> [ stopped_members_only_room_process_invitations_correctly, room_with_participants_is_not_stopped, room_with_only_owner_is_stopped, - deep_hibernation_metrics_are_updated + deep_hibernation_metrics_are_updated, + can_found_in_db_when_stopped ]}, {disco, [parallel], [ disco_service, @@ -248,9 +253,19 @@ groups() -> [ create_already_registered_room, check_presence_route_to_offline_room, check_message_route_to_offline_room - ]} + ]}, + {register, [parallel], register_cases()}, + {register_over_s2s, [parallel], register_cases()} ]. +register_cases() -> + [user_asks_for_registration_form, + user_submits_registration_form, + user_submits_registration_form_twice, + user_changes_nick, + user_unregisters_nick, + user_unregisters_nick_twice]. + rsm_cases() -> [pagination_first5, pagination_last5, @@ -259,7 +274,7 @@ rsm_cases() -> pagination_empty_rset]. suite() -> - escalus:suite(). + s2s_helper:suite(escalus:suite()). %%-------------------------------------------------------------------- %% Init & teardown @@ -337,6 +352,12 @@ init_per_group(hibernation, Config) -> ok end, Config; +init_per_group(register_over_s2s, Config) -> + Config1 = s2s_helper:init_s2s(Config, false), + Config2 = s2s_helper:configure_s2s(both_plain, Config1), + [{_,AliceSpec2}|Others] = escalus:get_users([alice2, bob, kate]), + Users = [{alice,AliceSpec2}|Others], + escalus:create_users(Config2, Users); init_per_group(_GroupName, Config) -> escalus:create_users(Config, escalus:get_users([alice, bob, kate])). @@ -396,6 +417,9 @@ end_per_group(hibernation, Config) -> ok end, Config; +end_per_group(register_over_s2s, Config) -> + s2s_helper:end_s2s(Config), + escalus:delete_users(Config, escalus:get_users([alice2, bob, kate])); end_per_group(_GroupName, Config) -> escalus:delete_users(Config, escalus:get_users([alice, bob, kate])). @@ -2691,13 +2715,90 @@ one2one_chat_to_muc(Config) -> end). +%%-------------------------------------------------------------------- +%% Registration at a server +%%-------------------------------------------------------------------- + +%% You send the register IQ to room jid. +%% But in MongooseIM you need to send it to "muc@host". +user_asks_for_registration_form(Config) -> + escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> + ?assert_equal(<<>>, get_nick(Alice)) + end). + +%% Example 71. User Submits Registration Form +%% You send the register IQ to room jid "muc@host/room". +%% But in MongooseIM you need to send it to "muc@host". +user_submits_registration_form(Config) -> + escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> + Nick = fresh_nick_name(<<"thirdwitch">>), + set_nick(Alice, Nick), + ?assert_equal(Nick, get_nick(Alice)) + end). + +user_submits_registration_form_over_s2s(Config) -> + escalus:fresh_story(Config, [{alice2, 1}], fun(Alice) -> + Nick = fresh_nick_name(<<"thirdwitch">>), + set_nick(Alice, Nick), + ?assert_equal(Nick, get_nick(Alice)) + end). + +user_submits_registration_form_twice(Config) -> + escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> + Nick = fresh_nick_name(<<"thirdwitch">>), + set_nick(Alice, Nick), + ?assert_equal(Nick, get_nick(Alice)), + + set_nick(Alice, Nick), + ?assert_equal(Nick, get_nick(Alice)) + end). + +user_changes_nick(Config) -> + escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> + Nick1 = fresh_nick_name(<<"thirdwitch1">>), + Nick2 = fresh_nick_name(<<"thirdwitch2">>), + set_nick(Alice, Nick1), + ?assert_equal(Nick1, get_nick(Alice)), + + set_nick(Alice, Nick2), + ?assert_equal(Nick2, get_nick(Alice)) + end). + +user_unregisters_nick(Config) -> + escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> + Nick = fresh_nick_name(<<"thirdwitch1">>), + set_nick(Alice, Nick), + ?assert_equal(Nick, get_nick(Alice)), + + unset_nick(Alice), + ?assert_equal(<<>>, get_nick(Alice)) + end). + +user_unregisters_nick_twice(Config) -> + escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> + Nick = fresh_nick_name(<<"thirdwitch1">>), + set_nick(Alice, Nick), + ?assert_equal(Nick, get_nick(Alice)), + + unset_nick(Alice), + ?assert_equal(<<>>, get_nick(Alice)), + + unset_nick(Alice), + ?assert_equal(<<>>, get_nick(Alice)) + end). + + +%%-------------------------------------------------------------------- +%% Registration in a room +%%-------------------------------------------------------------------- + %%Examples 66-76 %%Registartion feature is not implemented -%%TODO: create a differend goruop for the registration test cases (they will fail) +%%TODO: create a differend group for the registration test cases (they will fail) %registration_request(Config) -> % escalus:story(Config, [{alice, 1}, {bob, 1}], fun(_Alice, Bob) -> % escalus:send(Bob, stanza_to_room(escalus_stanza:iq_get(<<"jabber:iq:register">>, []), ?config(room, Config))), -% print_next_message(Bob) +% print_next_message(Bob) % end). % %%Example 67 @@ -4028,7 +4129,7 @@ hibernated_room_is_stopped(Config) -> end), destroy_room(muc_host(), RoomName), - forget_room(muc_host(), RoomName). + forget_room(domain(), muc_host(), RoomName). hibernated_room_is_stopped_and_restored_by_presence(Config) -> RoomName = fresh_room_name(), @@ -4044,7 +4145,7 @@ hibernated_room_is_stopped_and_restored_by_presence(Config) -> ct:sleep(timer:seconds(1)), escalus:send(Bob, stanza_join_room(RoomName, <<"bob">>)), - Presence = escalus:wait_for_stanza(Bob), + Presence = escalus:wait_for_stanza(Bob, ?WAIT_TIMEOUT), ct:print("~p", [Presence]), MessageWithSubject = escalus:wait_for_stanza(Bob), ct:print("~p", [MessageWithSubject]), @@ -4055,7 +4156,7 @@ hibernated_room_is_stopped_and_restored_by_presence(Config) -> end), destroy_room(muc_host(), RoomName), - forget_room(muc_host(), RoomName). + forget_room(domain(), muc_host(), RoomName). stopped_rooms_history_is_available(Config) -> RoomName = fresh_room_name(), @@ -4076,7 +4177,7 @@ stopped_rooms_history_is_available(Config) -> end), destroy_room(muc_host(), RoomName), - forget_room(muc_host(), RoomName). + forget_room(domain(), muc_host(), RoomName). stopped_members_only_room_process_invitations_correctly(Config) -> RoomName = fresh_room_name(), @@ -4098,14 +4199,14 @@ stopped_members_only_room_process_invitations_correctly(Config) -> Stanza2 = stanza_set_affiliations(RoomName, [{escalus_client:short_jid(Kate), <<"member">>}]), escalus:send(Alice, Stanza2), - escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice)), + escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice, ?WAIT_TIMEOUT)), is_invitation(escalus:wait_for_stanza(Kate)), ok end), destroy_room(muc_host(), RoomName), - forget_room(muc_host(), RoomName). + forget_room(domain(), muc_host(), RoomName). room_with_participants_is_not_stopped(Config) -> RoomName = fresh_room_name(), @@ -4116,7 +4217,7 @@ room_with_participants_is_not_stopped(Config) -> end), destroy_room(muc_host(), RoomName), - forget_room(muc_host(), RoomName). + forget_room(domain(), muc_host(), RoomName). room_with_only_owner_is_stopped(Config) -> RoomName = fresh_room_name(), @@ -4130,7 +4231,20 @@ room_with_only_owner_is_stopped(Config) -> end), destroy_room(muc_host(), RoomName), - forget_room(muc_host(), RoomName). + forget_room(domain(), muc_host(), RoomName). + +can_found_in_db_when_stopped(Config) -> + RoomName = fresh_room_name(), + escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> + {ok, _, Pid} = given_fresh_room_is_hibernated( + Alice, RoomName, [{persistentroom, true}]), + true = wait_for_room_to_be_stopped(Pid, timer:seconds(8)), + {ok, _} = escalus_ejabberd:rpc(mod_muc, restore_room, + [domain(), muc_host(), RoomName]) + end), + + destroy_room(muc_host(), RoomName), + forget_room(domain(), muc_host(), RoomName). deep_hibernation_metrics_are_updated(Config) -> RoomName = fresh_room_name(), @@ -4153,7 +4267,7 @@ deep_hibernation_metrics_are_updated(Config) -> end), destroy_room(muc_host(), RoomName), - forget_room(muc_host(), RoomName). + forget_room(domain(), muc_host(), RoomName). get_spiral_metric_count(Host, MetricName) -> Result = escalus_ejabberd:rpc(mongoose_metrics, get_metric_value, @@ -4235,8 +4349,8 @@ given_fresh_room_with_messages_is_hibernated(Owner, RoomName, Opts, Participant) true = wait_for_hibernation(Pid, 10), {MessageBin, Result}. -forget_room(MUCHost, RoomName) -> - ok = escalus_ejabberd:rpc(mod_muc, forget_room, [MUCHost, RoomName]). +forget_room(ServerHost, MUCHost, RoomName) -> + ok = escalus_ejabberd:rpc(mod_muc, forget_room, [ServerHost, MUCHost, RoomName]). wait_for_room_to_be_stopped(Pid, Timeout) -> Ref = erlang:monitor(process, Pid), @@ -4267,7 +4381,7 @@ wait_for_mam_result(RoomName, Client, Msg) -> {data_form, true}], QueryStanza = mam_helper:stanza_archive_request(Props, <<"q1">>), escalus:send(Client, muc_helper:stanza_to_room(QueryStanza, RoomName)), - S = escalus:wait_for_stanza(Client), + S = escalus:wait_for_stanza(Client, ?WAIT_TIMEOUT), M = exml_query:path(S, [{element, <<"result">>}, {element, <<"forwarded">>}, {element, <<"message">>}]), @@ -4401,10 +4515,12 @@ load_already_registered_permanent_rooms(_Config) -> HttpAuthPool = none, %% Write a permanent room - {atomic, ok} = escalus_ejabberd:rpc(mod_muc,store_room,[Host, Room, []]), + ok = escalus_ejabberd:rpc(mod_muc, store_room, + [domain(), Host, Room, []]), % Load permanent rooms - escalus_ejabberd:rpc(mod_muc, load_permanent_rooms , [Host, ServerHost, Access, HistorySize, RoomShaper, HttpAuthPool]), + escalus_ejabberd:rpc(mod_muc, load_permanent_rooms, + [Host, ServerHost, Access, HistorySize, RoomShaper, HttpAuthPool]), %% Read online room RoomJID = escalus_ejabberd:rpc(jid,make,[Room,Host,<<>>]), @@ -4441,7 +4557,8 @@ check_message_route_to_offline_room(Config) -> escalus:story(Config, [{alice, 1}], fun(Alice) -> Room = <<"testroom4">>, Host = muc_host(), - {atomic, ok} = escalus_ejabberd:rpc(mod_muc,store_room,[Host, Room, []]), + ok = escalus_ejabberd:rpc(mod_muc, store_room, + [domain(), Host, Room, []]), %% Send a message to an offline permanent room escalus:send(Alice, stanza_room_subject(Room, <<"Subject line">>)), @@ -4848,6 +4965,41 @@ stanza_get_services(Config) -> escalus_stanza:setattr(escalus_stanza:iq_get(?NS_DISCO_ITEMS, []), <<"to">>, ct:get_config({hosts, mim, domain})). +get_nick_form_iq() -> + NS = <<"jabber:iq:register">>, + GetIQ = escalus_stanza:iq_get(<<"jabber:iq:register">>, []), + escalus_stanza:to(GetIQ, ?MUC_HOST). + +change_nick_form_iq(Nick) -> + NS = <<"jabber:iq:register">>, + NickField = form_field({<<"nick">>, Nick, <<"text-single">>}), + Form = stanza_form([NickField], NS), + SetIQ = escalus_stanza:iq_set(NS, [Form]), + escalus_stanza:to(SetIQ, ?MUC_HOST). + +remove_nick_form_iq() -> + NS = <<"jabber:iq:register">>, + RemoveEl = #xmlel{name = <<"remove">>}, + SetIQ = escalus_stanza:iq_set(NS, [RemoveEl]), + escalus_stanza:to(SetIQ, ?MUC_HOST). + +set_nick(User, Nick) -> + escalus:send(User, change_nick_form_iq(Nick)), + ResultIQ = escalus:wait_for_stanza(User), + escalus:assert(is_iq_result, ResultIQ). + +unset_nick(User) -> + escalus:send(User, remove_nick_form_iq()), + ResultIQ = escalus:wait_for_stanza(User), + escalus:assert(is_iq_result, ResultIQ). + +get_nick(User) -> + escalus:send(User, get_nick_form_iq()), + ResultIQ = escalus:wait_for_stanza(User), + escalus:assert(is_iq_result, ResultIQ), + true = form_has_field(<<"nick">>, ResultIQ), + form_field_value(<<"nick">>, ResultIQ). + %%-------------------------------------------------------------------- %% Helpers (assertions) %%-------------------------------------------------------------------- @@ -4867,6 +5019,23 @@ is_form(Stanza) -> exml_query:path(Stanza,[{element, <<"query">>}, {element,<<"x">>}, {attr, <<"xmlns">>}]) =:= ?NS_DATA_FORMS. +form_has_field(VarName, Stanza) -> + Path = [{element, <<"query">>}, + {element, <<"x">>}, + {element, <<"field">>}, + {attr, <<"var">>}], + Vars = exml_query:paths(Stanza, Path), + lists:member(VarName, Vars). + +form_field_value(VarName, Stanza) -> + Path = [{element, <<"query">>}, + {element, <<"x">>}, + {element, <<"field">>}], + Fields = exml_query:paths(Stanza, Path), + hd([exml_query:path(Field, [{element, <<"value">>}, cdata]) + || Field <- Fields, + exml_query:attr(Field, <<"var">>) == VarName]). + is_groupchat_message(Stanza) -> escalus_pred:is_message(Stanza) andalso escalus_pred:has_type(<<"groupchat">>, Stanza). @@ -5117,3 +5286,9 @@ fresh_room_name(Username) -> fresh_room_name() -> fresh_room_name(base16:encode(crypto:strong_rand_bytes(5))). + +fresh_nick_name(Prefix) -> + <>. + +fresh_nick_name() -> + fresh_room_name(base16:encode(crypto:strong_rand_bytes(5))). diff --git a/test.disabled/ejabberd_tests/tests/muc_helper.erl b/test.disabled/ejabberd_tests/tests/muc_helper.erl index c7e60df524f..2788ed0be20 100644 --- a/test.disabled/ejabberd_tests/tests/muc_helper.erl +++ b/test.disabled/ejabberd_tests/tests/muc_helper.erl @@ -41,8 +41,15 @@ foreach_recipient(Users, VerifyFun) -> end, Users). load_muc(Host) -> + Backend = case mongoose_helper:is_odbc_enabled(<<"localhost">>) of + true -> odbc; + false -> mnesia + end, + %% TODO refactoring. "localhost" should be passed as a parameter dynamic_modules:start(<<"localhost">>, mod_muc, [{host, binary_to_list(Host)}, + %% XXX TODO Uncomment, when mod_muc_db_odbc is written + %{backend, Backend}, {hibernate_timeout, 2000}, {hibernated_room_check_interval, 1000}, {hibernated_room_timeout, 2000}, diff --git a/test.disabled/ejabberd_tests/tests/s2s_SUITE.erl b/test.disabled/ejabberd_tests/tests/s2s_SUITE.erl index ea83c56774e..3801c848f65 100644 --- a/test.disabled/ejabberd_tests/tests/s2s_SUITE.erl +++ b/test.disabled/ejabberd_tests/tests/s2s_SUITE.erl @@ -70,85 +70,26 @@ negative() -> [timeout_waiting_for_message]. suite() -> - require_s2s_nodes() ++ - escalus:suite(). + s2s_helper:suite(escalus:suite()). -require_s2s_nodes() -> - [{require, mim_node, {hosts, mim, node}}, - {require, fed_node, {hosts, fed, node}}]. +users() -> + [alice2, alice, bob]. %%%=================================================================== %%% Init & teardown %%%=================================================================== -init_per_suite(Config0) -> - Node1S2SCertfile = rpc(mim(), ejabberd_config, get_local_option, [s2s_certfile]), - Node1S2SUseStartTLS = rpc(mim(), ejabberd_config, get_local_option, [s2s_use_starttls]), - - rpc(fed(), mongoose_cover_helper, start, [[ejabberd]]), - - Node2S2SCertfile = rpc(fed(), ejabberd_config, get_local_option, [s2s_certfile]), - Node2S2SUseStartTLS = rpc(fed(), ejabberd_config, get_local_option, [s2s_use_starttls]), - S2S = #s2s_opts{node1_s2s_certfile = Node1S2SCertfile, - node1_s2s_use_starttls = Node1S2SUseStartTLS, - node2_s2s_certfile = Node2S2SCertfile, - node2_s2s_use_starttls = Node2S2SUseStartTLS}, - - Config1 = [{s2s_opts, S2S} | escalus:init_per_suite(Config0)], - Config2 = [{escalus_user_db, xmpp} | Config1], - escalus:create_users(Config2, escalus:get_users([alice2, alice, bob])). +init_per_suite(Config) -> + Config1 = s2s_helper:init_s2s(escalus:init_per_suite(Config), true), + escalus:create_users(Config1, escalus:get_users(users())). end_per_suite(Config) -> - S2SOrig = ?config(s2s_opts, Config), - configure_s2s(S2SOrig), - rpc(fed(), mongoose_cover_helper, analyze, []), - escalus:delete_users(Config, escalus:get_users([alice2, alice, bob])), + s2s_helper:end_s2s(Config), + escalus:delete_users(Config, escalus:get_users(users())), escalus:end_per_suite(Config). -init_per_group(both_plain, Config) -> - configure_s2s(#s2s_opts{}), - Config; -init_per_group(both_tls_optional, Config) -> - S2S = ?config(s2s_opts, Config), %The initial config assumes that both nodes are configured to use encrypted s2s - configure_s2s(S2S), - Config; -init_per_group(both_tls_required, Config) -> - S2S = ?config(s2s_opts, Config), - configure_s2s(S2S#s2s_opts{node1_s2s_use_starttls = required, - node2_s2s_use_starttls = required}), - Config; -init_per_group(node1_tls_optional_node2_tls_required, Config) -> - S2S = ?config(s2s_opts, Config), - configure_s2s(S2S#s2s_opts{node2_s2s_use_starttls = required}), - Config; -init_per_group(node1_tls_required_node2_tls_optional, Config) -> - S2S = ?config(s2s_opts, Config), - configure_s2s(S2S#s2s_opts{node1_s2s_use_starttls = required}), - Config; -init_per_group(node1_tls_required_trusted_node2_tls_optional, Config) -> - S2S = ?config(s2s_opts, Config), - configure_s2s(S2S#s2s_opts{node1_s2s_use_starttls = required_trusted}), - Config; -init_per_group(node1_tls_false_node2_tls_optional, Config) -> - S2S = ?config(s2s_opts, Config), - configure_s2s(S2S#s2s_opts{node1_s2s_use_starttls = false}), - Config; -init_per_group(node1_tls_optional_node2_tls_false, Config) -> - S2S = ?config(s2s_opts, Config), - configure_s2s(S2S#s2s_opts{node2_s2s_use_starttls = false}), - Config; -init_per_group(node1_tls_false_node2_tls_required, Config) -> - S2S = ?config(s2s_opts, Config), - configure_s2s(S2S#s2s_opts{node1_s2s_use_starttls = false, - node2_s2s_use_starttls = required}), - Config; -init_per_group(node1_tls_required_node2_tls_false, Config) -> - S2S = ?config(s2s_opts, Config), - configure_s2s(S2S#s2s_opts{node1_s2s_use_starttls = required, - node2_s2s_use_starttls = false}), - Config; -init_per_group(_GroupName, Config) -> - Config. +init_per_group(GroupName, Config) -> + s2s_helper:configure_s2s(GroupName, Config). end_per_group(_GroupName, _Config) -> ok. @@ -238,39 +179,3 @@ nonascii_addr(Config) -> escalus:assert(is_chat_message, [<<"Miło Cię poznać">>], Stanza2) end). - -configure_s2s(#s2s_opts{node1_s2s_certfile = Certfile1, - node1_s2s_use_starttls = StartTLS1, - node2_s2s_certfile = Certfile2, - node2_s2s_use_starttls = StartTLS2}) -> - configure_s2s(mim(), Certfile1, StartTLS1), - configure_s2s(fed(), Certfile2, StartTLS2), - restart_s2s(). - -configure_s2s(Node, Certfile, StartTLS) -> - rpc(Node, ejabberd_config, add_local_option, [s2s_certfile, Certfile]), - rpc(Node, ejabberd_config, add_local_option, [s2s_use_starttls, StartTLS]). - -restart_s2s() -> - restart_s2s(mim()), - restart_s2s(fed()). - -restart_s2s(Node) -> - Children = rpc(Node, supervisor, which_children, [ejabberd_s2s_out_sup]), - [rpc(Node, ejabberd_s2s_out, stop_connection, [Pid]) || - {_, Pid, _, _} <- Children], - - ChildrenIn = rpc(Node, supervisor, which_children, [ejabberd_s2s_in_sup]), - [rpc(Node, erlang, exit, [Pid, kill]) || - {_, Pid, _, _} <- ChildrenIn]. - -mim() -> - get_or_fail(mim_node). - -fed() -> - get_or_fail(fed_node). - -get_or_fail(Key) -> - Val = ct:get_config(Key), - Val == undefined andalso error({undefined, Key}), - Val. diff --git a/test.disabled/ejabberd_tests/tests/s2s_helper.erl b/test.disabled/ejabberd_tests/tests/s2s_helper.erl new file mode 100644 index 00000000000..c72d970dd0f --- /dev/null +++ b/test.disabled/ejabberd_tests/tests/s2s_helper.erl @@ -0,0 +1,134 @@ +-module(s2s_helper). +-export([suite/1]). +-export([init_s2s/2]). +-export([end_s2s/1]). +-export([configure_s2s/2]). + +-import(distributed_helper, [rpc/4, rpc/5]). + +-include_lib("escalus/include/escalus.hrl"). +-include_lib("common_test/include/ct.hrl"). +-record(s2s_opts, { + node1_s2s_certfile = undefined, + node1_s2s_use_starttls = undefined, + node2_s2s_certfile = undefined, + node2_s2s_use_starttls = undefined + }). + + +suite(Config) -> + require_s2s_nodes() ++ Config. + +require_s2s_nodes() -> + [{require, mim_node, {hosts, mim, node}}, + {require, fed_node, {hosts, fed, node}}]. + +init_s2s(Config, CoverEnabled) when is_boolean(CoverEnabled) -> + Node1S2SCertfile = rpc(mim(), ejabberd_config, get_local_option, [s2s_certfile]), + Node1S2SUseStartTLS = rpc(mim(), ejabberd_config, get_local_option, [s2s_use_starttls]), + case CoverEnabled of + true -> + rpc(fed(), mongoose_cover_helper, start, [[ejabberd]]); + _ -> + ok + end, + Node2S2SCertfile = rpc(fed(), ejabberd_config, get_local_option, [s2s_certfile]), + Node2S2SUseStartTLS = rpc(fed(), ejabberd_config, get_local_option, [s2s_use_starttls]), + S2S = #s2s_opts{node1_s2s_certfile = Node1S2SCertfile, + node1_s2s_use_starttls = Node1S2SUseStartTLS, + node2_s2s_certfile = Node2S2SCertfile, + node2_s2s_use_starttls = Node2S2SUseStartTLS}, + + [{s2s_opts, S2S}, + {escalus_user_db, xmpp}, + {cover_enabled, CoverEnabled} | Config]. + +end_s2s(Config) -> + S2SOrig = ?config(s2s_opts, Config), + configure_s2s(S2SOrig), + CoverEnabled = ?config(cover_enabled, Config), + case CoverEnabled of + true -> + rpc(fed(), mongoose_cover_helper, analyze, []); + _ -> + ok + end. + +configure_s2s(both_plain, Config) -> + configure_s2s(#s2s_opts{}), + Config; +configure_s2s(both_tls_optional, Config) -> + S2S = ?config(s2s_opts, Config), %The initial config assumes that both nodes are configured to use encrypted s2s + configure_s2s(S2S), + Config; +configure_s2s(both_tls_required, Config) -> + S2S = ?config(s2s_opts, Config), + configure_s2s(S2S#s2s_opts{node1_s2s_use_starttls = required, + node2_s2s_use_starttls = required}), + Config; +configure_s2s(node1_tls_optional_node2_tls_required, Config) -> + S2S = ?config(s2s_opts, Config), + configure_s2s(S2S#s2s_opts{node2_s2s_use_starttls = required}), + Config; +configure_s2s(node1_tls_required_node2_tls_optional, Config) -> + S2S = ?config(s2s_opts, Config), + configure_s2s(S2S#s2s_opts{node1_s2s_use_starttls = required}), + Config; +configure_s2s(node1_tls_required_trusted_node2_tls_optional, Config) -> + S2S = ?config(s2s_opts, Config), + configure_s2s(S2S#s2s_opts{node1_s2s_use_starttls = required_trusted}), + Config; +configure_s2s(node1_tls_false_node2_tls_optional, Config) -> + S2S = ?config(s2s_opts, Config), + configure_s2s(S2S#s2s_opts{node1_s2s_use_starttls = false}), + Config; +configure_s2s(node1_tls_optional_node2_tls_false, Config) -> + S2S = ?config(s2s_opts, Config), + configure_s2s(S2S#s2s_opts{node2_s2s_use_starttls = false}), + Config; +configure_s2s(node1_tls_false_node2_tls_required, Config) -> + S2S = ?config(s2s_opts, Config), + configure_s2s(S2S#s2s_opts{node1_s2s_use_starttls = false, + node2_s2s_use_starttls = required}), + Config; +configure_s2s(node1_tls_required_node2_tls_false, Config) -> + S2S = ?config(s2s_opts, Config), + configure_s2s(S2S#s2s_opts{node1_s2s_use_starttls = required, + node2_s2s_use_starttls = false}), + Config. + +configure_s2s(#s2s_opts{node1_s2s_certfile = Certfile1, + node1_s2s_use_starttls = StartTLS1, + node2_s2s_certfile = Certfile2, + node2_s2s_use_starttls = StartTLS2}) -> + configure_s2s(mim(), Certfile1, StartTLS1), + configure_s2s(fed(), Certfile2, StartTLS2), + restart_s2s(). + +configure_s2s(Node, Certfile, StartTLS) -> + rpc(Node, ejabberd_config, add_local_option, [s2s_certfile, Certfile]), + rpc(Node, ejabberd_config, add_local_option, [s2s_use_starttls, StartTLS]). + +restart_s2s() -> + restart_s2s(mim()), + restart_s2s(fed()). + +restart_s2s(Node) -> + Children = rpc(Node, supervisor, which_children, [ejabberd_s2s_out_sup]), + [rpc(Node, ejabberd_s2s_out, stop_connection, [Pid]) || + {_, Pid, _, _} <- Children], + + ChildrenIn = rpc(Node, supervisor, which_children, [ejabberd_s2s_in_sup]), + [rpc(Node, erlang, exit, [Pid, kill]) || + {_, Pid, _, _} <- ChildrenIn]. + +mim() -> + get_or_fail(mim_node). + +fed() -> + get_or_fail(fed_node). + +get_or_fail(Key) -> + Val = ct:get_config(Key), + Val == undefined andalso error({undefined, Key}), + Val.