diff --git a/conference.js b/conference.js index 4c215ef996..a1cd055239 100644 --- a/conference.js +++ b/conference.js @@ -1646,10 +1646,14 @@ export default { APP.store.dispatch(conferenceUniqueIdSet(room, ...args)); }); - room.on( - JitsiConferenceEvents.AUTH_STATUS_CHANGED, - (authEnabled, authLogin) => - APP.store.dispatch(authStatusChanged(authEnabled, authLogin))); + // we want to ignore this event in case of tokenAuthUrl config + // we are deprecating this and at some point will get rid of it + if (!config.tokenAuthUrl) { + room.on( + JitsiConferenceEvents.AUTH_STATUS_CHANGED, + (authEnabled, authLogin) => + APP.store.dispatch(authStatusChanged(authEnabled, authLogin))); + } room.on(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED, user => { APP.store.dispatch(updateRemoteParticipantFeatures(user)); diff --git a/config.js b/config.js index 3c4ecb406b..10920696ca 100644 --- a/config.js +++ b/config.js @@ -1447,6 +1447,7 @@ var config = { peopleSearchUrl requireDisplayName tokenAuthUrl + tokenLogoutUrl */ /** diff --git a/package-lock.json b/package-lock.json index b30bb3dc26..e994b4f218 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,7 +60,7 @@ "js-md5": "0.6.1", "js-sha512": "0.8.0", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1665.0.0+9ffab6aa/lib-jitsi-meet.tgz", + "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1668.0.0+92693bbd/lib-jitsi-meet.tgz", "lodash": "4.17.21", "moment": "2.29.4", "moment-duration-format": "2.2.2", @@ -12770,8 +12770,8 @@ }, "node_modules/lib-jitsi-meet": { "version": "0.0.0", - "resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1665.0.0+9ffab6aa/lib-jitsi-meet.tgz", - "integrity": "sha512-uAFXQ2mqN7nxOveFcIFVPVU9HKLJbVc2Bn8iJvT4cHshsdOY1rvCdXqnNPg+Xl31xGxupH+zU7Yw3+sLwZyz2A==", + "resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1668.0.0+92693bbd/lib-jitsi-meet.tgz", + "integrity": "sha512-MeEV+LNUICTsYfnoIWPomQf/mHIFbadzVZWynYGn0z0W0JlX5YAsooEk6mDFM996FL0FVl+Qpj3vLIpEhzy4Xw==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -29289,8 +29289,8 @@ } }, "lib-jitsi-meet": { - "version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1665.0.0+9ffab6aa/lib-jitsi-meet.tgz", - "integrity": "sha512-uAFXQ2mqN7nxOveFcIFVPVU9HKLJbVc2Bn8iJvT4cHshsdOY1rvCdXqnNPg+Xl31xGxupH+zU7Yw3+sLwZyz2A==", + "version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1668.0.0+92693bbd/lib-jitsi-meet.tgz", + "integrity": "sha512-MeEV+LNUICTsYfnoIWPomQf/mHIFbadzVZWynYGn0z0W0JlX5YAsooEk6mDFM996FL0FVl+Qpj3vLIpEhzy4Xw==", "requires": { "@jitsi/js-utils": "2.0.0", "@jitsi/logger": "2.0.0", diff --git a/package.json b/package.json index b3a54b1ddc..085754b47c 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "js-md5": "0.6.1", "js-sha512": "0.8.0", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1665.0.0+9ffab6aa/lib-jitsi-meet.tgz", + "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1668.0.0+92693bbd/lib-jitsi-meet.tgz", "lodash": "4.17.21", "moment": "2.29.4", "moment-duration-format": "2.2.2", diff --git a/react/features/authentication/actions.native.ts b/react/features/authentication/actions.native.ts index b958a18dbd..da07baf63f 100644 --- a/react/features/authentication/actions.native.ts +++ b/react/features/authentication/actions.native.ts @@ -55,9 +55,13 @@ export function cancelWaitForOwner() { // clients/consumers need an event. const { authRequired } = getState()['features/base/conference']; - authRequired && dispatch(conferenceLeft(authRequired)); + if (authRequired) { + dispatch(conferenceLeft(authRequired)); - dispatch(appNavigate(undefined)); + // in case we are showing lobby and on top of it wait for owner + // we do not want to navigate away from the conference + dispatch(appNavigate(undefined)); + } }; } diff --git a/react/features/authentication/actions.web.ts b/react/features/authentication/actions.web.ts index 3b61835035..86131043dd 100644 --- a/react/features/authentication/actions.web.ts +++ b/react/features/authentication/actions.web.ts @@ -24,13 +24,17 @@ export function cancelLogin() { /** * Cancels authentication, closes {@link WaitForOwnerDialog} - * and navigates back to the welcome page. + * and navigates back to the welcome page only in the case of authentication required error. + * We can be showing the dialog while lobby is enabled and participant is still waiting there and hiding this dialog + * should do nothing. * * @returns {Function} */ export function cancelWaitForOwner() { - return (dispatch: IStore['dispatch']) => { - dispatch(maybeRedirectToWelcomePage()); + return (dispatch: IStore['dispatch'], getState: IStore['getState']) => { + const { authRequired } = getState()['features/base/conference']; + + authRequired && dispatch(maybeRedirectToWelcomePage()); }; } diff --git a/react/features/authentication/middleware.ts b/react/features/authentication/middleware.ts index c4f9e5660a..f1ce754fcb 100644 --- a/react/features/authentication/middleware.ts +++ b/react/features/authentication/middleware.ts @@ -89,8 +89,11 @@ MiddlewareRegistry.register(store => next => action => { // CONFERENCE_FAILED caused by // JitsiConferenceErrors.AUTHENTICATION_REQUIRED. let recoverable; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [ _lobbyJid, lobbyWaitingForHost ] = error.params; - if (error.name === JitsiConferenceErrors.AUTHENTICATION_REQUIRED) { + if (error.name === JitsiConferenceErrors.AUTHENTICATION_REQUIRED + || (error.name === JitsiConferenceErrors.MEMBERS_ONLY_ERROR && lobbyWaitingForHost)) { if (typeof error.recoverable === 'undefined') { error.recoverable = true; } @@ -149,9 +152,17 @@ MiddlewareRegistry.register(store => next => action => { break; } - store.dispatch(openLogoutDialog(() => - conference.room.moderator.logout(() => store.dispatch(hangup(true))) - )); + store.dispatch(openLogoutDialog(() => { + const logoutUrl = store.getState()['features/base/config'].tokenLogoutUrl; + + if (logoutUrl) { + window.location.href = logoutUrl; + + return; + } + + conference.room.moderator.logout(() => store.dispatch(hangup(true))); + })); break; } diff --git a/react/features/base/conference/actions.ts b/react/features/base/conference/actions.ts index c49465ffb6..2679e6b008 100644 --- a/react/features/base/conference/actions.ts +++ b/react/features/base/conference/actions.ts @@ -95,9 +95,14 @@ function _addConferenceListeners(conference: IJitsiConference, dispatch: IStore[ // Dispatches into features/base/conference follow: - conference.on( - JitsiConferenceEvents.AUTH_STATUS_CHANGED, - (authEnabled: boolean, authLogin: string) => dispatch(authStatusChanged(authEnabled, authLogin))); + // we want to ignore this event in case of tokenAuthUrl config + // we are deprecating this and at some point will get rid of it + if (!state['features/base/config'].tokenAuthUrl) { + conference.on( + JitsiConferenceEvents.AUTH_STATUS_CHANGED, + (authEnabled: boolean, authLogin: string) => dispatch(authStatusChanged(authEnabled, authLogin))); + } + conference.on( JitsiConferenceEvents.CONFERENCE_FAILED, (err: string, ...args: any[]) => dispatch(conferenceFailed(conference, err, ...args))); diff --git a/react/features/base/conference/middleware.any.ts b/react/features/base/conference/middleware.any.ts index fdde18bc11..e24eb77c6d 100644 --- a/react/features/base/conference/middleware.any.ts +++ b/react/features/base/conference/middleware.any.ts @@ -49,6 +49,7 @@ import { SET_ROOM } from './actionTypes'; import { + authStatusChanged, conferenceFailed, conferenceWillInit, conferenceWillLeave, @@ -351,9 +352,23 @@ function _conferenceJoined({ dispatch, getState }: IStore, next: Function, actio * @private * @returns {Object} The value returned by {@code next(action)}. */ -async function _connectionEstablished({ dispatch }: IStore, next: Function, action: AnyAction) { +async function _connectionEstablished({ dispatch, getState }: IStore, next: Function, action: AnyAction) { const result = next(action); + const { tokenAuthUrl = false } = getState()['features/base/config']; + + // if there is token auth URL defined and local participant is using jwt + // this means it is logged in when connection is established, so we can change the state + if (tokenAuthUrl) { + let email; + + if (getState()['features/base/jwt'].jwt) { + email = getLocalParticipant(getState())?.email; + } + + dispatch(authStatusChanged(true, email || '')); + } + // FIXME: Workaround for the web version. Currently, the creation of the // conference is handled by /conference.js. if (typeof APP === 'undefined') { diff --git a/react/features/base/config/configType.ts b/react/features/base/config/configType.ts index 2ff77b8407..3305042c6c 100644 --- a/react/features/base/config/configType.ts +++ b/react/features/base/config/configType.ts @@ -576,6 +576,7 @@ export interface IConfig { numberOfVisibleTiles?: number; }; tokenAuthUrl?: string; + tokenLogoutUrl?: string; toolbarButtons?: Array; toolbarConfig?: { alwaysVisible?: boolean; diff --git a/react/features/base/jwt/middleware.ts b/react/features/base/jwt/middleware.ts index d17cb1cca3..80f0067349 100644 --- a/react/features/base/jwt/middleware.ts +++ b/react/features/base/jwt/middleware.ts @@ -153,11 +153,12 @@ function _setJWT(store: IStore, next: Function, action: AnyAction) { _overwriteLocalParticipant( store, { ...newUser, features: context.features }); - } else if (jwtPayload.name || jwtPayload.picture) { + } else if (jwtPayload.name || jwtPayload.picture || jwtPayload.email) { // there are some tokens (firebase) having picture and name on the main level. _overwriteLocalParticipant(store, { avatarURL: jwtPayload.picture, - name: jwtPayload.name + name: jwtPayload.name, + email: jwtPayload.email }); } } diff --git a/react/features/lobby/middleware.ts b/react/features/lobby/middleware.ts index 8da1c725a0..259fbda964 100644 --- a/react/features/lobby/middleware.ts +++ b/react/features/lobby/middleware.ts @@ -275,12 +275,15 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio error.recoverable = true; } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [ _lobbyJid, lobbyWaitingForHost ] = error.params; + const result = next(action); dispatch(openLobbyScreen()); // if there was an error about display name and pre-join is not enabled - if (shouldAutoKnock(state) || (isDisplayNameRequiredError && !prejoinConfig?.enabled)) { + if (shouldAutoKnock(state) || (isDisplayNameRequiredError && !prejoinConfig?.enabled) || lobbyWaitingForHost) { dispatch(startKnocking()); } diff --git a/resources/prosody-plugins/mod_muc_lobby_rooms.lua b/resources/prosody-plugins/mod_muc_lobby_rooms.lua index 9511c64e1c..f043e8b61b 100644 --- a/resources/prosody-plugins/mod_muc_lobby_rooms.lua +++ b/resources/prosody-plugins/mod_muc_lobby_rooms.lua @@ -484,7 +484,18 @@ function handle_create_lobby(event) end function handle_destroy_lobby(event) - destroy_lobby_room(event.room, event.newjid, event.message); + local room = event.room; + + -- since this is called by backend rather than triggered by UI, we need to + -- trigger a 104 (config change) status message so UI state is properly updated for existing users (and jicofo) + destroy_lobby_room(room, event.newjid, event.message); + + -- Trigger a presence with 104 so existing participants retrieves new muc#roomconfig + room:broadcast_message( + st.message({ type='groupchat', from=room.jid }) + :tag('x', { xmlns='http://jabber.org/protocol/muc#user' }) + :tag('status', { code='104' }) + ); end module:hook_global('config-reloaded', load_config); diff --git a/resources/prosody-plugins/mod_muc_wait_for_host.lua b/resources/prosody-plugins/mod_muc_wait_for_host.lua new file mode 100644 index 0000000000..79484b3e53 --- /dev/null +++ b/resources/prosody-plugins/mod_muc_wait_for_host.lua @@ -0,0 +1,112 @@ +-- This module is activated under the main muc component +-- This will prevent anyone joining the call till jicofo and one moderator join the room +-- for the rest of the participants lobby will be turned on and they will be waiting there till +-- the main participant joins and lobby will be turned off at that time and rest of the participants will +-- join the room. It expects main virtual host to be set to require jwt tokens and guests to use +-- the guest domain which is anonymous. +-- The module has the option to set participants to moderators when connected via token/when they are authenticated +-- This module depends on mod_persistent_lobby. +local um_is_admin = require 'core.usermanager'.is_admin; +local jid = require 'util.jid'; + +local disable_auto_owners = module:get_option_boolean('wait_for_host_disable_auto_owners', false); + +local muc_domain_base = module:get_option_string('muc_mapper_domain_base'); +if not muc_domain_base then + module:log('warn', "No 'muc_mapper_domain_base' option set, disabling muc_mapper plugin inactive"); + return +end + +-- to activate this you need the following config in general config file in log = { } +-- { to = 'file', filename = '/var/log/prosody/prosody.audit.log', levels = { 'audit' } } +local logger = require 'util.logger'; +local audit_logger = logger.make_logger('mod_'..module.name, 'audit'); + +local lobby_muc_component_config = 'lobby.' .. muc_domain_base; +local lobby_host; + +if not disable_auto_owners then + module:hook('muc-occupant-joined', function (event) + local room, occupant, session = event.room, event.occupant, event.origin; + + -- for jwt authenticated and username and password authenticated + if session.auth_token or (session.username and jid.host(occupant.bare_jid) == muc_domain_base) then + room:set_affiliation(true, occupant.bare_jid, 'owner'); + end + end, 2); +end + +local function is_admin(jid) + return um_is_admin(jid, module.host); +end + +-- if not authenticated user is trying to join the room we enable lobby in it +-- and wait for the moderator to join +module:hook('muc-occupant-pre-join', function (event) + local room, occupant, session = event.room, event.occupant, event.origin; + + -- we ignore jicofo as we want it to join the room or if the room has already seen its + -- authenticated host + if is_admin(occupant.bare_jid) or room.has_host then + return; + end + + local has_host = false; + for _, o in room:each_occupant() do + if jid.host(o.bare_jid) == muc_domain_base then + room.has_host = true; + end + end + + if not room.has_host then + if session.auth_token or (session.username and jid.host(occupant.bare_jid) == muc_domain_base) then + -- the host is here, let's drop the lobby + room:set_members_only(false); + + -- let's set the default role of 'participant' for the newly created occupant as it was nil when created + -- when the room was still members_only, later if not disabled this participant will become a moderator + occupant.role = room:get_default_role(room:get_affiliation(occupant.bare_jid)) or 'participant'; + + module:log('info', 'Host %s arrived in %s.', occupant.bare_jid, room.jid); + audit_logger('room_jid:%s created_by:%s', room.jid, + session.jitsi_meet_context_user.id and session.jitsi_meet_context_user.id or 'nil'); + lobby_host:fire_event('destroy-lobby-room', { + room = room, + newjid = room.jid, + message = 'Host arrived.', + }); + elseif not room:get_members_only() then + -- let's enable lobby + module:log('info', 'Will wait for host in %s.', room.jid); + prosody.events.fire_event('create-persistent-lobby-room', { + room = room; + reason = 'waiting-for-host', + skip_display_name_check = true; + }); + end + end +end); + +-- process a host module directly if loaded or hooks to wait for its load +function process_host_module(name, callback) + local function process_host(host) + if host == name then + callback(module:context(host), host); + end + end + + if prosody.hosts[name] == nil then + module:log('debug', 'No host/component found, will wait for it: %s', name) + + -- when a host or component is added + prosody.events.add_handler('host-activated', process_host); + else + process_host(name); + end +end + +process_host_module(lobby_muc_component_config, function(host_module, host) + -- lobby muc component created + module:log('info', 'Lobby component loaded %s', host); + lobby_host = module:context(host_module); +end); diff --git a/resources/prosody-plugins/token/util.lib.lua b/resources/prosody-plugins/token/util.lib.lua index 4edc2ab5e8..cf1fbf1cf3 100644 --- a/resources/prosody-plugins/token/util.lib.lua +++ b/resources/prosody-plugins/token/util.lib.lua @@ -297,6 +297,9 @@ function Util:process_and_verify_token(session, acceptedIssuers) if claims["context"]["room"] ~= nil then session.jitsi_meet_context_room = claims["context"]["room"] end + elseif claims["user_id"] then + session.jitsi_meet_context_user = {}; + session.jitsi_meet_context_user.id = claims["user_id"]; end return true; else