diff --git a/lib/selectors/socket-selectors.js b/lib/selectors/socket-selectors.js index 579fb6704..a62cdc7be 100644 --- a/lib/selectors/socket-selectors.js +++ b/lib/selectors/socket-selectors.js @@ -1,248 +1,254 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import { updatesCurrentAsOfSelector, currentAsOfSelector, urlPrefixSelector, cookieSelector, } from './keyserver-selectors.js'; import { currentCalendarQuery } from './nav-selectors.js'; import { createOpenSocketFunction } from '../shared/socket-utils.js'; import type { BoundStateSyncSpec } from '../shared/state-sync/state-sync-spec.js'; import { stateSyncSpecs } from '../shared/state-sync/state-sync-specs.js'; import threadWatcher from '../shared/thread-watcher.js'; import type { SignedIdentityKeysBlob } from '../types/crypto-types.js'; import { type CalendarQuery } from '../types/entry-types.js'; import type { AppState } from '../types/redux-types.js'; import type { ClientReportCreationRequest } from '../types/report-types.js'; import { serverRequestTypes, type ClientServerRequest, type ClientClientResponse, } from '../types/request-types.js'; import type { SessionState } from '../types/session-types.js'; import { getConfig } from '../utils/config.js'; import { values } from '../utils/objects.js'; const baseOpenSocketSelector: ( keyserverID: string, ) => (state: AppState) => ?() => WebSocket = keyserverID => createSelector( urlPrefixSelector(keyserverID), // We don't actually use the cookie in the socket open function, // but we do use it in the initial message, and when the cookie changes // the socket needs to be reopened. By including the cookie here, // whenever the cookie changes this function will change, // which tells the Socket component to restart the connection. cookieSelector(keyserverID), (urlPrefix: ?string) => { if (!urlPrefix) { return null; } return createOpenSocketFunction(urlPrefix); }, ); const openSocketSelector: ( keyserverID: string, ) => (state: AppState) => ?() => WebSocket = _memoize(baseOpenSocketSelector); const queuedReports: ( state: AppState, ) => $ReadOnlyArray = createSelector( (state: AppState) => state.reportStore.queuedReports, ( mainQueuedReports: $ReadOnlyArray, ): $ReadOnlyArray => mainQueuedReports, ); // We pass all selectors specified in stateSyncSpecs and get the resulting // BoundStateSyncSpecs in the specs array. We do it so we don't have to // modify the selector when we add a new spec. type BoundStateSyncSpecs = { +specsPerHashKey: { +[string]: BoundStateSyncSpec }, +specPerInnerHashKey: { +[string]: BoundStateSyncSpec }, }; const stateSyncSpecSelectors = values(stateSyncSpecs).map( spec => spec.selector, ); const boundStateSyncSpecsSelector: AppState => BoundStateSyncSpecs = // The FlowFixMe is needed because createSelector types require flow // to know the number of subselectors at compile time. // $FlowFixMe createSelector(stateSyncSpecSelectors, (...specs) => { const boundSpecs = (specs: BoundStateSyncSpec[]); // We create a map from `hashKey` to a given spec for easier lookup later const specsPerHashKey = Object.fromEntries( boundSpecs.map(spec => [spec.hashKey, spec]), ); // We do the same for innerHashKey const specPerInnerHashKey = Object.fromEntries( boundSpecs .filter(spec => spec.innerHashSpec?.hashKey) .map(spec => [spec.innerHashSpec?.hashKey, spec]), ); return { specsPerHashKey, specPerInnerHashKey }; }); const getClientResponsesSelector: ( state: AppState, keyserverID: string, ) => ( calendarActive: boolean, getSignedIdentityKeysBlob: () => Promise, getInitialNotificationsEncryptedMessage: ?( keyserverID: string, ) => Promise, serverRequests: $ReadOnlyArray, ) => Promise<$ReadOnlyArray> = createSelector( boundStateSyncSpecsSelector, currentCalendarQuery, (state: AppState, keyserverID: string) => keyserverID, ( boundStateSyncSpecs: BoundStateSyncSpecs, calendarQuery: (calendarActive: boolean) => CalendarQuery, keyserverID: string, ) => { return async ( calendarActive: boolean, getSignedIdentityKeysBlob: () => Promise, getInitialNotificationsEncryptedMessage: ?( keyserverID: string, ) => Promise, serverRequests: $ReadOnlyArray, ): Promise<$ReadOnlyArray> => { const clientResponses = []; const serverRequestedPlatformDetails = serverRequests.some( request => request.type === serverRequestTypes.PLATFORM_DETAILS, ); const { specsPerHashKey, specPerInnerHashKey } = boundStateSyncSpecs; for (const serverRequest of serverRequests) { if ( serverRequest.type === serverRequestTypes.PLATFORM && !serverRequestedPlatformDetails ) { clientResponses.push({ type: serverRequestTypes.PLATFORM, platform: getConfig().platformDetails.platform, }); } else if (serverRequest.type === serverRequestTypes.PLATFORM_DETAILS) { clientResponses.push({ type: serverRequestTypes.PLATFORM_DETAILS, platformDetails: getConfig().platformDetails, }); } else if (serverRequest.type === serverRequestTypes.CHECK_STATE) { const query = calendarQuery(calendarActive); const hashResults: { [string]: boolean } = {}; for (const key in serverRequest.hashesToCheck) { const expectedHashValue = serverRequest.hashesToCheck[key]; let hashValue; const [specKey, id] = key.split('|'); if (id) { - hashValue = specPerInnerHashKey[specKey]?.getInfoHash(id); + hashValue = specPerInnerHashKey[specKey]?.getInfoHash( + id, + keyserverID, + ); } else { - hashValue = specsPerHashKey[specKey]?.getAllInfosHash(query); + hashValue = specsPerHashKey[specKey]?.getAllInfosHash( + query, + keyserverID, + ); } // If hashValue values is null then we are still calculating // the hashes in the background. In this case we return true // to skip this state check. Future state checks (after the hash // calculation complete) will be handled normally. if (!hashValue) { hashResults[key] = true; } else { hashResults[key] = expectedHashValue === hashValue; } } const { failUnmentioned } = serverRequest; for (const spec of values(specPerInnerHashKey)) { const innerHashKey = spec.innerHashSpec?.hashKey; if (!failUnmentioned?.[spec.hashKey] || !innerHashKey) { continue; } - const ids = spec.getIDs(query); + const ids = spec.getIDs(query, keyserverID); if (!ids) { continue; } for (const id of ids) { const key = `${innerHashKey}|${id}`; const hashResult = hashResults[key]; if (hashResult === null || hashResult === undefined) { hashResults[key] = false; } } } clientResponses.push({ type: serverRequestTypes.CHECK_STATE, hashResults, }); } else if ( serverRequest.type === serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB ) { const signedIdentityKeysBlob = await getSignedIdentityKeysBlob(); clientResponses.push({ type: serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB, signedIdentityKeysBlob, }); } else if ( serverRequest.type === serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE && getInitialNotificationsEncryptedMessage ) { const initialNotificationsEncryptedMessage = await getInitialNotificationsEncryptedMessage(keyserverID); clientResponses.push({ type: serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE, initialNotificationsEncryptedMessage, }); } } return clientResponses; }; }, ); const baseSessionStateFuncSelector: ( keyserverID: string, ) => ( state: AppState, ) => (calendarActive: boolean) => SessionState = keyserverID => createSelector( currentAsOfSelector(keyserverID), updatesCurrentAsOfSelector(keyserverID), currentCalendarQuery, ( messagesCurrentAsOf: number, updatesCurrentAsOf: number, calendarQuery: (calendarActive: boolean) => CalendarQuery, ) => (calendarActive: boolean): SessionState => ({ calendarQuery: calendarQuery(calendarActive), messagesCurrentAsOf, updatesCurrentAsOf, watchedIDs: threadWatcher.getWatchedIDs(), }), ); const sessionStateFuncSelector: ( keyserverID: string, ) => (state: AppState) => (calendarActive: boolean) => SessionState = _memoize( baseSessionStateFuncSelector, ); export { openSocketSelector, queuedReports, getClientResponsesSelector, sessionStateFuncSelector, }; diff --git a/lib/shared/state-sync/entries-state-sync-spec.js b/lib/shared/state-sync/entries-state-sync-spec.js index bee6173dd..3de46f394 100644 --- a/lib/shared/state-sync/entries-state-sync-spec.js +++ b/lib/shared/state-sync/entries-state-sync-spec.js @@ -1,127 +1,137 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; import { createSelector } from 'reselect'; import type { StateSyncSpec, BoundStateSyncSpec } from './state-sync-spec.js'; import { extractKeyserverIDFromID } from '../../keyserver-conn/keyserver-call-utils.js'; import { type CalendarQuery, type RawEntryInfos, type RawEntryInfo, } from '../../types/entry-types.js'; import type { AppState } from '../../types/redux-types.js'; import { reportTypes, type ClientEntryInconsistencyReportCreationRequest, } from '../../types/report-types.js'; import type { ProcessServerRequestAction } from '../../types/request-types.js'; import { actionLogger } from '../../utils/action-logger.js'; -import { authoritativeKeyserverID } from '../../utils/authoritative-keyserver.js'; import { getConfig } from '../../utils/config.js'; import { values, combineUnorderedHashes, hash } from '../../utils/objects.js'; import { generateReportID } from '../../utils/report-utils.js'; import { sanitizeActionSecrets } from '../../utils/sanitization.js'; import { filterRawEntryInfosByCalendarQuery, serverEntryInfosObject, } from '../entry-utils.js'; const selector: ( state: AppState, ) => BoundStateSyncSpec< RawEntryInfos, RawEntryInfo, $ReadOnlyArray, > = createSelector( (state: AppState) => state.entryStore.entryInfos, (entryInfos: RawEntryInfos) => ({ ...entriesStateSyncSpec, - getInfoHash: (id: string) => - hash(entryInfos[`${authoritativeKeyserverID()}|${id}`]), - getAllInfosHash: (calendarQuery: CalendarQuery) => - getEntryInfosHash(entryInfos, calendarQuery), - getIDs: (calendarQuery: CalendarQuery) => - getEntryIDs(entryInfos, calendarQuery), + getInfoHash: (id: string, keyserverID: string) => + hash(entryInfos[`${keyserverID}|${id}`]), + getAllInfosHash: (calendarQuery: CalendarQuery, keyserverID: string) => + getEntryInfosHash(entryInfos, calendarQuery, keyserverID), + getIDs: (calendarQuery: CalendarQuery, keyserverID: string) => + getEntryIDs(entryInfos, calendarQuery, keyserverID), }), ); export const entriesStateSyncSpec: StateSyncSpec< RawEntryInfos, RawEntryInfo, $ReadOnlyArray, > = Object.freeze({ hashKey: 'entryInfos', innerHashSpec: { hashKey: 'entryInfo', deleteKey: 'deleteEntryIDs', rawInfosKey: 'rawEntryInfos', }, findStoreInconsistencies( action: ProcessServerRequestAction, beforeStateCheck: RawEntryInfos, afterStateCheck: RawEntryInfos, ) { const keyserverID = action.payload.keyserverID; const filteredBeforeStateCheck = values(beforeStateCheck).filter( entry => entry.id && extractKeyserverIDFromID(entry.id) === keyserverID, ); const filteredAfterStateCheck = values(afterStateCheck).filter( entry => entry.id && extractKeyserverIDFromID(entry.id) === keyserverID, ); const calendarQuery = action.payload.calendarQuery; // We don't want to bother reporting an inconsistency if it's just because // of extraneous EntryInfos (not within the current calendarQuery) on either // side const filteredBeforeResult = filterRawEntryInfosByCalendarQuery( serverEntryInfosObject(filteredBeforeStateCheck), calendarQuery, ); const filteredAfterResult = filterRawEntryInfosByCalendarQuery( serverEntryInfosObject(filteredAfterStateCheck), calendarQuery, ); if (_isEqual(filteredBeforeResult)(filteredAfterResult)) { return emptyArray; } return [ { type: reportTypes.ENTRY_INCONSISTENCY, platformDetails: getConfig().platformDetails, beforeAction: beforeStateCheck, action: sanitizeActionSecrets(action), calendarQuery, pushResult: afterStateCheck, lastActions: actionLogger.interestingActionSummaries, time: Date.now(), id: generateReportID(), }, ]; }, selector, }); const emptyArray: $ReadOnlyArray = []; function getEntryInfosHash( entryInfos: RawEntryInfos, calendarQuery: CalendarQuery, + keyserverID: string, ) { + const filteredEntries = values(entryInfos).filter( + entry => entry.id && extractKeyserverIDFromID(entry.id) === keyserverID, + ); const filteredEntryInfos = filterRawEntryInfosByCalendarQuery( - serverEntryInfosObject(values(entryInfos)), + serverEntryInfosObject(filteredEntries), calendarQuery, ); return combineUnorderedHashes(Object.values(filteredEntryInfos).map(hash)); } -function getEntryIDs(entryInfos: RawEntryInfos, calendarQuery: CalendarQuery) { +function getEntryIDs( + entryInfos: RawEntryInfos, + calendarQuery: CalendarQuery, + keyserverID: string, +) { + const filteredEntries = values(entryInfos).filter( + entry => entry.id && extractKeyserverIDFromID(entry.id) === keyserverID, + ); const filteredEntryInfos = filterRawEntryInfosByCalendarQuery( - serverEntryInfosObject(values(entryInfos)), + serverEntryInfosObject(filteredEntries), calendarQuery, ); return Object.keys(filteredEntryInfos).map(id => id.split('|')[1]); } diff --git a/lib/shared/state-sync/state-sync-spec.js b/lib/shared/state-sync/state-sync-spec.js index e5809c154..91aa47300 100644 --- a/lib/shared/state-sync/state-sync-spec.js +++ b/lib/shared/state-sync/state-sync-spec.js @@ -1,35 +1,35 @@ // @flow import type { CalendarQuery } from '../../types/entry-types.js'; import type { AppState } from '../../types/redux-types.js'; import type { ProcessServerRequestAction } from '../../types/request-types.js'; export type StateSyncSpec = { +hashKey: string, +innerHashSpec?: { +hashKey: string, +deleteKey: string, +rawInfosKey: string, +additionalDeleteCondition?: Info => boolean, }, +findStoreInconsistencies: ( action: ProcessServerRequestAction, beforeStateCheck: Infos, afterStateCheck: Infos, ) => Inconsistencies, +selector: ( state: AppState, ) => BoundStateSyncSpec, }; // All ids specified here (getInfoHash and getIDs) are server ids. // E.g. in the case of threadStore or entryStore the keyserver prefix // needs to be handled additionaly export type BoundStateSyncSpec = { // If these function depend on background hashing that is still not complete // they should return null, to indicate that the hashes aren't available yet - +getInfoHash: (id: string) => ?number, - +getAllInfosHash: (query: CalendarQuery) => ?number, - +getIDs: (query: CalendarQuery) => ?Array, + +getInfoHash: (id: string, keyserverID: string) => ?number, + +getAllInfosHash: (query: CalendarQuery, keyserverID: string) => ?number, + +getIDs: (query: CalendarQuery, keyserverID: string) => ?Array, ...StateSyncSpec, }; diff --git a/lib/shared/state-sync/threads-state-sync-spec.js b/lib/shared/state-sync/threads-state-sync-spec.js index d1d175871..80e7d0005 100644 --- a/lib/shared/state-sync/threads-state-sync-spec.js +++ b/lib/shared/state-sync/threads-state-sync-spec.js @@ -1,91 +1,99 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; import _pickBy from 'lodash/fp/pickBy.js'; import { createSelector } from 'reselect'; import type { StateSyncSpec, BoundStateSyncSpec } from './state-sync-spec.js'; import { extractKeyserverIDFromID } from '../../keyserver-conn/keyserver-call-utils.js'; +import type { CalendarQuery } from '../../types/entry-types.js'; import type { RawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import type { AppState } from '../../types/redux-types.js'; import { reportTypes, type ClientThreadInconsistencyReportCreationRequest, } from '../../types/report-types.js'; import type { ProcessServerRequestAction } from '../../types/request-types.js'; import { type MixedRawThreadInfos, type LegacyRawThreadInfo, } from '../../types/thread-types.js'; import { actionLogger } from '../../utils/action-logger.js'; -import { authoritativeKeyserverID } from '../../utils/authoritative-keyserver.js'; import { getConfig } from '../../utils/config.js'; -import { combineUnorderedHashes, values } from '../../utils/objects.js'; +import { combineUnorderedHashes, entries } from '../../utils/objects.js'; import { generateReportID } from '../../utils/report-utils.js'; import { sanitizeActionSecrets } from '../../utils/sanitization.js'; const selector: ( state: AppState, ) => BoundStateSyncSpec< MixedRawThreadInfos, LegacyRawThreadInfo | RawThreadInfo, $ReadOnlyArray, > = createSelector( (state: AppState) => state.integrityStore.threadHashes, (state: AppState) => state.integrityStore.threadHashingStatus === 'completed', (threadHashes: { +[string]: number }, threadHashingComplete: boolean) => ({ ...threadsStateSyncSpec, - getInfoHash: (id: string) => - threadHashes[`${authoritativeKeyserverID()}|${id}`], + getInfoHash: (id: string, keyserverID: string) => + threadHashes[`${keyserverID}|${id}`], getAllInfosHash: threadHashingComplete - ? () => combineUnorderedHashes(values(threadHashes)) + ? (query: CalendarQuery, keyserverID: string) => + combineUnorderedHashes( + entries(threadHashes) + .filter(([id]) => extractKeyserverIDFromID(id) === keyserverID) + .map(([, threadHash]) => threadHash), + ) : () => null, getIDs: threadHashingComplete - ? () => Object.keys(threadHashes).map(id => id.split('|')[1]) + ? (query: CalendarQuery, keyserverID: string) => + Object.keys(threadHashes) + .filter(id => extractKeyserverIDFromID(id) === keyserverID) + .map(id => id.split('|')[1]) : () => null, }), ); export const threadsStateSyncSpec: StateSyncSpec< MixedRawThreadInfos, LegacyRawThreadInfo | RawThreadInfo, $ReadOnlyArray, > = Object.freeze({ hashKey: 'threadInfos', innerHashSpec: { hashKey: 'threadInfo', deleteKey: 'deleteThreadIDs', rawInfosKey: 'rawThreadInfos', }, findStoreInconsistencies( action: ProcessServerRequestAction, beforeStateCheck: MixedRawThreadInfos, afterStateCheck: MixedRawThreadInfos, ) { const keyserverID = action.payload.keyserverID; const filter = _pickBy( thread => extractKeyserverIDFromID(thread.id) === keyserverID, ); const filteredBeforeStateCheck = filter(beforeStateCheck); const filteredAfterStateCheck = filter(afterStateCheck); if (_isEqual(filteredBeforeStateCheck)(filteredAfterStateCheck)) { return emptyArray; } return [ { type: reportTypes.THREAD_INCONSISTENCY, platformDetails: getConfig().platformDetails, beforeAction: beforeStateCheck, action: sanitizeActionSecrets(action), pushResult: afterStateCheck, lastActions: actionLogger.interestingActionSummaries, time: Date.now(), id: generateReportID(), }, ]; }, selector, }); const emptyArray: $ReadOnlyArray = [];