From 5b4383d835d4e3489077c8eb6448fb92b9c7a173 Mon Sep 17 00:00:00 2001 From: Hristo Terezov Date: Mon, 26 Aug 2024 14:49:34 -0500 Subject: [PATCH] feat(shared-video): Get allowed URL domains from config and dynamic branding. --- config.js | 8 +++- modules/API/API.js | 2 +- react/features/base/config/configType.ts | 2 +- react/features/dynamic-branding/reducer.ts | 6 +-- react/features/shared-video/actionTypes.ts | 4 +- react/features/shared-video/actions.any.ts | 18 +++++---- .../components/AbstractSharedVideoDialog.tsx | 9 ++++- .../components/native/SharedVideoDialog.tsx | 18 ++++++++- .../components/web/SharedVideoDialog.tsx | 18 ++++++++- react/features/shared-video/constants.ts | 9 ++++- react/features/shared-video/functions.ts | 40 ++++++++----------- react/features/shared-video/middleware.any.ts | 37 ++++++++++++++--- react/features/shared-video/reducer.ts | 21 ++++++---- 13 files changed, 133 insertions(+), 59 deletions(-) diff --git a/config.js b/config.js index 43560bccb7..f8ea743d05 100644 --- a/config.js +++ b/config.js @@ -1443,8 +1443,12 @@ var config = { */ // dynamicBrandingUrl: '', - // Own url domains list added to the white listed domains for shared video - // ownVideoURLDomains: [ '' ], + // A list of allowed URL domains for shared video. + // + // NOTE: + // '*' is allowed value and it will allow any URL to be used for shared video. We do not recommend using '*', + // use it at your own risk! + // sharedVideoAllowedURLDomains: [ ], // Options related to the participants pane. // participantsPane: { diff --git a/modules/API/API.js b/modules/API/API.js index d4887e3d7a..835d00a90c 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -547,7 +547,7 @@ function initCommands() { }, 'start-share-video': url => { sendAnalytics(createApiEvent('share.video.start')); - const id = extractYoutubeIdOrURL(url); + const id = extractYoutubeIdOrURL(url, APP.store.getState()['features/shared-video'].allowedUrlDomains); if (id) { APP.store.dispatch(playSharedVideo(id)); diff --git a/react/features/base/config/configType.ts b/react/features/base/config/configType.ts index a1bf904f7f..c867891f05 100644 --- a/react/features/base/config/configType.ts +++ b/react/features/base/config/configType.ts @@ -456,7 +456,6 @@ export interface IConfig { notifications?: Array; openSharedDocumentOnJoin?: boolean; opusMaxAverageBitrate?: number; - ownVideoURLDomains?: Array; p2p?: { backToP2PDelay?: number; codecPreferenceOrder?: Array; @@ -532,6 +531,7 @@ export interface IConfig { hideLobbyButton?: boolean; }; serviceUrl?: string; + sharedVideoAllowedURLDomains?: Array; sipInviteUrl?: string; speakerStats?: { disableSearch?: boolean; diff --git a/react/features/dynamic-branding/reducer.ts b/react/features/dynamic-branding/reducer.ts index 3ac03da2e3..77e8feb2db 100644 --- a/react/features/dynamic-branding/reducer.ts +++ b/react/features/dynamic-branding/reducer.ts @@ -157,8 +157,8 @@ export interface IDynamicBrandingState { logoImageUrl: string; muiBrandedTheme?: boolean; premeetingBackground: string; + sharedVideoAllowedURLDomains?: Array; showGiphyIntegration?: boolean; - urlWhitelist?: Array; useDynamicBrandingData: boolean; virtualBackgrounds: Array; } @@ -182,8 +182,8 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STA logoImageUrl, muiBrandedTheme, premeetingBackground, + sharedVideoAllowedURLDomains, showGiphyIntegration, - urlWhitelist, virtualBackgrounds } = action.value; @@ -200,10 +200,10 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STA logoImageUrl, muiBrandedTheme, premeetingBackground, + sharedVideoAllowedURLDomains, showGiphyIntegration, customizationFailed: false, customizationReady: true, - urlWhitelist, useDynamicBrandingData: true, virtualBackgrounds: formatImages(virtualBackgrounds || []) }; diff --git a/react/features/shared-video/actionTypes.ts b/react/features/shared-video/actionTypes.ts index ec8ac7351c..1cb641ad0c 100644 --- a/react/features/shared-video/actionTypes.ts +++ b/react/features/shared-video/actionTypes.ts @@ -33,7 +33,7 @@ export const SET_DISABLE_BUTTON = 'SET_DISABLE_BUTTON'; * The type of the action which sets an array of whitelisted urls. * * { - * type: SET_URL_WHITELIST + * type: SET_ALLOWED_URL_DOMAINS * } */ -export const SET_URL_WHITELIST = 'SET_URL_WHITELIST'; +export const SET_ALLOWED_URL_DOMAINS = 'SET_ALLOWED_URL_DOMAINS'; diff --git a/react/features/shared-video/actions.any.ts b/react/features/shared-video/actions.any.ts index 6415ab1dd0..8d3cd3fade 100644 --- a/react/features/shared-video/actions.any.ts +++ b/react/features/shared-video/actions.any.ts @@ -3,7 +3,7 @@ import { getCurrentConference } from '../base/conference/functions'; import { openDialog } from '../base/dialog/actions'; import { getLocalParticipant } from '../base/participants/functions'; -import { RESET_SHARED_VIDEO_STATUS, SET_SHARED_VIDEO_STATUS, SET_URL_WHITELIST } from './actionTypes'; +import { RESET_SHARED_VIDEO_STATUS, SET_ALLOWED_URL_DOMAINS, SET_SHARED_VIDEO_STATUS } from './actionTypes'; import { SharedVideoDialog } from './components'; import { isSharedVideoEnabled, isURLAllowedForSharedVideo } from './functions'; @@ -90,7 +90,8 @@ export function stopSharedVideo() { */ export function playSharedVideo(videoUrl: string) { return (dispatch: IStore['dispatch'], getState: IStore['getState']) => { - if (!isSharedVideoEnabled(getState()) || !isURLAllowedForSharedVideo(getState(), videoUrl)) { + if (!isSharedVideoEnabled(getState()) + || !isURLAllowedForSharedVideo(videoUrl, getState()['features/shared-video'].allowedUrlDomains, true)) { return; } const conference = getCurrentConference(getState()); @@ -128,16 +129,17 @@ export function toggleSharedVideo() { } /** - * Resets the status of the shared video. + * Sets the allowed URL domains of the shared video. * - *@param {Array} urlWhitelist - The new whitelist to be set. + * @param {Array} allowedUrlDomains - The new whitelist to be set. * @returns {{ - * type: SET_SHARED_VIDEO_STATUS, + * type: SET_ALLOWED_URL_DOMAINS, + * allowedUrlDomains: Array * }} */ -export function setUrlWhitelist(urlWhitelist: Array) { +export function setAllowedUrlDomians(allowedUrlDomains: Array) { return { - type: SET_URL_WHITELIST, - urlWhitelist + type: SET_ALLOWED_URL_DOMAINS, + allowedUrlDomains }; } diff --git a/react/features/shared-video/components/AbstractSharedVideoDialog.tsx b/react/features/shared-video/components/AbstractSharedVideoDialog.tsx index ff78904a73..bbccfbf656 100644 --- a/react/features/shared-video/components/AbstractSharedVideoDialog.tsx +++ b/react/features/shared-video/components/AbstractSharedVideoDialog.tsx @@ -10,6 +10,11 @@ import { extractYoutubeIdOrURL } from '../functions'; */ export interface IProps extends WithTranslation { + /** + * The allowed URL domains for shared video. + */ + _allowedUrlDomains: Array; + /** * Invoked to update the shared video link. */ @@ -48,9 +53,9 @@ export default class AbstractSharedVideoDialog extends Component < IProps, S * @returns {boolean} */ _onSetVideoLink(link: string) { - const { onPostSubmit } = this.props; + const { _allowedUrlDomains, onPostSubmit } = this.props; - const id = extractYoutubeIdOrURL(link); + const id = extractYoutubeIdOrURL(link, _allowedUrlDomains); if (!id) { return false; diff --git a/react/features/shared-video/components/native/SharedVideoDialog.tsx b/react/features/shared-video/components/native/SharedVideoDialog.tsx index aefa5b0d25..a6e0de1f7d 100644 --- a/react/features/shared-video/components/native/SharedVideoDialog.tsx +++ b/react/features/shared-video/components/native/SharedVideoDialog.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; +import { IReduxState } from '../../../app/types'; import InputDialog from '../../../base/dialog/components/native/InputDialog'; import { translate } from '../../../base/i18n/functions'; import AbstractSharedVideoDialog, { IProps } from '../AbstractSharedVideoDialog'; @@ -67,4 +68,19 @@ class SharedVideoDialog extends AbstractSharedVideoDialog { } } -export default translate(connect()(SharedVideoDialog)); +/** + * Maps part of the Redux state to the props of this component. + * + * @param {Object} state - The Redux state. + * @private + * @returns {IProps} + */ +function mapStateToProps(state: IReduxState) { + const { allowedUrlDomains } = state['features/shared-video']; + + return { + _allowedUrlDomains: allowedUrlDomains + }; +} + +export default translate(connect(mapStateToProps)(SharedVideoDialog)); diff --git a/react/features/shared-video/components/web/SharedVideoDialog.tsx b/react/features/shared-video/components/web/SharedVideoDialog.tsx index e56b260019..4beb408053 100644 --- a/react/features/shared-video/components/web/SharedVideoDialog.tsx +++ b/react/features/shared-video/components/web/SharedVideoDialog.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; +import { IReduxState } from '../../../app/types'; import { hideDialog } from '../../../base/dialog/actions'; import { translate } from '../../../base/i18n/functions'; import Dialog from '../../../base/ui/components/web/Dialog'; @@ -99,4 +100,19 @@ class SharedVideoDialog extends AbstractSharedVideoDialog { } } -export default translate(connect()(SharedVideoDialog)); +/** + * Maps part of the Redux state to the props of this component. + * + * @param {Object} state - The Redux state. + * @private + * @returns {IProps} + */ +function mapStateToProps(state: IReduxState) { + const { allowedUrlDomains } = state['features/shared-video']; + + return { + _allowedUrlDomains: allowedUrlDomains + }; +} + +export default translate(connect(mapStateToProps)(SharedVideoDialog)); diff --git a/react/features/shared-video/constants.ts b/react/features/shared-video/constants.ts index af550dfc12..2305a0848f 100644 --- a/react/features/shared-video/constants.ts +++ b/react/features/shared-video/constants.ts @@ -35,6 +35,11 @@ export const PLAYBACK_STATUSES = { export const YOUTUBE_URL_DOMAIN = 'youtube.com'; /** - * The white listed domains for shared video. + * The constant to allow URL domains. */ -export const URL_WHITELIST = [ YOUTUBE_URL_DOMAIN ]; +export const ALLOW_ALL_URL_DOMAINS = '*'; + +/** + * The default white listed domains for shared video. + */ +export const DEFAULT_ALLOWED_URL_DOMAINS = [ YOUTUBE_URL_DOMAIN ]; diff --git a/react/features/shared-video/functions.ts b/react/features/shared-video/functions.ts index 6ad563063b..0db7138f73 100644 --- a/react/features/shared-video/functions.ts +++ b/react/features/shared-video/functions.ts @@ -3,7 +3,7 @@ import { getFakeParticipants } from '../base/participants/functions'; import { toState } from '../base/redux/functions'; import { - URL_WHITELIST, + ALLOW_ALL_URL_DOMAINS, VIDEO_PLAYER_PARTICIPANT_NAME, YOUTUBE_PLAYER_PARTICIPANT_NAME, YOUTUBE_URL_DOMAIN @@ -63,9 +63,10 @@ export function isVideoPlaying(stateful: IStateful): boolean { * Extracts a Youtube id or URL from the user input. * * @param {string} input - The user input. + * @param {Array} allowedUrlDomains - The allowed URL domains for shared video. * @returns {string|undefined} */ -export function extractYoutubeIdOrURL(input: string) { +export function extractYoutubeIdOrURL(input: string, allowedUrlDomains?: Array) { if (!input) { return; } @@ -76,7 +77,7 @@ export function extractYoutubeIdOrURL(input: string) { return; } - if (areYoutubeURLsAllowedForSharedVideo()) { + if (areYoutubeURLsAllowedForSharedVideo(allowedUrlDomains)) { const youtubeId = getYoutubeId(trimmedLink); if (youtubeId) { @@ -84,15 +85,7 @@ export function extractYoutubeIdOrURL(input: string) { } } - // Check if the URL is valid, native may crash otherwise. - try { - // eslint-disable-next-line no-new - const url = new URL(trimmedLink); - - if (!URL_WHITELIST.includes(url?.hostname)) { - return; - } - } catch (_) { + if (!isURLAllowedForSharedVideo(trimmedLink, allowedUrlDomains)) { return; } @@ -108,32 +101,33 @@ export function extractYoutubeIdOrURL(input: string) { export function isSharedVideoEnabled(stateful: IStateful) { const state = toState(stateful); - const { urlWhitelist = [] } = toState(stateful)['features/shared-video']; + const { allowedUrlDomains = [] } = toState(stateful)['features/shared-video']; const { disableThirdPartyRequests = false } = state['features/base/config']; - return !disableThirdPartyRequests && urlWhitelist.length > 0; + return !disableThirdPartyRequests && allowedUrlDomains.length > 0; } /** * Checks if you youtube URLs should be allowed for shared videos. * + * @param {Array} allowedUrlDomains - The allowed URL domains for shared video. * @returns {boolean} */ -export function areYoutubeURLsAllowedForSharedVideo() { - return URL_WHITELIST.includes(YOUTUBE_URL_DOMAIN); +export function areYoutubeURLsAllowedForSharedVideo(allowedUrlDomains?: Array) { + return Boolean(allowedUrlDomains?.includes(YOUTUBE_URL_DOMAIN)); } /** * Returns true if the passed url is allowed to be used for shared video or not. * - * @param {IStateful} stateful - The redux store, state, or - * {@code getState} function. * @param {string} url - The URL. + * @param {Array} allowedUrlDomains - The allowed url domains. + * @param {boolean} considerNonURLsAllowedForYoututbe - If true, the invalid URLs will be considered youtube IDs + * and if youtube is allowed the function will return true. * @returns {boolean} */ -export function isURLAllowedForSharedVideo(stateful: IStateful, url: string) { - const { urlWhitelist } = toState(stateful)['features/shared-video']; - +export function isURLAllowedForSharedVideo(url: string, + allowedUrlDomains: Array = [], considerNonURLsAllowedForYoututbe = false) { if (!url) { return false; } @@ -142,10 +136,10 @@ export function isURLAllowedForSharedVideo(stateful: IStateful, url: string) { const urlObject = new URL(url); if ([ 'http:', 'https:' ].includes(urlObject?.protocol?.toLowerCase())) { - return urlWhitelist?.includes(urlObject?.hostname); + return allowedUrlDomains.includes(ALLOW_ALL_URL_DOMAINS) || allowedUrlDomains.includes(urlObject?.hostname); } } catch (_e) { // it should be YouTube id. - return urlWhitelist?.includes(YOUTUBE_URL_DOMAIN); + return considerNonURLsAllowedForYoututbe && allowedUrlDomains.includes(YOUTUBE_URL_DOMAIN); } return false; diff --git a/react/features/shared-video/middleware.any.ts b/react/features/shared-video/middleware.any.ts index 909d53ec60..3fc81e3ae1 100644 --- a/react/features/shared-video/middleware.any.ts +++ b/react/features/shared-video/middleware.any.ts @@ -4,19 +4,27 @@ import { IStore } from '../app/types'; import { CONFERENCE_JOIN_IN_PROGRESS, CONFERENCE_LEFT } from '../base/conference/actionTypes'; import { getCurrentConference } from '../base/conference/functions'; import { IJitsiConference } from '../base/conference/reducer'; +import { SET_CONFIG } from '../base/config/actionTypes'; import { MEDIA_TYPE } from '../base/media/constants'; import { PARTICIPANT_LEFT } from '../base/participants/actionTypes'; import { participantJoined, participantLeft, pinParticipant } from '../base/participants/actions'; import { getLocalParticipant, getParticipantById } from '../base/participants/functions'; import { FakeParticipant } from '../base/participants/types'; import MiddlewareRegistry from '../base/redux/MiddlewareRegistry'; +import { SET_DYNAMIC_BRANDING_DATA } from '../dynamic-branding/actionTypes'; import { RESET_SHARED_VIDEO_STATUS, SET_SHARED_VIDEO_STATUS } from './actionTypes'; import { resetSharedVideoStatus, + setAllowedUrlDomians, setSharedVideoStatus } from './actions.any'; -import { PLAYBACK_STATUSES, SHARED_VIDEO, VIDEO_PLAYER_PARTICIPANT_NAME } from './constants'; +import { + DEFAULT_ALLOWED_URL_DOMAINS, + PLAYBACK_STATUSES, + SHARED_VIDEO, + VIDEO_PLAYER_PARTICIPANT_NAME +} from './constants'; import { isSharedVideoEnabled, isSharingStatus, isURLAllowedForSharedVideo } from './functions'; import logger from './logger'; @@ -30,22 +38,22 @@ import logger from './logger'; */ MiddlewareRegistry.register(store => next => action => { const { dispatch, getState } = store; - const state = getState(); - if (!isSharedVideoEnabled(state)) { + if (!isSharedVideoEnabled(getState())) { return next(action); } switch (action.type) { case CONFERENCE_JOIN_IN_PROGRESS: { const { conference } = action; - const localParticipantId = getLocalParticipant(state)?.id; + const localParticipantId = getLocalParticipant(getState())?.id; conference.addCommandListener(SHARED_VIDEO, ({ value, attributes }: { attributes: { from: string; muted: string; state: string; time: string; }; value: string; }) => { + const state = getState(); - if (!isURLAllowedForSharedVideo(state, value)) { + if (!isURLAllowedForSharedVideo(value, getState()['features/shared-video'].allowedUrlDomains, true)) { logger.debug(`Shared Video: Received a not allowed URL ${value}`); return; @@ -72,9 +80,11 @@ MiddlewareRegistry.register(store => next => action => { break; } case CONFERENCE_LEFT: + dispatch(setAllowedUrlDomians(DEFAULT_ALLOWED_URL_DOMAINS)); dispatch(resetSharedVideoStatus()); break; case PARTICIPANT_LEFT: { + const state = getState(); const conference = getCurrentConference(state); const { ownerId: stateOwnerId, videoUrl: statevideoUrl } = state['features/shared-video']; @@ -86,7 +96,23 @@ MiddlewareRegistry.register(store => next => action => { } break; } + case SET_CONFIG: + case SET_DYNAMIC_BRANDING_DATA: { + const result = next(action); + const state = getState(); + const { sharedVideoAllowedURLDomains: allowedURLDomainsFromConfig = [] } = state['features/base/config']; + const { sharedVideoAllowedURLDomains: allowedURLDomainsFromBranding = [] } = state['features/dynamic-branding']; + + dispatch(setAllowedUrlDomians([ + ...DEFAULT_ALLOWED_URL_DOMAINS, + ...allowedURLDomainsFromBranding, + ...allowedURLDomainsFromConfig + ])); + + return result; + } case SET_SHARED_VIDEO_STATUS: { + const state = getState(); const conference = getCurrentConference(state); const localParticipantId = getLocalParticipant(state)?.id; const { videoUrl, status, ownerId, time, muted, volume } = action; @@ -112,6 +138,7 @@ MiddlewareRegistry.register(store => next => action => { break; } case RESET_SHARED_VIDEO_STATUS: { + const state = getState(); const localParticipantId = getLocalParticipant(state)?.id; const { ownerId: stateOwnerId, videoUrl: statevideoUrl } = state['features/shared-video']; diff --git a/react/features/shared-video/reducer.ts b/react/features/shared-video/reducer.ts index 4c8008badd..f3bd9d9a40 100644 --- a/react/features/shared-video/reducer.ts +++ b/react/features/shared-video/reducer.ts @@ -2,21 +2,23 @@ import ReducerRegistry from '../base/redux/ReducerRegistry'; import { RESET_SHARED_VIDEO_STATUS, + SET_ALLOWED_URL_DOMAINS, SET_DISABLE_BUTTON, - SET_SHARED_VIDEO_STATUS, - SET_URL_WHITELIST + SET_SHARED_VIDEO_STATUS } from './actionTypes'; -import { YOUTUBE_URL_DOMAIN } from './constants'; +import { DEFAULT_ALLOWED_URL_DOMAINS } from './constants'; -const initialState = {}; +const initialState = { + allowedUrlDomains: DEFAULT_ALLOWED_URL_DOMAINS +}; export interface ISharedVideoState { + allowedUrlDomains: Array; disabled?: boolean; muted?: boolean; ownerId?: string; status?: string; time?: number; - urlWhitelist?: Array; videoUrl?: string; volume?: number; } @@ -30,7 +32,10 @@ ReducerRegistry.register('features/shared-video', switch (action.type) { case RESET_SHARED_VIDEO_STATUS: - return initialState; + return { + ...initialState, + allowedUrlDomains: state.allowedUrlDomains + }; case SET_SHARED_VIDEO_STATUS: return { ...state, @@ -48,10 +53,10 @@ ReducerRegistry.register('features/shared-video', disabled }; - case SET_URL_WHITELIST: { + case SET_ALLOWED_URL_DOMAINS: { return { ...state, - urlWhitelist: [ YOUTUBE_URL_DOMAIN, ...action.urlWhitelist ] + allowedUrlDomains: action.allowedUrlDomains }; }