Skip to content

Commit

Permalink
feat(shared-video): Allow only whitelisted URLs.
Browse files Browse the repository at this point in the history
  • Loading branch information
hristoterezov committed Aug 13, 2024
1 parent b352006 commit ad41017
Show file tree
Hide file tree
Showing 13 changed files with 146 additions and 17 deletions.
2 changes: 1 addition & 1 deletion lang/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@
"shareVideoTitle": "Share video",
"shareYourScreen": "Share your screen",
"shareYourScreenDisabled": "Screen sharing disabled.",
"sharedVideoDialogError": "Error: Invalid URL",
"sharedVideoDialogError": "Error: Invalid or forbidden URL",
"sharedVideoLinkPlaceholder": "YouTube link or direct video link",
"show": "Show",
"start": "Start ",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
isTrackStreamingStatusInactive
} from '../../../connection-indicator/functions';
import SharedVideo from '../../../shared-video/components/native/SharedVideo';
import { isSharedVideoEnabled } from '../../../shared-video/functions';
import { IStateful } from '../../app/types';
import Avatar from '../../avatar/components/Avatar';
import { translate } from '../../i18n/functions';
Expand Down Expand Up @@ -236,7 +237,8 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
_isConnectionInactive: isTrackStreamingStatusInactive(videoTrack),
_isSharedVideoParticipant: isSharedVideoParticipant(participant),
_participantName: getParticipantDisplayName(state, participantId),
_renderVideo: shouldRenderParticipantVideo(state, participantId) && !disableVideo,
_renderVideo: shouldRenderParticipantVideo(state, participantId) && !disableVideo
&& isSharedVideoEnabled(state),
_videoTrack: videoTrack
};
}
Expand Down
4 changes: 4 additions & 0 deletions react/features/shared-video/actions.any.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getLocalParticipant } from '../base/participants/functions';

import { RESET_SHARED_VIDEO_STATUS, SET_SHARED_VIDEO_STATUS } from './actionTypes';
import { SharedVideoDialog } from './components';
import { isSharedVideoEnabled, isURLAllowedForSharedVideo } from './functions';

/**
* Resets the status of the shared video.
Expand Down Expand Up @@ -89,6 +90,9 @@ export function stopSharedVideo() {
*/
export function playSharedVideo(videoUrl: string) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
if (!isSharedVideoEnabled(getState()) || !isURLAllowedForSharedVideo(videoUrl)) {
return;
}
const conference = getCurrentConference(getState());

if (conference) {
Expand Down
1 change: 1 addition & 0 deletions react/features/shared-video/components/index.native.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
// @ts-ignore
export { default as SharedVideoDialog } from './native/SharedVideoDialog';
export { default as SharedVideoButton } from './native/SharedVideoButton';
1 change: 1 addition & 0 deletions react/features/shared-video/components/index.web.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default as SharedVideoDialog } from './web/SharedVideoDialog';
export { default as SharedVideoButton } from './web/SharedVideoButton';
14 changes: 13 additions & 1 deletion react/features/shared-video/components/web/SharedVideo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { IReduxState } from '../../../app/types';
import { getLocalParticipant } from '../../../base/participants/functions';
import { getVerticalViewMaxWidth } from '../../../filmstrip/functions.web';
import { getToolboxHeight } from '../../../toolbox/functions.web';
import { isSharedVideoEnabled } from '../../functions';

