From 8a2e4bc6281ee2d7a0602d359a03c8dc9c2c34e0 Mon Sep 17 00:00:00 2001 From: Duduman Bogdan Vlad Date: Mon, 16 Oct 2023 14:59:55 +0300 Subject: [PATCH] feat(screenshare) - add web security fix for electron (#13096) use send the share screen sources using the external api --------- Co-authored-by: Gabriel Borlea --- modules/API/API.js | 24 ++++ modules/API/external/external_api.js | 17 ++- react/features/app/reducers.web.ts | 1 + react/features/app/types.ts | 2 + react/features/desktop-picker/actionTypes.ts | 9 ++ react/features/desktop-picker/actions.ts | 26 +++++ .../components/DesktopPicker.tsx | 109 +++++++++++++----- .../components/DesktopSourcePreview.tsx | 44 ++++++- react/features/desktop-picker/constants.ts | 9 ++ react/features/desktop-picker/functions.ts | 42 ++++++- react/features/desktop-picker/reducer.ts | 34 ++++++ react/features/desktop-picker/types.ts | 14 +++ 12 files changed, 290 insertions(+), 41 deletions(-) create mode 100644 react/features/desktop-picker/actionTypes.ts create mode 100644 react/features/desktop-picker/constants.ts create mode 100644 react/features/desktop-picker/reducer.ts create mode 100644 react/features/desktop-picker/types.ts diff --git a/modules/API/API.js b/modules/API/API.js index 4ad13a086a..4d8e8bf222 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -67,6 +67,7 @@ import { toggleChat } from '../../react/features/chat/actions'; import { openChat } from '../../react/features/chat/actions.web'; +import { setDesktopSources } from '../../react/features/desktop-picker/actions'; import { processExternalDeviceRequest } from '../../react/features/device-selection/functions'; @@ -838,6 +839,16 @@ function initCommands() { }, 'toggle-whiteboard': () => { APP.store.dispatch(toggleWhiteboard()); + }, + '_request-desktop-sources-result': data => { + if (data.error) { + logger.error(`Error to retrieve desktop sources result, error data: ${data.error}`); + + return; + } + if (data.success?.data?.sources) { + APP.store.dispatch(setDesktopSources(data.success.data.sources)); + } } }; transport.on('event', ({ data, name }) => { @@ -1279,6 +1290,19 @@ class API { }); } + /** + * Notify request desktop sources. + * + * @param {Object} options - Object with the options for desktop sources. + * @returns {void} + */ + notifyRequestDesktopSources(options) { + this._sendEvent({ + name: '_request-desktop-sources', + options + }); + } + /** * Notify external application that the video quality setting has changed. * diff --git a/modules/API/external/external_api.js b/modules/API/external/external_api.js index 84070d886c..93bba44ffd 100644 --- a/modules/API/external/external_api.js +++ b/modules/API/external/external_api.js @@ -91,7 +91,8 @@ const commands = { toggleTileView: 'toggle-tile-view', toggleVirtualBackgroundDialog: 'toggle-virtual-background', toggleVideo: 'toggle-video', - toggleWhiteboard: 'toggle-whiteboard' + toggleWhiteboard: 'toggle-whiteboard', + _requestDesktopSourcesResult: '_request-desktop-sources-result' }; /** @@ -159,7 +160,8 @@ const events = { 'suspend-detected': 'suspendDetected', 'tile-view-changed': 'tileViewChanged', 'toolbar-button-clicked': 'toolbarButtonClicked', - 'whiteboard-status-changed': 'whiteboardStatusChanged' + 'whiteboard-status-changed': 'whiteboardStatusChanged', + '_request-desktop-sources': '_requestDesktopSources' }; /** @@ -1311,6 +1313,17 @@ export default class JitsiMeetExternalAPI extends EventEmitter { } } + /** + * Send request to request desktop sources. + * + * @returns {Promise} - Result. + */ + _requestDesktopSources() { + return this._transport.sendRequest({ + name: '_request-desktop-sources' + }); + } + /** * Passes an event along to the local conference participant to establish * or update a direct peer connection. This is currently used for developing diff --git a/react/features/app/reducers.web.ts b/react/features/app/reducers.web.ts index 071d0534ef..052f4539fc 100644 --- a/react/features/app/reducers.web.ts +++ b/react/features/app/reducers.web.ts @@ -1,6 +1,7 @@ import '../base/devices/reducer'; import '../base/premeeting/reducer'; import '../base/tooltip/reducer'; +import '../desktop-picker/reducer'; import '../e2ee/reducer'; import '../face-landmarks/reducer'; import '../feedback/reducer'; diff --git a/react/features/app/types.ts b/react/features/app/types.ts index 9293206b5c..75466bf38a 100644 --- a/react/features/app/types.ts +++ b/react/features/app/types.ts @@ -32,6 +32,7 @@ import { IBreakoutRoomsState } from '../breakout-rooms/reducer'; import { ICalendarSyncState } from '../calendar-sync/reducer'; import { IChatState } from '../chat/reducer'; import { IDeepLinkingState } from '../deep-linking/reducer'; +import { IDesktopPicker } from '../desktop-picker/reducer'; import { IDropboxState } from '../dropbox/reducer'; import { IDynamicBrandingState } from '../dynamic-branding/reducer'; import { IE2EEState } from '../e2ee/reducer'; @@ -123,6 +124,7 @@ export interface IReduxState { 'features/call-integration': ICallIntegrationState; 'features/chat': IChatState; 'features/deep-linking': IDeepLinkingState; + 'features/desktop-picker': IDesktopPicker; 'features/dropbox': IDropboxState; 'features/dynamic-branding': IDynamicBrandingState; 'features/e2ee': IE2EEState; diff --git a/react/features/desktop-picker/actionTypes.ts b/react/features/desktop-picker/actionTypes.ts new file mode 100644 index 0000000000..1cc83f6838 --- /dev/null +++ b/react/features/desktop-picker/actionTypes.ts @@ -0,0 +1,9 @@ +/** + * Action type to set the device sources. + */ +export const SET_DESKTOP_SOURCES = 'SET_DESKTOP_SOURCES'; + +/** + * Action type to DELETE_DESKTOP_SOURCES. + */ +export const DELETE_DESKTOP_SOURCES = 'DELETE_DESKTOP_SOURCES'; diff --git a/react/features/desktop-picker/actions.ts b/react/features/desktop-picker/actions.ts index c6e647209e..0e6f7ec064 100644 --- a/react/features/desktop-picker/actions.ts +++ b/react/features/desktop-picker/actions.ts @@ -1,6 +1,8 @@ import { openDialog } from '../base/dialog/actions'; +import { DELETE_DESKTOP_SOURCES, SET_DESKTOP_SOURCES } from './actionTypes'; import DesktopPicker from './components/DesktopPicker'; +import { _separateSourcesByType } from './functions'; /** * Signals to open a dialog with the DesktopPicker component. @@ -18,3 +20,27 @@ export function showDesktopPicker(options: { desktopSharingSources?: any; } = {} onSourceChoose }); } + +/** + * Signals to open a dialog with the DesktopPicker component with screen sharing sources. + * + * @param {Array} sources - Desktop capturer sources. + * @returns {Function} + */ +export function setDesktopSources(sources: Array) { + return { + type: SET_DESKTOP_SOURCES, + sources: _separateSourcesByType(sources ?? []) + }; +} + +/** + * Action used to delete desktop sources. + * + * @returns {Object} + */ +export function deleteDesktopSources() { + return { + type: DELETE_DESKTOP_SOURCES + }; +} diff --git a/react/features/desktop-picker/components/DesktopPicker.tsx b/react/features/desktop-picker/components/DesktopPicker.tsx index f108443c7b..f98e49b7ee 100644 --- a/react/features/desktop-picker/components/DesktopPicker.tsx +++ b/react/features/desktop-picker/components/DesktopPicker.tsx @@ -1,26 +1,24 @@ +import _ from 'lodash'; import React, { PureComponent } from 'react'; import { WithTranslation } from 'react-i18next'; import { connect } from 'react-redux'; -import { IStore } from '../../app/types'; +import { IReduxState, IStore } from '../../app/types'; import { hideDialog } from '../../base/dialog/actions'; import { translate } from '../../base/i18n/functions'; import Dialog from '../../base/ui/components/web/Dialog'; import Tabs from '../../base/ui/components/web/Tabs'; -import { obtainDesktopSources } from '../functions'; +import { deleteDesktopSources } from '../actions'; +import { THUMBNAIL_SIZE } from '../constants'; +import { + getDesktopPickerSources, + obtainDesktopSources, + oldJitsiMeetElectronUsage +} from '../functions'; +import { IDesktopSources } from '../types'; import DesktopPickerPane from './DesktopPickerPane'; -/** - * The size of the requested thumbnails. - * - * @type {Object} - */ -const THUMBNAIL_SIZE = { - height: 300, - width: 300 -}; - /** * The sources polling interval in ms. * @@ -47,6 +45,11 @@ const VALID_TYPES = Object.keys(TAB_LABELS); */ interface IProps extends WithTranslation { + /** + * An object containing all the DesktopCapturerSources. + */ + _sources: IDesktopSources; + /** * An array with desktop sharing sources to be displayed. */ @@ -182,6 +185,29 @@ class DesktopPicker extends PureComponent { this._stopPolling(); } + /** + * Clean up component and DesktopCapturerSource store state. + * + * @inheritdoc + */ + componentDidUpdate(prevProps: IProps) { + // skip logic if old jitsi meet electron used. + if (oldJitsiMeetElectronUsage()) { + return; + } + + if (this.props._sources && !_.isEqual(this.props._sources, prevProps._sources)) { + const selectedSource = this._getSelectedSource(this.props._sources); + + // update state with latest thumbnail desktop sources + this.setState({ + sources: this.props._sources, + selectedSource + }); + } + } + + /** * Implements React's {@link Component#render()}. * @@ -282,6 +308,7 @@ class DesktopPicker extends PureComponent { _onCloseModal(id = '', type?: string, screenShareAudio = false) { this.props.onSourceChoose(id, type, screenShareAudio); this.props.dispatch(hideDialog()); + this.props.dispatch(deleteDesktopSources()); } /** @@ -322,14 +349,14 @@ class DesktopPicker extends PureComponent { _onTabSelected(id: string) { const { sources } = this.state; - this._selectedTabType = id; - // When we change tabs also reset the screenShareAudio state so we don't // use the option from one tab when sharing from another. this.setState({ screenShareAudio: false, selectedSource: this._getSelectedSource(sources), - selectedTab: id + + // select type `window` or `screen` from id + selectedTab: id.split('-')[0] }); } @@ -406,24 +433,44 @@ class DesktopPicker extends PureComponent { _updateSources() { const { types } = this.state; - if (types.length > 0) { - obtainDesktopSources( - this.state.types, - { thumbnailSize: THUMBNAIL_SIZE } - ) - .then((sources: any) => { - const selectedSource = this._getSelectedSource(sources); + if (oldJitsiMeetElectronUsage()) { - // TODO: Maybe check if we have stopped the timer and unmounted - // the component. - this.setState({ - sources, - selectedSource - }); - }) - .catch(() => { /* ignore */ }); + if (types.length > 0) { + obtainDesktopSources( + this.state.types, + { thumbnailSize: THUMBNAIL_SIZE } + ) + .then((sources: any) => { + const selectedSource = this._getSelectedSource(sources); + + this.setState({ + sources, + selectedSource + }); + }) + .catch(() => { /* ignore */ }); + } + } else { + APP.API.notifyRequestDesktopSources({ + types, + thumbnailSize: THUMBNAIL_SIZE + }); } } } -export default translate(connect()(DesktopPicker)); +/** + * Maps (parts of) the redux state to the React props. + * + * @param {Object} state - The redux state. + * @returns {{ +* _sources: IDesktopPicker +* }} +*/ +function _mapStateToProps(state: IReduxState) { + return { + _sources: getDesktopPickerSources(state) + }; +} + +export default translate(connect(_mapStateToProps)(DesktopPicker)); diff --git a/react/features/desktop-picker/components/DesktopSourcePreview.tsx b/react/features/desktop-picker/components/DesktopSourcePreview.tsx index 00259c2fb1..60c3f66bcd 100644 --- a/react/features/desktop-picker/components/DesktopSourcePreview.tsx +++ b/react/features/desktop-picker/components/DesktopSourcePreview.tsx @@ -74,12 +74,7 @@ class DesktopSourcePreview extends Component { className = { displayClasses } onClick = { this._onClick } onDoubleClick = { this._onDoubleClick }> -
- { -
+ {this._renderThumbnailImageContainer()}
{ this.props.source.name }
@@ -87,6 +82,43 @@ class DesktopSourcePreview extends Component { ); } + /** + * Render thumbnail screenshare image. + * + * @returns {Object} - Thumbnail image. + */ + _renderThumbnailImageContainer() { + // default data URL for thumnbail image + let srcImage = this.props.source.thumbnail.dataUrl; + + // legacy thumbnail image + if (typeof this.props.source.thumbnail.toDataURL === 'function') { + srcImage = this.props.source.thumbnail.toDataURL(); + } + + return ( +
+ { this._renderThumbnailImage(srcImage) } +
+ ); + + } + + /** + * Render thumbnail screenshare image. + * + * @param {string} src - Of the image. + * @returns {Object} - Thumbnail image. + */ + _renderThumbnailImage(src: string) { + return ( + { + ); + } + /** * Invokes the passed in onClick callback. * diff --git a/react/features/desktop-picker/constants.ts b/react/features/desktop-picker/constants.ts new file mode 100644 index 0000000000..0233d5f0e6 --- /dev/null +++ b/react/features/desktop-picker/constants.ts @@ -0,0 +1,9 @@ +/** + * The size of the requested thumbnails. + * + * @type {Object} + */ +export const THUMBNAIL_SIZE = { + height: 300, + width: 300 +}; diff --git a/react/features/desktop-picker/functions.ts b/react/features/desktop-picker/functions.ts index 7be9eaf3fd..11a3e62500 100644 --- a/react/features/desktop-picker/functions.ts +++ b/react/features/desktop-picker/functions.ts @@ -1,4 +1,28 @@ +import { IReduxState } from '../app/types'; + import logger from './logger'; +import { ElectronWindowType } from './types'; + +/** + * Returns root conference state. + * + * @param {IReduxState} state - Global state. + * @returns {Object} Conference state. + */ +export const getDesktopPicker = (state: IReduxState) => state['features/desktop-picker']; + +/** +* Selector to return a list of knocking participants. +* +* @param {IReduxState} state - State object. +* @returns {IDesktopSources} +*/ +export function getDesktopPickerSources(state: IReduxState) { + const root = getDesktopPicker(state); + + return root.sources; +} + /** * Begins a request to get available DesktopCapturerSources. @@ -20,7 +44,7 @@ export function obtainDesktopSources(types: string[], options: { thumbnailSize?: } return new Promise((resolve, reject) => { - const { JitsiMeetElectron } = window; + const { JitsiMeetElectron } = window as ElectronWindowType; if (JitsiMeetElectron?.obtainDesktopStreams) { JitsiMeetElectron.obtainDesktopStreams( @@ -43,6 +67,20 @@ export function obtainDesktopSources(types: string[], options: { thumbnailSize?: }); } +/** + * Check usage of old jitsi meet electron version. + * + * @returns {boolean} True if we use old jitsi meet electron, otherwise false. + */ +export function oldJitsiMeetElectronUsage() { + const { JitsiMeetElectron } = window as ElectronWindowType; + + if (JitsiMeetElectron?.obtainDesktopStreams) { + return true; + } + + return false; +} /** * Converts an array of DesktopCapturerSources to an object with types for keys @@ -53,7 +91,7 @@ export function obtainDesktopSources(types: string[], options: { thumbnailSize?: * @returns {Object} An object with the sources split into separate arrays based * on source type. */ -function _separateSourcesByType(sources: Array<{ id: string; }> = []) { +export function _separateSourcesByType(sources: Array<{ id: string; }> = []) { const sourcesByType: any = { screen: [], window: [] diff --git a/react/features/desktop-picker/reducer.ts b/react/features/desktop-picker/reducer.ts new file mode 100644 index 0000000000..7b528d1e70 --- /dev/null +++ b/react/features/desktop-picker/reducer.ts @@ -0,0 +1,34 @@ +import ReducerRegistry from '../base/redux/ReducerRegistry'; + +import { DELETE_DESKTOP_SOURCES, SET_DESKTOP_SOURCES } from './actionTypes'; +import { IDesktopSources } from './types'; + +/** + * The initial state of the web-hid feature. +*/ +const DEFAULT_STATE: IDesktopPicker = { + sources: {} as IDesktopSources +}; + +export interface IDesktopPicker { + sources: IDesktopSources; +} + +ReducerRegistry.register( +'features/desktop-picker', +(state: IDesktopPicker = DEFAULT_STATE, action): IDesktopPicker => { + switch (action.type) { + case SET_DESKTOP_SOURCES: + return { + ...state, + sources: action.sources + }; + case DELETE_DESKTOP_SOURCES: + return { + ...state, + ...DEFAULT_STATE + }; + default: + return state; + } +}); diff --git a/react/features/desktop-picker/types.ts b/react/features/desktop-picker/types.ts new file mode 100644 index 0000000000..29bb0d59ff --- /dev/null +++ b/react/features/desktop-picker/types.ts @@ -0,0 +1,14 @@ +export interface IDesktopSources { + sources: ISourcesByType; +} + +export interface ISourcesByType { + screen: []; + window: []; +} + +export type ElectronWindowType = { + JitsiMeetElectron?: { + obtainDesktopStreams: Function; + } ; +} & typeof window;