diff --git a/ts/session/protocols/SessionProtocol.ts b/ts/session/protocols/SessionProtocol.ts index 92479de7a..e357e46a3 100644 --- a/ts/session/protocols/SessionProtocol.ts +++ b/ts/session/protocols/SessionProtocol.ts @@ -5,248 +5,259 @@ import { createOrUpdateItem, getItemById } from '../../../js/modules/data'; interface StringToNumberMap { [key: string]: number; } +// tslint:disable: function-name +// tslint:disable: no-unnecessary-class +export class SessionProtocol { + + private static dbLoaded: Boolean = false; + /** + * This map olds the sent session timestamps, i.e. session requests message effectively sent to the recipient. + * It is backed by a database entry so it's loaded from db on startup. + * This map should not be used directly, but instead through + * `updateSendSessionTimestamp()`, `getSendSessionRequest()` or `hasSendSessionRequest()` + */ + private static sentSessionsTimestamp: StringToNumberMap; + + + /** + * This map olds the processed session timestamps, i.e. when we received a session request and handled it. + * It is backed by a database entry so it's loaded from db on startup. + * This map should not be used directly, but instead through + * `updateProcessedSessionTimestamp()`, `getProcessedSessionRequest()` or `hasProcessedSessionRequest()` + */ + private static processedSessionsTimestamp: StringToNumberMap; + + /** + * This map olds the timestamp on which a sent session reset is triggered for a specific device. + * Once the message is sent or failed to sent, this device is removed from here. + * This is a memory only map. Which means that on app restart it's starts empty. + */ + private static readonly pendingSendSessionsTimestamp: Set = new Set(); + + + /** Returns true if we already have a session with that device */ + public static async hasSession(device: string): Promise { + // Session does not use the concept of a deviceId, thus it's always 1 + const address = new window.libsignal.SignalProtocolAddress(device, 1); + const sessionCipher = new window.libsignal.SessionCipher( + window.textsecure.storage.protocol, + address + ); -/** - * This map olds the sent session timestamps, i.e. session requests message effectively sent to the recipient. - * It is backed by a database entry so it's loaded from db on startup. - * This map should not be used directly, but instead through - * `_updateSendSessionTimestamp()`, `_getSendSessionRequest()` or `_hasSendSessionRequest()` - */ -let sentSessionsTimestamp: StringToNumberMap; - -/** - * This map olds the processed session timestamps, i.e. when we received a session request and handled it. - * It is backed by a database entry so it's loaded from db on startup. - * This map should not be used directly, but instead through - * `_updateProcessedSessionTimestamp()`, `_getProcessedSessionRequest()` or `_hasProcessedSessionRequest()` - */ -let processedSessionsTimestamp: StringToNumberMap; - -/** - * This map olds the timestamp on which a sent session reset is triggered for a specific device. - * Once the message is sent or failed to sent, this device is removed from here. - * This is a memory only map. Which means that on app restart it's starts empty. - */ -const pendingSendSessionsTimestamp: Set = new Set(); - -/** ======= exported functions ======= */ - -/** Returns true if we already have a session with that device */ -export async function hasSession(device: string): Promise { - // Session does not use the concept of a deviceId, thus it's always 1 - const address = new window.libsignal.SignalProtocolAddress(device, 1); - const sessionCipher = new window.libsignal.SessionCipher( - window.textsecure.storage.protocol, - address - ); - - return sessionCipher.hasOpenSession(); -} - -/** - * Returns true if we sent a session request to that device already OR - * if a session request to that device is right now being sent. - */ -export async function hasSentSessionRequest(device: string): Promise { - const pendingSend = pendingSendSessionsTimestamp.has(device); - const hasSent = await _hasSentSessionRequest(device); + return sessionCipher.hasOpenSession(); + } - return pendingSend || hasSent; -} + /** + * Returns true if we sent a session request to that device already OR + * if a session request to that device is right now being sent. + */ + public static async hasSentSessionRequest(device: string): Promise { + const pendingSend = SessionProtocol.pendingSendSessionsTimestamp.has(device); + const hasSent = await SessionProtocol._hasSentSessionRequest(device); -/** - * Triggers a SessionResetMessage to be sent if: - * - we do not already have a session and - * - we did not sent a session request already to that device and - * - we do not have a session request currently being send to that device - */ -export async function sendSessionRequestIfNeeded( - device: string -): Promise { - if (hasSession(device) || hasSentSessionRequest(device)) { - return Promise.resolve(); - } - - const preKeyBundle = await window.libloki.storage.getPreKeyBundleForContact( - device - ); - const sessionReset = new SessionResetMessage({ - preKeyBundle, - timestamp: Date.now(), - }); - - return sendSessionRequest(sessionReset, device); -} + return pendingSend || hasSent; + } -/** */ -export async function sendSessionRequest( - message: SessionResetMessage, - device: string -): Promise { - const timestamp = Date.now(); - - // mark the session as being pending send with current timestamp - // so we know we already triggered a new session with that device - pendingSendSessionsTimestamp.add(device); - // const rawMessage = toRawMessage(message); - // // TODO: Send out the request via MessageSender - - // try { - // await MessageSender.send(rawMessage); - // await _updateSentSessionTimestamp(device, timestamp); - // } catch (e) { - // window.console.log('Failed to send session request to', device); - // } finally { - // pendingSendSessionsTimestamp.delete(device); - // } -} + /** + * Triggers a SessionResetMessage to be sent if: + * - we do not already have a session and + * - we did not sent a session request already to that device and + * - we do not have a session request currently being send to that device + */ + public static async sendSessionRequestIfNeeded( + device: string + ): Promise { + if (SessionProtocol.hasSession(device) || SessionProtocol.hasSentSessionRequest(device)) { + return Promise.resolve(); + } -/** - * Called when a session is establish so we store on database this info. - */ -export async function onSessionEstablished(device: string) { - // remove our existing sent timestamp for that device - return _updateSentSessionTimestamp(device, undefined); -} + const preKeyBundle = await window.libloki.storage.getPreKeyBundleForContact( + device + ); + const sessionReset = new SessionResetMessage({ + preKeyBundle, + timestamp: Date.now(), + }); -export async function shouldProcessSessionRequest( - device: string, - messageTimestamp: number -): Promise { - const existingSentTimestamp = (await _getSentSessionRequest(device)) || 0; - const existingProcessedTimestamp = - (await _getProcessedSessionRequest(device)) || 0; - - return ( - messageTimestamp > existingSentTimestamp && - messageTimestamp > existingProcessedTimestamp - ); -} + return SessionProtocol.sendSessionRequest(sessionReset, device); + } -export async function onSessionRequestProcessed(device: string) { - return _updateProcessedSessionTimestamp(device, Date.now()); -} + /** */ + public static async sendSessionRequest( + message: SessionResetMessage, + device: string + ): Promise { + const timestamp = Date.now(); + + // mark the session as being pending send with current timestamp + // so we know we already triggered a new session with that device + SessionProtocol.pendingSendSessionsTimestamp.add(device); + // const rawMessage = toRawMessage(message); + // // TODO: Send out the request via MessageSender + + // try { + // await MessageSender.send(rawMessage); + // await SessionProtocolupdateSentSessionTimestamp(device, timestamp); + // } catch (e) { + // window.console.log('Failed to send session request to', device); + // } finally { + // SessionProtocolpendingSendSessionsTimestamp.delete(device); + // } + } -/** ======= local / utility functions ======= */ + /** + * Called when a session is establish so we store on database this info. + */ + public static async onSessionEstablished(device: string) { + // remove our existing sent timestamp for that device + return SessionProtocol.updateSentSessionTimestamp(device, undefined); + } -/** - * We only need to fetch once from the database, because we are the only one writing to it - */ -async function _fetchFromDBIfNeeded(): Promise { - if (!sentSessionsTimestamp) { - const sentItem = await getItemById( - 'sentSessionsTimestamp' + public static async shouldProcessSessionRequest( + device: string, + messageTimestamp: number + ): Promise { + const existingSentTimestamp = (await SessionProtocol.getSentSessionRequest(device)) || 0; + const existingProcessedTimestamp = + (await SessionProtocol.getProcessedSessionRequest(device)) || 0; + + return ( + messageTimestamp > existingSentTimestamp && + messageTimestamp > existingProcessedTimestamp ); - if (sentItem) { - sentSessionsTimestamp = sentItem.value; - } else { - sentSessionsTimestamp = {}; - } + } - const processedItem = await getItemById( - 'processedSessionsTimestamp' - ); - if (processedItem) { - processedSessionsTimestamp = processedItem.value; - } else { - processedSessionsTimestamp = {}; + public static async onSessionRequestProcessed(device: string) { + return SessionProtocol.updateProcessedSessionTimestamp(device, Date.now()); + } + + public static reset() { + SessionProtocol.dbLoaded = false; + SessionProtocol.sentSessionsTimestamp = {}; + SessionProtocol.processedSessionsTimestamp = {}; + } + + + /** + * We only need to fetch once from the database, because we are the only one writing to it + */ + private static async fetchFromDBIfNeeded(): Promise { + if (!SessionProtocol.dbLoaded) { + const sentItem = await getItemById( + 'sentSessionsTimestamp' + ); + if (sentItem) { + SessionProtocol.sentSessionsTimestamp = sentItem.value; + } else { + SessionProtocol.sentSessionsTimestamp = {}; + } + + const processedItem = await getItemById( + 'processedSessionsTimestamp' + ); + if (processedItem) { + SessionProtocol.processedSessionsTimestamp = processedItem.value; + } else { + SessionProtocol.processedSessionsTimestamp = {}; + } + SessionProtocol.dbLoaded = true; } } -} -async function _writeToDBSentSessions(): Promise { - const data = { - id: 'sentSessionsTimestamp', - value: JSON.stringify(sentSessionsTimestamp), - }; + private static async writeToDBSentSessions(): Promise { + const data = { + id: 'sentSessionsTimestamp', + value: JSON.stringify(SessionProtocol.sentSessionsTimestamp), + }; - await createOrUpdateItem(data); -} + await createOrUpdateItem(data); + } -async function _writeToDBProcessedSessions(): Promise { - const data = { - id: 'processedSessionsTimestamp', - value: JSON.stringify(processedSessionsTimestamp), - }; + private static async writeToDBProcessedSessions(): Promise { + const data = { + id: 'processedSessionsTimestamp', + value: JSON.stringify(SessionProtocol.processedSessionsTimestamp), + }; - await createOrUpdateItem(data); -} + await createOrUpdateItem(data); + } -/** - * This is a utility function to avoid duplicated code of _updateSentSessionTimestamp and _updateProcessedSessionTimestamp - */ -async function _updateSessionTimestamp( - device: string, - timestamp: number | undefined, - map: StringToNumberMap -): Promise { - await _fetchFromDBIfNeeded(); - if (!timestamp) { - if (!!map[device]) { - delete map.device; - - return true; + /** + * This is a utility function to avoid duplicated code of updateSentSessionTimestamp and updateProcessedSessionTimestamp + */ + private static async updateSessionTimestamp( + device: string, + timestamp: number | undefined, + map: StringToNumberMap + ): Promise { + await SessionProtocol.fetchFromDBIfNeeded(); + if (!timestamp) { + if (!!map[device]) { + delete map.device; + // FIXME double check how are args handle in ts (by ref/value) + return true; + } + + return false; } + map[device] = timestamp; - return false; + return true; } - map[device] = timestamp; - return true; -} - -/** - * - * @param device the device id - * @param timestamp undefined to remove the key/value pair, otherwise updates the sent timestamp and write to DB - */ -async function _updateSentSessionTimestamp( - device: string, - timestamp: number | undefined -): Promise { - if (_updateSessionTimestamp(device, timestamp, sentSessionsTimestamp)) { - await _writeToDBSentSessions(); + /** + * + * @param device the device id + * @param timestamp undefined to remove the key/value pair, otherwise updates the sent timestamp and write to DB + */ + private static async updateSentSessionTimestamp( + device: string, + timestamp: number | undefined + ): Promise { + if (SessionProtocol.updateSessionTimestamp(device, timestamp, SessionProtocol.sentSessionsTimestamp)) { + await SessionProtocol.writeToDBSentSessions(); + } } -} -/** - * timestamp undefined to remove the key/value pair, otherwise updates the processed timestamp and writes to DB - */ -async function _updateProcessedSessionTimestamp( - device: string, - timestamp: number | undefined -): Promise { - if (_updateSessionTimestamp(device, timestamp, processedSessionsTimestamp)) { - await _writeToDBProcessedSessions(); + /** + * timestamp undefined to remove the key/value pair, otherwise updates the processed timestamp and writes to DB + */ + private static async updateProcessedSessionTimestamp( + device: string, + timestamp: number | undefined + ): Promise { + if (SessionProtocol.updateSessionTimestamp(device, timestamp, SessionProtocol.processedSessionsTimestamp)) { + await SessionProtocol.writeToDBProcessedSessions(); + } } -} -/** - * This is a utility function to avoid duplicate code between `_getProcessedSessionRequest()` and `_getSentSessionRequest()` - */ -async function _getSessionRequest( - device: string, - map: StringToNumberMap -): Promise { - await _fetchFromDBIfNeeded(); + /** + * This is a utility function to avoid duplicate code between `getProcessedSessionRequest()` and `getSentSessionRequest()` + */ + private static async getSessionRequest( + device: string, + map: StringToNumberMap + ): Promise { + await SessionProtocol.fetchFromDBIfNeeded(); - return map[device]; -} + return map[device]; + } -async function _getSentSessionRequest( - device: string -): Promise { - return _getSessionRequest(device, sentSessionsTimestamp); -} + private static async getSentSessionRequest( + device: string + ): Promise { + return SessionProtocol.getSessionRequest(device, SessionProtocol.sentSessionsTimestamp); + } -async function _getProcessedSessionRequest( - device: string -): Promise { - return _getSessionRequest(device, processedSessionsTimestamp); -} + private static async getProcessedSessionRequest( + device: string + ): Promise { + return SessionProtocol.getSessionRequest(device, SessionProtocol.processedSessionsTimestamp); + } -async function _hasSentSessionRequest(device: string): Promise { - await _fetchFromDBIfNeeded(); + private static async _hasSentSessionRequest(device: string): Promise { + await SessionProtocol.fetchFromDBIfNeeded(); - return !!sentSessionsTimestamp[device]; + return !!SessionProtocol.sentSessionsTimestamp[device]; + } } diff --git a/ts/session/protocols/index.ts b/ts/session/protocols/index.ts index e0cfeb680..dbf3e0fd0 100644 --- a/ts/session/protocols/index.ts +++ b/ts/session/protocols/index.ts @@ -1,4 +1,4 @@ -import * as SessionProtocol from './SessionProtocol'; +import {SessionProtocol} from './SessionProtocol'; import * as MultiDeviceProtocol from './MultiDeviceProtocol'; export { SessionProtocol, MultiDeviceProtocol };