diff --git a/lib/selectors/account-selectors.js b/lib/selectors/account-selectors.js index 4aef2fdcc..4b18040d1 100644 --- a/lib/selectors/account-selectors.js +++ b/lib/selectors/account-selectors.js @@ -1,97 +1,96 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import { cookieSelector, sessionIDSelector, deviceTokensSelector, } from './keyserver-selectors.js'; import { currentCalendarQuery } from './nav-selectors.js'; import type { LogInExtraInfo } from '../types/account-types.js'; import type { CalendarQuery } from '../types/entry-types.js'; import type { KeyserverInfos } from '../types/keyserver-types.js'; import type { AppState } from '../types/redux-types.js'; import type { PreRequestUserState, PreRequestUserKeyserverSessionInfo, } from '../types/session-types.js'; import type { CurrentUserInfo } from '../types/user-types.js'; const logInExtraInfoSelector: ( state: AppState, ) => (calendarActive: boolean) => LogInExtraInfo = createSelector( deviceTokensSelector, currentCalendarQuery, ( deviceTokens: { +[keyserverID: string]: ?string }, calendarQuery: (calendarActive: boolean) => CalendarQuery, ) => { const deviceTokenUpdateRequest = {}; for (const keyserverID in deviceTokens) { if (deviceTokens[keyserverID]) { deviceTokenUpdateRequest[keyserverID] = { deviceToken: deviceTokens[keyserverID], }; } } // Return a function since we depend on the time of evaluation return (calendarActive: boolean): LogInExtraInfo => ({ calendarQuery: calendarQuery(calendarActive), deviceTokenUpdateRequest, }); }, ); const basePreRequestUserStateForSingleKeyserverSelector: ( keyserverID: string, - // eslint-disable-next-line no-unused-vars ) => (state: AppState) => PreRequestUserState = keyserverID => createSelector( (state: AppState) => state.currentUserInfo, cookieSelector, - sessionIDSelector, + sessionIDSelector(keyserverID), ( currentUserInfo: ?CurrentUserInfo, cookie: ?string, sessionID: ?string, ) => ({ currentUserInfo, cookiesAndSessions: { [keyserverID]: { cookie, sessionID } }, }), ); const preRequestUserStateForSingleKeyserverSelector: ( keyserverID: string, ) => (state: AppState) => PreRequestUserState = _memoize( basePreRequestUserStateForSingleKeyserverSelector, ); const preRequestUserStateSelector: (state: AppState) => PreRequestUserState = createSelector( (state: AppState) => state.currentUserInfo, (state: AppState) => state.keyserverStore.keyserverInfos, (currentUserInfo: ?CurrentUserInfo, keyserverInfos: KeyserverInfos) => { const cookiesAndSessions: { [string]: PreRequestUserKeyserverSessionInfo, } = {}; for (const keyserverID in keyserverInfos) { cookiesAndSessions[keyserverID] = { cookie: keyserverInfos[keyserverID].cookie, sessionID: keyserverInfos[keyserverID].sessionID, }; } return { currentUserInfo, cookiesAndSessions, }; }, ); export { logInExtraInfoSelector, preRequestUserStateForSingleKeyserverSelector, preRequestUserStateSelector, }; diff --git a/lib/selectors/keyserver-selectors.js b/lib/selectors/keyserver-selectors.js index 580dc58da..6764b47c1 100644 --- a/lib/selectors/keyserver-selectors.js +++ b/lib/selectors/keyserver-selectors.js @@ -1,142 +1,147 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import type { PlatformDetails } from '../types/device-types'; import type { KeyserverInfo, KeyserverInfos, SelectedKeyserverInfo, } from '../types/keyserver-types'; import type { AppState } from '../types/redux-types.js'; import type { ConnectionInfo } from '../types/socket-types.js'; import type { UserInfos } from '../types/user-types.js'; import { ashoatKeyserverID } from '../utils/validation-utils.js'; const cookieSelector: (state: AppState) => ?string = (state: AppState) => state.keyserverStore.keyserverInfos[ashoatKeyserverID]?.cookie; const cookiesSelector: (state: AppState) => { +[keyserverID: string]: string, } = createSelector( (state: AppState) => state.keyserverStore.keyserverInfos, (infos: { +[key: string]: KeyserverInfo }) => { const cookies = {}; for (const keyserverID in infos) { cookies[keyserverID] = infos[keyserverID].cookie; } return cookies; }, ); -const sessionIDSelector: (state: AppState) => ?string = (state: AppState) => - state.keyserverStore.keyserverInfos[ashoatKeyserverID]?.sessionID; +const baseSessionIDSelector: ( + keyserverID: string, +) => (state: AppState) => ?string = keyserverID => (state: AppState) => + state.keyserverStore.keyserverInfos[keyserverID]?.sessionID; + +const sessionIDSelector: (keyserverID: string) => (state: AppState) => ?string = + _memoize(baseSessionIDSelector); const baseUpdatesCurrentAsOfSelector: ( keyserverID: string, ) => (state: AppState) => number = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID]?.updatesCurrentAsOf ?? 0; const updatesCurrentAsOfSelector: ( keyserverID: string, ) => (state: AppState) => number = _memoize(baseUpdatesCurrentAsOfSelector); const baseCurrentAsOfSelector: ( keyserverID: string, ) => (state: AppState) => number = keyserverID => (state: AppState) => state.messageStore.currentAsOf[keyserverID] ?? 0; const currentAsOfSelector: ( keyserverID: string, ) => (state: AppState) => number = _memoize(baseCurrentAsOfSelector); const baseUrlPrefixSelector: ( keyserverID: string, ) => (state: AppState) => ?string = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID]?.urlPrefix; const urlPrefixSelector: (keyserverID: string) => (state: AppState) => ?string = _memoize(baseUrlPrefixSelector); const baseConnectionSelector: ( keyserverID: string, ) => (state: AppState) => ?ConnectionInfo = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID]?.connection; const connectionSelector: ( keyserverID: string, ) => (state: AppState) => ?ConnectionInfo = _memoize(baseConnectionSelector); const baseLastCommunicatedPlatformDetailsSelector: ( keyserverID: string, ) => (state: AppState) => ?PlatformDetails = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID] ?.lastCommunicatedPlatformDetails; const lastCommunicatedPlatformDetailsSelector: ( keyserverID: string, ) => (state: AppState) => ?PlatformDetails = _memoize( baseLastCommunicatedPlatformDetailsSelector, ); const selectedKeyserversSelector: ( state: AppState, ) => $ReadOnlyArray = createSelector( (state: AppState) => state.keyserverStore.keyserverInfos, (state: AppState) => state.userStore.userInfos, (keyserverInfos: KeyserverInfos, userInfos: UserInfos) => { const result = []; for (const key in keyserverInfos) { const keyserverInfo = keyserverInfos[key]; const keyserverAdminUsername = userInfos[key]?.username; if (!keyserverAdminUsername) { continue; } result.push({ keyserverAdminUsername, keyserverInfo, }); } return result; }, ); const deviceTokensSelector: (state: AppState) => { +[keyserverID: string]: ?string, } = createSelector( (state: AppState) => state.keyserverStore.keyserverInfos, (infos: { +[key: string]: KeyserverInfo }) => { const deviceTokens = {}; for (const keyserverID in infos) { deviceTokens[keyserverID] = infos[keyserverID].deviceToken; } return deviceTokens; }, ); const baseDeviceTokenSelector: ( keyserverID: string, ) => (state: AppState) => ?string = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID]?.deviceToken; const deviceTokenSelector: ( keyserverID: string, ) => (state: AppState) => ?string = _memoize(baseDeviceTokenSelector); export { cookieSelector, cookiesSelector, sessionIDSelector, updatesCurrentAsOfSelector, currentAsOfSelector, urlPrefixSelector, connectionSelector, lastCommunicatedPlatformDetailsSelector, deviceTokensSelector, deviceTokenSelector, selectedKeyserversSelector, }; diff --git a/lib/selectors/server-calls.js b/lib/selectors/server-calls.js index 7024c101d..e96c584cb 100644 --- a/lib/selectors/server-calls.js +++ b/lib/selectors/server-calls.js @@ -1,63 +1,63 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import { cookieSelector, sessionIDSelector, urlPrefixSelector, connectionSelector, lastCommunicatedPlatformDetailsSelector, } from './keyserver-selectors.js'; import type { PlatformDetails } from '../types/device-types.js'; import type { AppState } from '../types/redux-types.js'; import type { ConnectionInfo, ConnectionStatus, } from '../types/socket-types.js'; import { type CurrentUserInfo } from '../types/user-types.js'; export type ServerCallState = { +cookie: ?string, +urlPrefix: ?string, +sessionID: ?string, +currentUserInfo: ?CurrentUserInfo, +connectionStatus: ?ConnectionStatus, +lastCommunicatedPlatformDetails: ?PlatformDetails, }; const baseServerCallStateSelector: ( keyserverID: string, ) => (state: AppState) => ServerCallState = keyserverID => createSelector( cookieSelector, urlPrefixSelector(keyserverID), - sessionIDSelector, + sessionIDSelector(keyserverID), (state: AppState) => state.currentUserInfo, connectionSelector(keyserverID), lastCommunicatedPlatformDetailsSelector(keyserverID), ( cookie: ?string, urlPrefix: ?string, sessionID: ?string, currentUserInfo: ?CurrentUserInfo, connectionInfo: ?ConnectionInfo, lastCommunicatedPlatformDetails: ?PlatformDetails, ) => ({ cookie, urlPrefix, sessionID, currentUserInfo, connectionStatus: connectionInfo?.status, lastCommunicatedPlatformDetails, }), ); const serverCallStateSelector: ( keyserverID: string, ) => (state: AppState) => ServerCallState = _memoize( baseServerCallStateSelector, ); export { serverCallStateSelector }; diff --git a/lib/shared/session-utils.js b/lib/shared/session-utils.js index 1d91b443b..e23f9ad53 100644 --- a/lib/shared/session-utils.js +++ b/lib/shared/session-utils.js @@ -1,63 +1,63 @@ // @flow import { cookieSelector, sessionIDSelector, } from '../selectors/keyserver-selectors.js'; import { logInActionSources, type LogInActionSource, } from '../types/account-types.js'; import type { AppState } from '../types/redux-types.js'; import type { PreRequestUserState } from '../types/session-types.js'; import type { CurrentUserInfo } from '../types/user-types.js'; import { ashoatKeyserverID } from '../utils/validation-utils.js'; function invalidSessionDowngrade( currentReduxState: AppState, actionCurrentUserInfo: ?CurrentUserInfo, preRequestUserState: ?PreRequestUserState, ): boolean { // If this action represents a session downgrade - oldState has a loggedIn // currentUserInfo, but the action has an anonymous one - then it is only // valid if the currentUserInfo used for the request matches what oldState // currently has. If the currentUserInfo in Redux has changed since the // request, and is currently loggedIn, then the session downgrade does not // apply to it. In this case we will simply swallow the action. const currentCurrentUserInfo = currentReduxState.currentUserInfo; return !!( currentCurrentUserInfo && !currentCurrentUserInfo.anonymous && // Note that an undefined actionCurrentUserInfo represents an action that // doesn't affect currentUserInfo, whereas a null one represents an action // that sets it to null (actionCurrentUserInfo === null || (actionCurrentUserInfo && actionCurrentUserInfo.anonymous)) && preRequestUserState && (preRequestUserState.currentUserInfo?.id !== currentCurrentUserInfo.id || preRequestUserState.cookiesAndSessions[ashoatKeyserverID].cookie !== cookieSelector(currentReduxState) || preRequestUserState.cookiesAndSessions[ashoatKeyserverID].sessionID !== - sessionIDSelector(currentReduxState)) + sessionIDSelector(ashoatKeyserverID)(currentReduxState)) ); } function invalidSessionRecovery( currentReduxState: AppState, actionCurrentUserInfo: CurrentUserInfo, logInActionSource: ?LogInActionSource, ): boolean { if ( logInActionSource !== logInActionSources.cookieInvalidationResolutionAttempt && logInActionSource !== logInActionSources.socketAuthErrorResolutionAttempt ) { return false; } return ( !currentReduxState.dataLoaded || currentReduxState.currentUserInfo?.id !== actionCurrentUserInfo.id ); } export { invalidSessionDowngrade, invalidSessionRecovery }; diff --git a/web/selectors/socket-selectors.js b/web/selectors/socket-selectors.js index cb9a5d38f..00b7360e8 100644 --- a/web/selectors/socket-selectors.js +++ b/web/selectors/socket-selectors.js @@ -1,155 +1,162 @@ // @flow import olm from '@commapp/olm'; import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import { sessionIDSelector, urlPrefixSelector, cookieSelector, } from 'lib/selectors/keyserver-selectors.js'; import { getClientResponsesSelector, sessionStateFuncSelector, } from 'lib/selectors/socket-selectors.js'; import { createOpenSocketFunction } from 'lib/shared/socket-utils.js'; import type { OLMIdentityKeys, PickledOLMAccount, SignedIdentityKeysBlob, IdentityKeysBlob, } from 'lib/types/crypto-types.js'; import type { ClientServerRequest, ClientClientResponse, } from 'lib/types/request-types.js'; import type { SessionIdentification, SessionState, } from 'lib/types/session-types.js'; import type { OneTimeKeyGenerator } from 'lib/types/socket-types.js'; import { initOlm } from '../olm/olm-utils.js'; import type { AppState } from '../redux/redux-setup.js'; const baseOpenSocketSelector: ( keyserverID: string, ) => (state: AppState) => ?() => WebSocket = keyserverID => createSelector(urlPrefixSelector(keyserverID), (urlPrefix: ?string) => { if (!urlPrefix) { return null; } return createOpenSocketFunction(urlPrefix); }); const openSocketSelector: ( keyserverID: string, ) => (state: AppState) => ?() => WebSocket = _memoize(baseOpenSocketSelector); +const baseSessionIdentificationSelector: ( + keyserverID: string, +) => (state: AppState) => SessionIdentification = keyserverID => + createSelector( + cookieSelector, + sessionIDSelector(keyserverID), + (cookie: ?string, sessionID: ?string): SessionIdentification => ({ + cookie, + sessionID, + }), + ); + const sessionIdentificationSelector: ( - state: AppState, -) => SessionIdentification = createSelector( - cookieSelector, - sessionIDSelector, - (cookie: ?string, sessionID: ?string): SessionIdentification => ({ - cookie, - sessionID, - }), + keyserverID: string, +) => (state: AppState) => SessionIdentification = _memoize( + baseSessionIdentificationSelector, ); const getSignedIdentityKeysBlobSelector: ( state: AppState, ) => ?() => Promise = createSelector( (state: AppState) => state.cryptoStore.primaryAccount, (state: AppState) => state.cryptoStore.primaryIdentityKeys, (state: AppState) => state.cryptoStore.notificationIdentityKeys, ( primaryAccount: ?PickledOLMAccount, primaryIdentityKeys: ?OLMIdentityKeys, notificationIdentityKeys: ?OLMIdentityKeys, ) => { if (!primaryAccount || !primaryIdentityKeys || !notificationIdentityKeys) { return null; } return async () => { await initOlm(); const primaryOLMAccount = new olm.Account(); primaryOLMAccount.unpickle( primaryAccount.picklingKey, primaryAccount.pickledAccount, ); const identityKeysBlob: IdentityKeysBlob = { primaryIdentityPublicKeys: primaryIdentityKeys, notificationIdentityPublicKeys: notificationIdentityKeys, }; const payloadToBeSigned: string = JSON.stringify(identityKeysBlob); const signedIdentityKeysBlob: SignedIdentityKeysBlob = { payload: payloadToBeSigned, signature: primaryOLMAccount.sign(payloadToBeSigned), }; return signedIdentityKeysBlob; }; }, ); const webGetClientResponsesSelector: ( state: AppState, ) => ( serverRequests: $ReadOnlyArray, ) => Promise<$ReadOnlyArray> = createSelector( getClientResponsesSelector, getSignedIdentityKeysBlobSelector, (state: AppState) => state.navInfo.tab === 'calendar', ( getClientResponsesFunc: ( calendarActive: boolean, oneTimeKeyGenerator: ?OneTimeKeyGenerator, getSignedIdentityKeysBlob: ?() => Promise, getInitialNotificationsEncryptedMessage: ?() => Promise, serverRequests: $ReadOnlyArray, ) => Promise<$ReadOnlyArray>, getSignedIdentityKeysBlob: ?() => Promise, calendarActive: boolean, ) => (serverRequests: $ReadOnlyArray) => getClientResponsesFunc( calendarActive, null, getSignedIdentityKeysBlob, null, serverRequests, ), ); const baseWebSessionStateFuncSelector: ( keyserverID: string, ) => (state: AppState) => () => SessionState = keyserverID => createSelector( sessionStateFuncSelector(keyserverID), (state: AppState) => state.navInfo.tab === 'calendar', ( sessionStateFunc: (calendarActive: boolean) => SessionState, calendarActive: boolean, ) => () => sessionStateFunc(calendarActive), ); const webSessionStateFuncSelector: ( keyserverID: string, ) => (state: AppState) => () => SessionState = _memoize( baseWebSessionStateFuncSelector, ); export { openSocketSelector, sessionIdentificationSelector, getSignedIdentityKeysBlobSelector, webGetClientResponsesSelector, webSessionStateFuncSelector, }; diff --git a/web/socket.react.js b/web/socket.react.js index 426ecbb25..5af836137 100644 --- a/web/socket.react.js +++ b/web/socket.react.js @@ -1,100 +1,102 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useDispatch } from 'react-redux'; import { useLogOut } from 'lib/actions/user-actions.js'; import { preRequestUserStateForSingleKeyserverSelector } from 'lib/selectors/account-selectors.js'; import { cookieSelector, urlPrefixSelector, connectionSelector, lastCommunicatedPlatformDetailsSelector, } from 'lib/selectors/keyserver-selectors.js'; import Socket, { type BaseSocketProps } from 'lib/socket/socket.react.js'; import { useDispatchActionPromise } from 'lib/utils/action-utils.js'; import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; import { useSelector } from './redux/redux-utils.js'; import { activeThreadSelector, webCalendarQuery, } from './selectors/nav-selectors.js'; import { openSocketSelector, sessionIdentificationSelector, webGetClientResponsesSelector, webSessionStateFuncSelector, } from './selectors/socket-selectors.js'; import { decompressMessage } from './utils/decompress.js'; const WebSocket: React.ComponentType = React.memo(function WebSocket(props) { const cookie = useSelector(cookieSelector); const urlPrefix = useSelector(urlPrefixSelector(ashoatKeyserverID)); invariant(urlPrefix, 'missing urlPrefix for given keyserver id'); const connection = useSelector(connectionSelector(ashoatKeyserverID)); invariant(connection, 'keyserver missing from keyserverStore'); const active = useSelector( state => !!state.currentUserInfo && !state.currentUserInfo.anonymous && state.lifecycleState !== 'background', ); const openSocket = useSelector(openSocketSelector(ashoatKeyserverID)); invariant(openSocket, 'openSocket failed to be created'); - const sessionIdentification = useSelector(sessionIdentificationSelector); + const sessionIdentification = useSelector( + sessionIdentificationSelector(ashoatKeyserverID), + ); const preRequestUserState = useSelector( preRequestUserStateForSingleKeyserverSelector(ashoatKeyserverID), ); const getClientResponses = useSelector(webGetClientResponsesSelector); const sessionStateFunc = useSelector( webSessionStateFuncSelector(ashoatKeyserverID), ); const currentCalendarQuery = useSelector(webCalendarQuery); const reduxActiveThread = useSelector(activeThreadSelector); const windowActive = useSelector(state => state.windowActive); const activeThread = React.useMemo(() => { if (!active || !windowActive) { return null; } return reduxActiveThread; }, [active, windowActive, reduxActiveThread]); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const callLogOut = useLogOut(); const lastCommunicatedPlatformDetails = useSelector( lastCommunicatedPlatformDetailsSelector(ashoatKeyserverID), ); return ( ); }); export default WebSocket;