import VideoManager from './VideoManager';
import YoutubeVideoManager from './YoutubeVideoManager';
Expand All @@ -33,6 +34,11 @@ interface IProps {
*/
filmstripWidth: number;

/**
* Whether the shared video is enabled or not.
*/
isEnabled: boolean;

/**
* Is the video shared by the local user.
*/
Expand Down Expand Up @@ -118,7 +124,12 @@ class SharedVideo extends Component<IProps> {
* @returns {React$Element}
*/
render() {
const { isOwner, isResizing } = this.props;
const { isEnabled, isOwner, isResizing } = this.props;

if (!isEnabled) {
return null;
}

const className = !isResizing && isOwner ? '' : 'disable-pointer';

return (
Expand Down Expand Up @@ -152,6 +163,7 @@ function _mapStateToProps(state: IReduxState) {
clientWidth,
filmstripVisible: visible,
filmstripWidth: getVerticalViewMaxWidth(state),
isEnabled: isSharedVideoEnabled(state),
isOwner: ownerId === localParticipant?.id,
isResizing,
videoUrl
Expand Down
10 changes: 10 additions & 0 deletions react/features/shared-video/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,13 @@ export const PLAYBACK_STATUSES = {
PAUSED: 'pause',
STOPPED: 'stop'
};

/**
* The domain for youtube URLs.
*/
export const YOUTUBE_URL_DOMAIN = 'youtube.com';

/**
* The white listed domains for shared video.
*/
export const URL_WHITELIST = [ YOUTUBE_URL_DOMAIN ];
67 changes: 62 additions & 5 deletions react/features/shared-video/functions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { IStateful } from '../base/app/types';
import { getFakeParticipants } from '../base/participants/functions';
import { toState } from '../base/redux/functions';

import { VIDEO_PLAYER_PARTICIPANT_NAME, YOUTUBE_PLAYER_PARTICIPANT_NAME } from './constants';
import {
URL_WHITELIST,
VIDEO_PLAYER_PARTICIPANT_NAME,
YOUTUBE_PLAYER_PARTICIPANT_NAME,
YOUTUBE_URL_DOMAIN
} from './constants';

/**
* Validates the entered video url.
Expand Down Expand Up @@ -70,20 +76,71 @@ export function extractYoutubeIdOrURL(input: string) {
return;
}

const youtubeId = getYoutubeId(trimmedLink);
if (areYoutubeURLsAllowedForSharedVideo()) {
const youtubeId = getYoutubeId(trimmedLink);

if (youtubeId) {
return youtubeId;
if (youtubeId) {
return youtubeId;
}
}

// Check if the URL is valid, native may crash otherwise.
try {
// eslint-disable-next-line no-new
new URL(trimmedLink);
const url = new URL(trimmedLink);

if (!URL_WHITELIST.includes(url?.hostname)) {
return;
}
} catch (_) {
return;
}

return trimmedLink;
}

/**
* Returns true if shared video functionality is enabled and false otherwise.
*
* @param {IStateful} stateful - - The redux store or {@code getState} function.
* @returns {boolean}
*/
export function isSharedVideoEnabled(stateful: IStateful) {
const state = toState(stateful);
const { disableThirdPartyRequests = false } = state['features/base/config'];

return !disableThirdPartyRequests && URL_WHITELIST.length > 0;
}

/**
* Checks if you youtube URLs should be allowed for shared videos.
*
* @returns {boolean}
*/
export function areYoutubeURLsAllowedForSharedVideo() {
return URL_WHITELIST.includes(YOUTUBE_URL_DOMAIN);
}

/**
* Returns true if the passed url is allowed to be used for shared video or not.
*
* @param {string} url - The URL.
* @returns {boolean}
*/
export function isURLAllowedForSharedVideo(url: string) {
if (!url) {
return false;
}

try {
const urlObject = new URL(url);

if ([ 'http:', 'https:' ].includes(urlObject?.protocol?.toLowerCase())) {
return URL_WHITELIST.includes(urlObject?.hostname);
}
} catch (_e) { // it should be youtube id.
return areYoutubeURLsAllowedForSharedVideo();
}

return false;
}
24 changes: 24 additions & 0 deletions react/features/shared-video/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useSelector } from 'react-redux';

import { SharedVideoButton } from './components';
import { isSharedVideoEnabled } from './functions';

const shareVideo = {
key: 'sharedvideo',
Content: SharedVideoButton,
group: 3
};

/**
* A hook that returns the shared video button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
export function useSharedVideoButton() {
const sharedVideoEnabled = useSelector(isSharedVideoEnabled);

if (sharedVideoEnabled) {
return shareVideo;
}
}

12 changes: 11 additions & 1 deletion react/features/shared-video/middleware.any.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
setSharedVideoStatus
} from './actions.any';
import { PLAYBACK_STATUSES, SHARED_VIDEO, VIDEO_PLAYER_PARTICIPANT_NAME } from './constants';
import { isSharingStatus } from './functions';
import { isSharedVideoEnabled, isSharingStatus, isURLAllowedForSharedVideo } from './functions';
import logger from './logger';


Expand All @@ -32,6 +32,10 @@ MiddlewareRegistry.register(store => next => action => {
const { dispatch, getState } = store;
const state = getState();

if (!isSharedVideoEnabled(state)) {
return next(action);
}

switch (action.type) {
case CONFERENCE_JOIN_IN_PROGRESS: {
const { conference } = action;
Expand All @@ -41,6 +45,12 @@ MiddlewareRegistry.register(store => next => action => {
({ value, attributes }: { attributes: {
from: string; muted: string; state: string; time: string; }; value: string; }) => {

if (!isURLAllowedForSharedVideo(value)) {
logger.debug(`Shared Video: Received a not allowed URL ${value}`);

return;
}

const { from } = attributes;
const sharedVideoStatus = attributes.state;

Expand Down
5 changes: 5 additions & 0 deletions react/features/shared-video/middleware.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';

import { setDisableButton } from './actions.web';
import { SHARED_VIDEO } from './constants';
import { isSharedVideoEnabled } from './functions';

import './middleware.any';

Expand All @@ -13,6 +14,10 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {

switch (action.type) {
case CONFERENCE_JOIN_IN_PROGRESS: {
if (!isSharedVideoEnabled(state)) {
break;
}

const { conference } = action;

conference.addCommandListener(SHARED_VIDEO, ({ attributes }: { attributes:
Expand Down
10 changes: 9 additions & 1 deletion react/features/toolbox/components/native/OverflowMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import RecordButton from '../../../recording/components/Recording/native/RecordB
import SecurityDialogButton
from '../../../security/components/security-dialog/native/SecurityDialogButton';
import SharedVideoButton from '../../../shared-video/components/native/SharedVideoButton';
import { isSharedVideoEnabled } from '../../../shared-video/functions';
import SpeakerStatsButton from '../../../speaker-stats/components/native/SpeakerStatsButton';
import { isSpeakerStatsDisabled } from '../../../speaker-stats/functions';
import ClosedCaptionButton from '../../../subtitles/components/native/ClosedCaptionButton';
Expand Down Expand Up @@ -55,6 +56,11 @@ interface IProps {
*/
_isOpen: boolean;

/**
* Whether the shared video is enabled or not.
*/
_isSharedVideoEnabled: boolean;

/**
* Whether or not speaker stats is disable.
*/
Expand Down Expand Up @@ -121,6 +127,7 @@ class OverflowMenu extends PureComponent<IProps, IState> {
const {
_isBreakoutRoomsSupported,
_isSpeakerStatsDisabled,
_isSharedVideoEnabled,
_shouldDisplayReactionsButtons,
_width,
dispatch
Expand Down Expand Up @@ -168,7 +175,7 @@ class OverflowMenu extends PureComponent<IProps, IState> {
<WhiteboardButton { ...buttonProps } />
{/* @ts-ignore */}
<Divider style = { styles.divider as ViewStyle } />
<SharedVideoButton { ...buttonProps } />
{_isSharedVideoEnabled && <SharedVideoButton { ...buttonProps } />}
{!toolbarButtons.has('screensharing') && <ScreenSharingButton { ...buttonProps } />}
{!_isSpeakerStatsDisabled && <SpeakerStatsButton { ...buttonProps } />}
{!toolbarButtons.has('tileview') && <TileViewButton { ...buttonProps } />}
Expand Down Expand Up @@ -255,6 +262,7 @@ function _mapStateToProps(state: IReduxState) {
return {
_customToolbarButtons: customToolbarButtons,
_isBreakoutRoomsSupported: conference?.getBreakoutRooms()?.isSupported(),
_isSharedVideoEnabled: isSharedVideoEnabled(state),
_isSpeakerStatsDisabled: isSpeakerStatsDisabled(state),
_shouldDisplayReactionsButtons: shouldDisplayReactionsButtons(state),
_width: state['features/base/responsive-ui'].clientWidth
Expand Down
9 changes: 2 additions & 7 deletions react/features/toolbox/hooks.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import ShareAudioButton from '../screen-share/components/web/ShareAudioButton';
import { isScreenAudioSupported, isScreenVideoShared } from '../screen-share/functions';
import { useSecurityDialogButton } from '../security/hooks.web';
import SettingsButton from '../settings/components/web/SettingsButton';
import SharedVideoButton from '../shared-video/components/web/SharedVideoButton';
import { useSharedVideoButton } from '../shared-video/hooks';
import SpeakerStats from '../speaker-stats/components/web/SpeakerStats';
import { isSpeakerStatsDisabled } from '../speaker-stats/functions';
import { useSpeakerStatsButton } from '../speaker-stats/hooks.web';
Expand Down Expand Up @@ -142,12 +142,6 @@ const linkToSalesforce = {
group: 2
};

const shareVideo = {
key: 'sharedvideo',
Content: SharedVideoButton,
group: 3
};

const shareAudio = {
key: 'shareaudio',
Content: ShareAudioButton,
Expand Down Expand Up @@ -288,6 +282,7 @@ export function useToolboxButtons(
const liveStreaming = useLiveStreamingButton();
const linktosalesforce = useLinkToSalesforceButton();
const shareaudio = getShareAudioButton();
const shareVideo = useSharedVideoButton();
const whiteboard = useWhiteboardButton();
const etherpad = useEtherpadButton();
const virtualBackground = useVirtualBackgroundButton();
Expand Down

0 comments on commit ad41017

Please sign in to comment.