You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

middleware.ts 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. import { batch } from 'react-redux';
  2. import { IStore } from '../app/types';
  3. import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
  4. import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
  5. import { getCurrentConference } from '../base/conference/functions';
  6. import { openDialog } from '../base/dialog/actions';
  7. import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
  8. import { PARTICIPANT_JOINED, PARTICIPANT_LEFT, PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
  9. import { participantUpdated } from '../base/participants/actions';
  10. import {
  11. getLocalParticipant,
  12. getParticipantById,
  13. getParticipantCount,
  14. getRemoteParticipants,
  15. isScreenShareParticipant
  16. } from '../base/participants/functions';
  17. import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
  18. import StateListenerRegistry from '../base/redux/StateListenerRegistry';
  19. import { playSound, registerSound, unregisterSound } from '../base/sounds/actions';
  20. import { PARTICIPANT_VERIFIED, SET_MEDIA_ENCRYPTION_KEY, START_VERIFICATION, TOGGLE_E2EE } from './actionTypes';
  21. import { setE2EEMaxMode, setEveryoneEnabledE2EE, setEveryoneSupportE2EE, toggleE2EE } from './actions';
  22. import ParticipantVerificationDialog from './components/ParticipantVerificationDialog';
  23. import { E2EE_OFF_SOUND_ID, E2EE_ON_SOUND_ID, MAX_MODE } from './constants';
  24. import { isMaxModeReached, isMaxModeThresholdReached } from './functions';
  25. import logger from './logger';
  26. import { E2EE_OFF_SOUND_FILE, E2EE_ON_SOUND_FILE } from './sounds';
  27. /**
  28. * Middleware that captures actions related to E2EE.
  29. *
  30. * @param {Store} store - The redux store.
  31. * @returns {Function}
  32. */
  33. MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
  34. const conference = getCurrentConference(getState);
  35. switch (action.type) {
  36. case APP_WILL_MOUNT:
  37. dispatch(registerSound(
  38. E2EE_OFF_SOUND_ID,
  39. E2EE_OFF_SOUND_FILE));
  40. dispatch(registerSound(
  41. E2EE_ON_SOUND_ID,
  42. E2EE_ON_SOUND_FILE));
  43. break;
  44. case APP_WILL_UNMOUNT:
  45. dispatch(unregisterSound(E2EE_OFF_SOUND_ID));
  46. dispatch(unregisterSound(E2EE_ON_SOUND_ID));
  47. break;
  48. case CONFERENCE_JOINED:
  49. _updateMaxMode(dispatch, getState);
  50. break;
  51. case PARTICIPANT_UPDATED: {
  52. const { id, e2eeEnabled, e2eeSupported } = action.participant;
  53. const oldParticipant = getParticipantById(getState(), id);
  54. const result = next(action);
  55. if (e2eeEnabled !== oldParticipant?.e2eeEnabled
  56. || e2eeSupported !== oldParticipant?.e2eeSupported) {
  57. const state = getState();
  58. let newEveryoneSupportE2EE = true;
  59. let newEveryoneEnabledE2EE = true;
  60. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  61. for (const [ key, p ] of getRemoteParticipants(state)) {
  62. if (!p.e2eeEnabled) {
  63. newEveryoneEnabledE2EE = false;
  64. }
  65. if (!p.e2eeSupported) {
  66. newEveryoneSupportE2EE = false;
  67. }
  68. if (!newEveryoneEnabledE2EE && !newEveryoneSupportE2EE) {
  69. break;
  70. }
  71. }
  72. if (!getLocalParticipant(state)?.e2eeEnabled) {
  73. newEveryoneEnabledE2EE = false;
  74. }
  75. batch(() => {
  76. dispatch(setEveryoneEnabledE2EE(newEveryoneEnabledE2EE));
  77. dispatch(setEveryoneSupportE2EE(newEveryoneSupportE2EE));
  78. });
  79. }
  80. return result;
  81. }
  82. case PARTICIPANT_JOINED: {
  83. const result = next(action);
  84. const { e2eeEnabled, e2eeSupported, local } = action.participant;
  85. const { everyoneEnabledE2EE } = getState()['features/e2ee'];
  86. const participantCount = getParticipantCount(getState);
  87. if (isScreenShareParticipant(action.participant)) {
  88. return result;
  89. }
  90. // the initial values
  91. if (participantCount === 1) {
  92. batch(() => {
  93. dispatch(setEveryoneEnabledE2EE(e2eeEnabled));
  94. dispatch(setEveryoneSupportE2EE(e2eeSupported));
  95. });
  96. }
  97. // if all had it enabled and this one disabled it, change value in store
  98. // otherwise there is no change in the value we store
  99. if (everyoneEnabledE2EE && !e2eeEnabled) {
  100. dispatch(setEveryoneEnabledE2EE(false));
  101. }
  102. if (local) {
  103. return result;
  104. }
  105. const { everyoneSupportE2EE } = getState()['features/e2ee'];
  106. // if all supported it and this one does not, change value in store
  107. // otherwise there is no change in the value we store
  108. if (everyoneSupportE2EE && !e2eeSupported) {
  109. dispatch(setEveryoneSupportE2EE(false));
  110. }
  111. _updateMaxMode(dispatch, getState);
  112. return result;
  113. }
  114. case PARTICIPANT_LEFT: {
  115. const previosState = getState();
  116. const participant = getParticipantById(previosState, action.participant?.id);
  117. const result = next(action);
  118. const newState = getState();
  119. const { e2eeEnabled = false, e2eeSupported = false } = participant ?? {};
  120. if (isScreenShareParticipant(participant)) {
  121. return result;
  122. }
  123. const { everyoneEnabledE2EE, everyoneSupportE2EE } = newState['features/e2ee'];
  124. // if it was not enabled by everyone, and the participant leaving had it disabled, or if it was not supported
  125. // by everyone, and the participant leaving had it not supported let's check is it enabled for all that stay
  126. if ((!everyoneEnabledE2EE && !e2eeEnabled) || (!everyoneSupportE2EE && !e2eeSupported)) {
  127. let latestEveryoneEnabledE2EE = true;
  128. let latestEveryoneSupportE2EE = true;
  129. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  130. for (const [ key, p ] of getRemoteParticipants(newState)) {
  131. if (!p.e2eeEnabled) {
  132. latestEveryoneEnabledE2EE = false;
  133. }
  134. if (!p.e2eeSupported) {
  135. latestEveryoneSupportE2EE = false;
  136. }
  137. if (!latestEveryoneEnabledE2EE && !latestEveryoneSupportE2EE) {
  138. break;
  139. }
  140. }
  141. if (!getLocalParticipant(newState)?.e2eeEnabled) {
  142. latestEveryoneEnabledE2EE = false;
  143. }
  144. batch(() => {
  145. if (!everyoneEnabledE2EE && latestEveryoneEnabledE2EE) {
  146. dispatch(setEveryoneEnabledE2EE(true));
  147. }
  148. if (!everyoneSupportE2EE && latestEveryoneSupportE2EE) {
  149. dispatch(setEveryoneSupportE2EE(true));
  150. }
  151. });
  152. }
  153. _updateMaxMode(dispatch, getState);
  154. return result;
  155. }
  156. case TOGGLE_E2EE: {
  157. if (conference?.isE2EESupported() && conference.isE2EEEnabled() !== action.enabled) {
  158. logger.debug(`E2EE will be ${action.enabled ? 'enabled' : 'disabled'}`);
  159. conference.toggleE2EE(action.enabled);
  160. // Broadcast that we enabled / disabled E2EE.
  161. const participant = getLocalParticipant(getState);
  162. dispatch(participantUpdated({
  163. e2eeEnabled: action.enabled,
  164. id: participant?.id ?? '',
  165. local: true
  166. }));
  167. const soundID = action.enabled ? E2EE_ON_SOUND_ID : E2EE_OFF_SOUND_ID;
  168. dispatch(playSound(soundID));
  169. }
  170. break;
  171. }
  172. case SET_MEDIA_ENCRYPTION_KEY: {
  173. if (conference?.isE2EESupported()) {
  174. const { exportedKey, index } = action.keyInfo;
  175. if (exportedKey) {
  176. window.crypto.subtle.importKey(
  177. 'raw',
  178. new Uint8Array(exportedKey),
  179. 'AES-GCM',
  180. false,
  181. [ 'encrypt', 'decrypt' ])
  182. .then(
  183. encryptionKey => {
  184. conference.setMediaEncryptionKey({
  185. encryptionKey,
  186. index
  187. });
  188. })
  189. .catch(error => logger.error('SET_MEDIA_ENCRYPTION_KEY error', error));
  190. } else {
  191. conference.setMediaEncryptionKey({
  192. encryptionKey: false,
  193. index
  194. });
  195. }
  196. }
  197. break;
  198. }
  199. case PARTICIPANT_VERIFIED: {
  200. const { isVerified, pId } = action;
  201. conference?.markParticipantVerified(pId, isVerified);
  202. break;
  203. }
  204. case START_VERIFICATION: {
  205. conference?.startVerification(action.pId);
  206. break;
  207. }
  208. }
  209. return next(action);
  210. });
  211. /**
  212. * Set up state change listener to perform maintenance tasks when the conference
  213. * is left or failed.
  214. */
  215. StateListenerRegistry.register(
  216. state => getCurrentConference(state),
  217. (conference, { dispatch }, previousConference) => {
  218. if (previousConference) {
  219. dispatch(toggleE2EE(false));
  220. }
  221. if (conference) {
  222. conference.on(JitsiConferenceEvents.E2EE_VERIFICATION_AVAILABLE, (pId: string) => {
  223. dispatch(participantUpdated({
  224. e2eeVerificationAvailable: true,
  225. id: pId
  226. }));
  227. });
  228. conference.on(JitsiConferenceEvents.E2EE_VERIFICATION_READY, (pId: string, sas: object) => {
  229. dispatch(openDialog(ParticipantVerificationDialog, { pId,
  230. sas }));
  231. });
  232. conference.on(JitsiConferenceEvents.E2EE_VERIFICATION_COMPLETED,
  233. (pId: string, success: boolean, message: string) => {
  234. if (message) {
  235. logger.warn('E2EE_VERIFICATION_COMPLETED warning', message);
  236. }
  237. dispatch(participantUpdated({
  238. e2eeVerified: success,
  239. id: pId
  240. }));
  241. });
  242. }
  243. });
  244. /**
  245. * Sets the maxMode based on the number of participants in the conference.
  246. *
  247. * @param { Dispatch<any>} dispatch - The redux dispatch function.
  248. * @param {Function|Object} getState - The {@code getState} function.
  249. * @private
  250. * @returns {void}
  251. */
  252. function _updateMaxMode(dispatch: IStore['dispatch'], getState: IStore['getState']) {
  253. const state = getState();
  254. const { e2ee = {} } = state['features/base/config'];
  255. if (e2ee.externallyManagedKey) {
  256. return;
  257. }
  258. if (isMaxModeThresholdReached(state)) {
  259. dispatch(setE2EEMaxMode(MAX_MODE.THRESHOLD_EXCEEDED));
  260. dispatch(toggleE2EE(false));
  261. } else if (isMaxModeReached(state)) {
  262. dispatch(setE2EEMaxMode(MAX_MODE.ENABLED));
  263. } else {
  264. dispatch(setE2EEMaxMode(MAX_MODE.DISABLED));
  265. }
  266. }