From b68338e26ccdddc2de6e06720ba51b2d97f88533 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 21 Apr 2021 11:58:14 +1000 Subject: [PATCH] working get and post request with opengroup api v2 --- js/background.js | 13 +- js/modules/loki_primitives.d.ts | 5 - js/modules/loki_primitives.js | 57 ---- js/modules/loki_public_chat_api.js | 1 - libloki/crypto.d.ts | 17 +- libloki/crypto.js | 23 +- ts/components/session/ActionsPanel.tsx | 15 +- ts/crypto/crypto.ts | 0 ts/data/opengroups.ts | 18 +- ts/models/conversation.ts | 3 + ts/opengroup/opengroupV1/OpenGroup.ts | 16 +- ts/opengroup/opengroupV2/ApiUtil.ts | 72 ++++ ts/opengroup/opengroupV2/JoinOpenGroupV2.ts | 26 +- ts/opengroup/opengroupV2/OpenGroupAPIV2.ts | 310 +++++++++++------- .../opengroupV2/OpenGroupManagerV2.ts | 60 ++-- .../opengroupV2/OpenGroupMessageV2.ts | 100 ++++++ ts/opengroup/opengroupV2/OpenGroupPollerV2.ts | 64 +--- ts/opengroup/utils/OpenGroupUtils.ts | 6 +- ts/session/constants.ts | 6 + ts/session/conversations/index.ts | 10 +- ts/session/onions/index.ts | 2 +- ts/session/onions/onionSend.ts | 9 +- ts/session/snode_api/onions.ts | 61 ++-- ts/session/snode_api/serviceNodeAPI.ts | 13 +- ts/session/snode_api/snodePool.ts | 6 +- ts/session/utils/Promise.ts | 65 ++++ ts/test/session/integration/common.ts | 4 - ts/test/session/unit/utils/Messages_test.ts | 11 +- 28 files changed, 621 insertions(+), 372 deletions(-) create mode 100644 ts/crypto/crypto.ts create mode 100644 ts/opengroup/opengroupV2/ApiUtil.ts create mode 100644 ts/opengroup/opengroupV2/OpenGroupMessageV2.ts diff --git a/js/background.js b/js/background.js index 5e7dd60fa..835ccb021 100644 --- a/js/background.js +++ b/js/background.js @@ -122,16 +122,15 @@ window.lokiMessageAPI = new window.LokiMessageAPI(); // singleton to relay events to libtextsecure/message_receiver window.lokiPublicChatAPI = new window.LokiPublicChatAPI(ourKey); - //FIXME audric // singleton to interface the File server // If already exists we registered as a secondary device - // if (!window.lokiFileServerAPI) { - // window.lokiFileServerAPIFactory = new window.LokiFileServerAPI(ourKey); - // window.lokiFileServerAPI = window.lokiFileServerAPIFactory.establishHomeConnection( - // window.getDefaultFileServer() - // ); - // } + if (!window.lokiFileServerAPI) { + window.lokiFileServerAPIFactory = new window.LokiFileServerAPI(ourKey); + window.lokiFileServerAPI = window.lokiFileServerAPIFactory.establishHomeConnection( + window.getDefaultFileServer() + ); + } window.initialisedAPI = true; }; diff --git a/js/modules/loki_primitives.d.ts b/js/modules/loki_primitives.d.ts index e189b00c7..32ad3e232 100644 --- a/js/modules/loki_primitives.d.ts +++ b/js/modules/loki_primitives.d.ts @@ -1,9 +1,4 @@ export async function sleepFor(ms: number); -export async function allowOnlyOneAtATime( - name: any, - process: any, - timeout?: any -); export async function abortableIterator( array: Array, diff --git a/js/modules/loki_primitives.js b/js/modules/loki_primitives.js index f296b9ea0..d509d49cf 100644 --- a/js/modules/loki_primitives.js +++ b/js/modules/loki_primitives.js @@ -18,62 +18,6 @@ const firstTrue = ps => { return Promise.race(newPs); }; -// one action resolves all -const snodeGlobalLocks = {}; -async function allowOnlyOneAtATime(name, process, timeout) { - // if currently not in progress - if (snodeGlobalLocks[name] === undefined) { - // set lock - snodeGlobalLocks[name] = new Promise(async (resolve, reject) => { - // set up timeout feature - let timeoutTimer = null; - if (timeout) { - timeoutTimer = setTimeout(() => { - log.warn( - `loki_primitives:::allowOnlyOneAtATime - TIMEDOUT after ${timeout}s` - ); - delete snodeGlobalLocks[name]; // clear lock - reject(); - }, timeout); - } - // do actual work - let innerRetVal; - try { - innerRetVal = await process(); - } catch (e) { - if (typeof e === 'string') { - log.error(`loki_primitives:::allowOnlyOneAtATime - error ${e}`); - } else { - log.error( - `loki_primitives:::allowOnlyOneAtATime - error ${e.code} ${e.message}` - ); - } - - // clear timeout timer - if (timeout) { - if (timeoutTimer !== null) { - clearTimeout(timeoutTimer); - timeoutTimer = null; - } - } - delete snodeGlobalLocks[name]; // clear lock - throw e; - } - // clear timeout timer - if (timeout) { - if (timeoutTimer !== null) { - clearTimeout(timeoutTimer); - timeoutTimer = null; - } - } - delete snodeGlobalLocks[name]; // clear lock - // release the kraken - resolve(innerRetVal); - }); - } - return snodeGlobalLocks[name]; -} - function abortableIterator(array, iterator) { let abortIteration = false; @@ -114,7 +58,6 @@ function abortableIterator(array, iterator) { module.exports = { sleepFor, - allowOnlyOneAtATime, abortableIterator, firstTrue, }; diff --git a/js/modules/loki_public_chat_api.js b/js/modules/loki_public_chat_api.js index aa70ccd19..f5512cff8 100644 --- a/js/modules/loki_public_chat_api.js +++ b/js/modules/loki_public_chat_api.js @@ -11,7 +11,6 @@ class LokiPublicChatFactoryAPI extends EventEmitter { this.allMembers = []; this.openGroupPubKeys = {}; // Multidevice states - this.primaryUserProfileName = {}; } // MessageReceiver.connect calls this diff --git a/libloki/crypto.d.ts b/libloki/crypto.d.ts index 703ce7c40..5b26ed94a 100644 --- a/libloki/crypto.d.ts +++ b/libloki/crypto.d.ts @@ -1,14 +1,15 @@ -declare enum PairingTypeEnum { - REQUEST = 1, - GRANT, -} - export interface CryptoInterface { DHDecrypt: any; DHEncrypt: any; - DecryptGCM: any; // AES-GCM - EncryptGCM: any; // AES-GCM - PairingType: PairingTypeEnum; + DecryptAESGCM: ( + symmetricKey: ArrayBuffer, + ivAndCiphertext: ArrayBuffer + ) => Promise; // AES-GCM + deriveSymmetricKey: ( + pubkey: ArrayBuffer, + seckey: ArrayBuffer + ) => Promise; + EncryptAESGCM: any; // AES-GCM _decodeSnodeAddressToPubKey: any; decryptToken: any; encryptForPubkey: any; diff --git a/libloki/crypto.js b/libloki/crypto.js index 701f58131..0a372a03f 100644 --- a/libloki/crypto.js +++ b/libloki/crypto.js @@ -32,10 +32,10 @@ return ivAndCiphertext; } - async function deriveSymmetricKey(pubkey, seckey) { + async function deriveSymmetricKey(x25519PublicKey, x25519PrivateKey) { const ephemeralSecret = await libsignal.Curve.async.calculateAgreement( - pubkey, - seckey + x25519PublicKey, + x25519PrivateKey ); const salt = window.Signal.Crypto.bytesFromString('LOKI'); @@ -63,12 +63,12 @@ const symmetricKey = await deriveSymmetricKey(snPubkey, ephemeral.privKey); - const ciphertext = await EncryptGCM(symmetricKey, payloadBytes); + const ciphertext = await EncryptAESGCM(symmetricKey, payloadBytes); return { ciphertext, symmetricKey, ephemeralKey: ephemeral.pubKey }; } - async function EncryptGCM(symmetricKey, plaintext) { + async function EncryptAESGCM(symmetricKey, plaintext) { const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH)); const key = await crypto.subtle.importKey( @@ -95,7 +95,7 @@ return ivAndCiphertext; } - async function DecryptGCM(symmetricKey, ivAndCiphertext) { + async function DecryptAESGCM(symmetricKey, ivAndCiphertext) { const nonce = ivAndCiphertext.slice(0, NONCE_LENGTH); const ciphertext = ivAndCiphertext.slice(NONCE_LENGTH); @@ -165,18 +165,13 @@ const sha512 = data => crypto.subtle.digest('SHA-512', data); - const PairingType = Object.freeze({ - REQUEST: 1, - GRANT: 2, - }); - window.libloki.crypto = { DHEncrypt, - EncryptGCM, // AES-GCM + EncryptAESGCM, // AES-GCM DHDecrypt, - DecryptGCM, // AES-GCM + DecryptAESGCM, // AES-GCM decryptToken, - PairingType, + deriveSymmetricKey, generateEphemeralKeyPair, encryptForPubkey, _decodeSnodeAddressToPubKey: decodeSnodeAddressToPubKey, diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index 8eedbe979..c69f75dfe 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -33,6 +33,10 @@ import { joinOpenGroupV2, parseOpenGroupV2, } from '../../opengroup/opengroupV2/JoinOpenGroupV2'; +import { + getAuthToken, + getModerators, +} from '../../opengroup/opengroupV2/OpenGroupAPIV2'; // tslint:disable-next-line: no-import-side-effect no-submodule-imports export enum SectionType { @@ -180,13 +184,20 @@ export const ActionsPanel = () => { // trigger a sync message if needed for our other devices // 'http://sessionopengroup.com/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231b' // 'https://sog.ibolpap.finance/main?public_key=b464aa186530c97d6bcf663a3a3b7465a5f782beaa67c83bee99468824b4aa10' + // 'https://opengroup.bilb.us/main?public_key=1352534ba73d4265973280431dbc72e097a3e43275d1ada984f9805b4943047d' void syncConfiguration(); const parsedRoom = parseOpenGroupV2( - 'https://sog.ibolpap.finance/main?public_key=b464aa186530c97d6bcf663a3a3b7465a5f782beaa67c83bee99468824b4aa10' + 'https://opengroup.bilb.us/main?public_key=1352534ba73d4265973280431dbc72e097a3e43275d1ada984f9805b4943047d' ); if (parsedRoom) { - setTimeout(() => void joinOpenGroupV2(parsedRoom), 10000); + setTimeout(async () => { + await joinOpenGroupV2(parsedRoom); + await getModerators({ + serverUrl: parsedRoom.serverUrl, + roomId: parsedRoom.roomId, + }); + }, 6000); } }, []); diff --git a/ts/crypto/crypto.ts b/ts/crypto/crypto.ts new file mode 100644 index 000000000..e69de29bb diff --git a/ts/data/opengroups.ts b/ts/data/opengroups.ts index b039f26d3..970037c88 100644 --- a/ts/data/opengroups.ts +++ b/ts/data/opengroups.ts @@ -47,11 +47,15 @@ export async function getV2OpenGroupRoom( return opengroupv2Rooms; } -export async function getV2OpenGroupRoomByRoomId( - serverUrl: string, - roomId: string -): Promise { - const room = await channels.getV2OpenGroupRoomByRoomId(serverUrl, roomId); +export async function getV2OpenGroupRoomByRoomId(roomInfos: { + serverUrl: string; + roomId: string; +}): Promise { + console.warn('getting roomInfo', roomInfos); + const room = await channels.getV2OpenGroupRoomByRoomId( + roomInfos.serverUrl, + roomInfos.roomId + ); if (!room) { return undefined; @@ -71,6 +75,7 @@ export async function saveV2OpenGroupRoom( ) { throw new Error('Cannot save v2 room, invalid data'); } + console.warn('saving roomInfo', opengroupsv2Room); await channels.saveV2OpenGroupRoom(opengroupsv2Room); } @@ -78,7 +83,8 @@ export async function saveV2OpenGroupRoom( export async function removeV2OpenGroupRoom( conversationId: string ): Promise { - // TODO sql + console.warn('removing roomInfo', conversationId); + await channels.removeV2OpenGroupRoom(conversationId); } diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 944ba00b7..2a7b2c750 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -200,6 +200,9 @@ export class ConversationModel extends Backbone.Model { public isPublic() { return !!(this.id && this.id.match(/^publicChat:/)); } + public isOpenGroupV2() { + return this.get('type') === ConversationType.OPEN_GROUP; + } public isClosedGroup() { return this.get('type') === ConversationType.GROUP && !this.isPublic(); } diff --git a/ts/opengroup/opengroupV1/OpenGroup.ts b/ts/opengroup/opengroupV1/OpenGroup.ts index 8bb30e6ce..3ff284769 100644 --- a/ts/opengroup/opengroupV1/OpenGroup.ts +++ b/ts/opengroup/opengroupV1/OpenGroup.ts @@ -1,7 +1,7 @@ -import { allowOnlyOneAtATime } from '../../../js/modules/loki_primitives'; import { ConversationModel, ConversationType } from '../../models/conversation'; import { ConversationController } from '../../session/conversations'; import { PromiseUtils } from '../../session/utils'; +import { allowOnlyOneAtATime } from '../../session/utils/Promise'; import { forceSyncConfigurationNowIfNeeded } from '../../session/utils/syncUtils'; import { prefixify } from '../utils/OpenGroupUtils'; @@ -226,24 +226,24 @@ export class OpenGroup { * To avoid this issue, we allow only a single join of a specific opengroup at a time. */ private static async attemptConnectionOneAtATime( - serverURL: string, + serverUrl: string, channelId: number = 1 ): Promise { - if (!serverURL) { + if (!serverUrl) { throw new Error('Cannot join open group with empty URL'); } - const oneAtaTimeStr = `oneAtaTimeOpenGroupJoin:${serverURL}${channelId}`; + const oneAtaTimeStr = `oneAtaTimeOpenGroupJoin:${serverUrl}${channelId}`; return allowOnlyOneAtATime(oneAtaTimeStr, async () => { - return OpenGroup.attemptConnection(serverURL, channelId); + return OpenGroup.attemptConnection(serverUrl, channelId); }); } // Attempts a connection to an open group server private static async attemptConnection( - serverURL: string, + serverUrl: string, channelId: number ): Promise { - let completeServerURL = serverURL.toLowerCase(); + let completeServerURL = serverUrl.toLowerCase(); const valid = OpenGroup.validate(completeServerURL); if (!valid) { return new Promise((_resolve, reject) => { @@ -254,7 +254,7 @@ export class OpenGroup { // Add http or https prefix to server completeServerURL = prefixify(completeServerURL); - const rawServerURL = serverURL + const rawServerURL = serverUrl .replace(/^https?:\/\//i, '') .replace(/[/\\]+$/i, ''); diff --git a/ts/opengroup/opengroupV2/ApiUtil.ts b/ts/opengroup/opengroupV2/ApiUtil.ts new file mode 100644 index 000000000..e138c3754 --- /dev/null +++ b/ts/opengroup/opengroupV2/ApiUtil.ts @@ -0,0 +1,72 @@ +export const defaultServer = 'https://sessionopengroup.com'; +export const defaultServerPublicKey = + '658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231b'; + +export type OpenGroupV2Request = { + method: 'GET' | 'POST' | 'DELETE' | 'PUT'; + room: string; + server: string; + endpoint: string; + // queryParams are used for post or get, but not the same way + queryParams?: Record; + headers?: Record; + isAuthRequired: boolean; + // Always `true` under normal circumstances. You might want to disable this when running over Lokinet. + useOnionRouting?: boolean; +}; + +export type OpenGroupV2Info = { + id: string; + name: string; + imageId?: string; +}; + +/** + * Try to build an full url and check it for validity. + * @returns null if the check failed. the built URL otherwise + */ +export const buildUrl = (request: OpenGroupV2Request): URL | null => { + let rawURL = `${request.server}/${request.endpoint}`; + if (request.method === 'GET') { + const entries = Object.entries(request.queryParams || {}); + + if (entries.length) { + const queryString = entries + .map(([key, value]) => `${key}=${value}`) + .join('&'); + rawURL += `?${queryString}`; + } + } + // this just check that the URL is valid + try { + return new URL(`${rawURL}`); + } catch (error) { + return null; + } +}; + +/** + * Map of serverUrl to roomId to list of moderators as a Set + */ +export const cachedModerators: Map< + string, + Map> +> = new Map(); + +export const setCachedModerators = ( + serverUrl: string, + roomId: string, + newModerators: Array +) => { + const allRoomsMods = cachedModerators.get(serverUrl); + if (!allRoomsMods) { + cachedModerators.set(serverUrl, new Map()); + } + // tslint:disable: no-non-null-assertion + if (!allRoomsMods!.get(roomId)) { + allRoomsMods!.set(roomId, new Set()); + } + newModerators.forEach(m => { + allRoomsMods!.get(roomId)?.add(m); + }); +}; diff --git a/ts/opengroup/opengroupV2/JoinOpenGroupV2.ts b/ts/opengroup/opengroupV2/JoinOpenGroupV2.ts index 76cfa7220..40cb71368 100644 --- a/ts/opengroup/opengroupV2/JoinOpenGroupV2.ts +++ b/ts/opengroup/opengroupV2/JoinOpenGroupV2.ts @@ -1,13 +1,15 @@ -import { allowOnlyOneAtATime } from '../../../js/modules/loki_primitives'; import { getV2OpenGroupRoomByRoomId, OpenGroupV2Room, + removeV2OpenGroupRoom, } from '../../data/opengroups'; -import { ConversationModel } from '../../models/conversation'; import { ConversationController } from '../../session/conversations'; import { PromiseUtils } from '../../session/utils'; import { forceSyncConfigurationNowIfNeeded } from '../../session/utils/syncUtils'; -import { prefixify } from '../utils/OpenGroupUtils'; +import { + getOpenGroupV2ConversationId, + prefixify, +} from '../utils/OpenGroupUtils'; import { attemptConnectionV2OneAtATime } from './OpenGroupManagerV2'; const protocolRegex = '(https?://)?'; @@ -89,13 +91,19 @@ export async function joinOpenGroupV2( const publicKey = room.serverPublicKey.toLowerCase(); const prefixedServer = prefixify(serverUrl); - const alreadyExist = await getV2OpenGroupRoomByRoomId(serverUrl, roomId); + const alreadyExist = await getV2OpenGroupRoomByRoomId({ serverUrl, roomId }); + const conversationId = getOpenGroupV2ConversationId(serverUrl, roomId); + const existingConvo = ConversationController.getInstance().get( + conversationId + ); - //FIXME audric - // if (alreadyExist) { - // window.log.warn('Skipping join opengroupv2: already exists'); - // return; - // } + if (alreadyExist && existingConvo) { + window.log.warn('Skipping join opengroupv2: already exists'); + return; + } else if (alreadyExist) { + // we don't have a convo associated with it. Remove the room in db + await removeV2OpenGroupRoom(conversationId); + } // Try to connect to server try { diff --git a/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts b/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts index 6bb59fd79..9f257f271 100644 --- a/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts +++ b/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts @@ -1,71 +1,21 @@ -import { Headers } from 'node-fetch'; -import { allowOnlyOneAtATime } from '../../../js/modules/loki_primitives'; -import { getV2OpenGroupRoomByRoomId } from '../../data/opengroups'; +import { + getV2OpenGroupRoomByRoomId, + saveV2OpenGroupRoom, +} from '../../data/opengroups'; import { sendViaOnion } from '../../session/onions/onionSend'; -import { fromBase64ToArray } from '../../session/utils/String'; +import { allowOnlyOneAtATime } from '../../session/utils/Promise'; +import { fromBase64ToArrayBuffer, toHex } from '../../session/utils/String'; import { getIdentityKeyPair, getOurPubKeyStrFromCache, } from '../../session/utils/User'; - -// HTTP HEADER FOR OPEN GROUP V2 -const HEADER_ROOM = 'Room'; -const HEADER_AUTHORIZATION = 'Authorization'; - -const PARAMETER_PUBLIC_KEY = 'public_key'; - -export const openGroupV2PubKeys: Record = {}; - -export const defaultServer = 'https://sessionopengroup.com'; -export const defaultServerPublicKey = - '658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231b'; - -type OpenGroupV2Request = { - method: 'GET' | 'POST' | 'DELETE' | 'PUT'; - room: string; - server: string; - endpoint: string; - // queryParams are used for post or get, but not the same way - queryParams?: Map; - headers?: Headers; - isAuthRequired: boolean; - // Always `true` under normal circumstances. You might want to disable this when running over Lokinet. - useOnionRouting?: boolean; -}; - -type OpenGroupV2Info = { - id: string; - name: string; - imageId?: string; -}; - -/** - * Try to build an full url and check it for validity. - * @returns null if the check failed. the built URL otherwise - */ -const buildUrl = (request: OpenGroupV2Request): URL | null => { - let rawURL = `${request.server}/${request.endpoint}`; - if (request.method === 'GET') { - if (!!request.queryParams?.size) { - const entries = [...request.queryParams.entries()]; - const queryString = entries - .map(([key, value]) => `${key}=${value}`) - .join('&'); - rawURL += `/?${queryString}`; - } - } - // this just check that the URL is valid - try { - return new URL(`${rawURL}`); - } catch (error) { - return null; - } -}; - -/** - * Map of serverUrl to roomId to list of moderators as a Set - */ -export const moderators: Map>> = new Map(); +import { + buildUrl, + cachedModerators, + OpenGroupV2Info, + OpenGroupV2Request, + setCachedModerators, +} from './ApiUtil'; // This function might throw async function sendOpenGroupV2Request( @@ -78,33 +28,51 @@ async function sendOpenGroupV2Request( } // set the headers sent by the caller, and the roomId. - const headersWithRoom = request.headers || new Headers(); - headersWithRoom.append(HEADER_ROOM, request.room); - console.warn(`request: ${builtUrl}`); + const headers = request.headers || {}; + headers.Room = request.room; + console.warn(`sending request: ${builtUrl}`); + let body = ''; + if (request.method !== 'GET') { + body = JSON.stringify(request.queryParams); + } // request.useOnionRouting === undefined defaults to true if (request.useOnionRouting || request.useOnionRouting === undefined) { - const roomDetails = await getV2OpenGroupRoomByRoomId( - request.server, - request.room - ); + const roomDetails = await getV2OpenGroupRoomByRoomId({ + serverUrl: request.server, + roomId: request.room, + }); if (!roomDetails?.serverPublicKey) { throw new Error('PublicKey not found for this server.'); } // Because auth happens on a per-room basis, we need both to make an authenticated request if (request.isAuthRequired && request.room) { - const token = await getAuthToken(request.room, request.server); + // this call will either return the token on the db, + // or the promise currently fetching a new token for that same room + // or fetch a new token for that room if no other request are currently being made. + const token = await getAuthToken({ + roomId: request.room, + serverUrl: request.server, + }); if (!token) { throw new Error('Failed to get token for open group v2'); } - headersWithRoom.append(HEADER_AUTHORIZATION, token); - - // FIXME use headersWithRoom + headers.Authorization = token; + const res = await sendViaOnion(roomDetails.serverPublicKey, builtUrl, { + method: request.method, + headers, + body, + }); + // A 401 means that we didn't provide a (valid) auth token for a route that required one. We use this as an + // indication that the token we're using has expired. Note that a 403 has a different meaning; it means that + // we provided a valid token but it doesn't have a high enough permission level for the route in question. + return res; } else { // no need for auth, just do the onion request const res = await sendViaOnion(roomDetails.serverPublicKey, builtUrl, { method: request.method, - headers: { ...headersWithRoom.entries() }, + headers, + body, }); return res; } @@ -118,53 +86,78 @@ async function sendOpenGroupV2Request( } // tslint:disable: member-ordering -export async function requestNewAuthToken( - serverUrl: string, - roomid: string -): Promise { +export async function requestNewAuthToken({ + serverUrl, + roomId, +}: { + serverUrl: string; + roomId: string; +}): Promise { const userKeyPair = await getIdentityKeyPair(); if (!userKeyPair) { throw new Error('Failed to fetch user keypair'); } const ourPubkey = getOurPubKeyStrFromCache(); - const parameters = [PARAMETER_PUBLIC_KEY, ourPubkey] as [string, string]; + const parameters = {} as Record; + parameters.public_key = ourPubkey; const request: OpenGroupV2Request = { method: 'GET', - room: roomid, + room: roomId, server: serverUrl, - queryParams: new Map([parameters]), + queryParams: parameters, isAuthRequired: false, endpoint: 'auth_token_challenge', }; const json = (await sendOpenGroupV2Request(request)) as any; // parse the json - const { challenge } = json; - if (!challenge) { + if (!json || !json?.result?.challenge) { throw new Error('Parsing failed'); } const { ciphertext: base64EncodedCiphertext, ephemeral_public_key: base64EncodedEphemeralPublicKey, - } = challenge; + } = json?.result?.challenge; if (!base64EncodedCiphertext || !base64EncodedEphemeralPublicKey) { throw new Error('Parsing failed'); } - const ciphertext = fromBase64ToArray(base64EncodedCiphertext); - const ephemeralPublicKey = fromBase64ToArray(base64EncodedEphemeralPublicKey); - console.warn('ciphertext', ciphertext); - console.warn('ephemeralPublicKey', ephemeralPublicKey); + const ciphertext = fromBase64ToArrayBuffer(base64EncodedCiphertext); + const ephemeralPublicKey = fromBase64ToArrayBuffer( + base64EncodedEphemeralPublicKey + ); + try { + const symmetricKey = await window.libloki.crypto.deriveSymmetricKey( + ephemeralPublicKey, + userKeyPair.privKey + ); + + const plaintextBuffer = await window.libloki.crypto.DecryptAESGCM( + symmetricKey, + ciphertext + ); + + const token = toHex(plaintextBuffer); + + console.warn('token', token); + return token; + } catch (e) { + window.log.error('Failed to decrypt token open group v2'); + throw e; + } } /** * This function might throw * */ -export async function openGroupV2GetRoomInfo( - roomId: string, - serverUrl: string -): Promise { +export async function openGroupV2GetRoomInfo({ + serverUrl, + roomId, +}: { + roomId: string; + serverUrl: string; +}): Promise { const request: OpenGroupV2Request = { method: 'GET', server: serverUrl, @@ -193,40 +186,133 @@ export async function openGroupV2GetRoomInfo( async function claimAuthToken( authToken: string, serverUrl: string, - roomid: string -): Promise { - const ourPubkey = getOurPubKeyStrFromCache(); - const parameters = [PARAMETER_PUBLIC_KEY, ourPubkey] as [string, string]; + roomId: string +): Promise { // Set explicitly here because is isn't in the database yet at this point - const headers = new Headers({ HEADER_AUTHORIZATION: authToken }); + const headers = { Authorization: authToken }; const request: OpenGroupV2Request = { method: 'POST', headers, - room: roomid, + room: roomId, server: serverUrl, - queryParams: new Map([parameters]), + queryParams: { public_key: getOurPubKeyStrFromCache() }, isAuthRequired: false, endpoint: 'claim_auth_token', }; - await sendOpenGroupV2Request(request); + const result = (await sendOpenGroupV2Request(request)) as any; + if (result?.result?.status_code !== 200) { + throw new Error( + `Could not claim token, status code: ${result?.result?.status_code}` + ); + } + return authToken; } -async function getAuthToken( - serverUrl: string, - roomId: string -): Promise { +export async function getAuthToken({ + serverUrl, + roomId, +}: { + serverUrl: string; + roomId: string; +}): Promise { // first try to fetch from db a saved token. - const roomDetails = await getV2OpenGroupRoomByRoomId(serverUrl, roomId); + const roomDetails = await getV2OpenGroupRoomByRoomId({ serverUrl, roomId }); + if (!roomDetails) { + throw new Error('getAuthToken Room does not exist.'); + } if (roomDetails?.token) { return roomDetails?.token; } - // const token = await allowOnlyOneAtATime( - // `getAuthTokenV2${serverUrl}:${roomId}`, - // async () => { - // requestNewAuthToken - // } - // ); + await allowOnlyOneAtATime( + `getAuthTokenV2${serverUrl}:${roomId}`, + async () => { + try { + const token = await requestNewAuthToken({ serverUrl, roomId }); + // claimAuthToken throws if the status code is not valid + const claimedToken = await claimAuthToken(token, serverUrl, roomId); + roomDetails.token = token; + await saveV2OpenGroupRoom(roomDetails); + } catch (e) { + window.log.error('Failed to getAuthToken', e); + throw e; + } + } + ); return 'token'; } + +export const getModerators = async ({ + serverUrl, + roomId, +}: { + serverUrl: string; + roomId: string; +}): Promise> => { + const request: OpenGroupV2Request = { + method: 'GET', + room: roomId, + server: serverUrl, + isAuthRequired: true, + endpoint: 'moderators', + }; + const result = (await sendOpenGroupV2Request(request)) as any; + if (result?.result?.status_code !== 200) { + throw new Error( + `Could not getModerators, status code: ${result?.result?.status_code}` + ); + } + const moderatorsGot = result?.result?.moderators; + if (moderatorsGot === undefined) { + throw new Error( + 'Could not getModerators, got no moderatorsGot at all in json.' + ); + } + setCachedModerators(serverUrl, roomId, moderatorsGot || []); + return moderatorsGot || []; +}; + +export const deleteAuthToken = async ({ + serverUrl, + roomId, +}: { + serverUrl: string; + roomId: string; +}) => { + const request: OpenGroupV2Request = { + method: 'DELETE', + room: roomId, + server: serverUrl, + isAuthRequired: false, + endpoint: 'auth_token', + }; + const result = (await sendOpenGroupV2Request(request)) as any; + if (result?.result?.status_code !== 200) { + throw new Error( + `Could not deleteAuthToken, status code: ${result?.result?.status_code}` + ); + } +}; + +export const getMessages = async ({ + serverUrl, + roomId, +}: { + serverUrl: string; + roomId: string; +}) => { + const request: OpenGroupV2Request = { + method: 'GET', + room: roomId, + server: serverUrl, + isAuthRequired: false, + endpoint: 'auth_token', + }; + const result = (await sendOpenGroupV2Request(request)) as any; + if (result?.result?.status_code !== 200) { + throw new Error( + `Could not deleteAuthToken, status code: ${result?.result?.status_code}` + ); + } +}; diff --git a/ts/opengroup/opengroupV2/OpenGroupManagerV2.ts b/ts/opengroup/opengroupV2/OpenGroupManagerV2.ts index b03432e5a..1f262fcff 100644 --- a/ts/opengroup/opengroupV2/OpenGroupManagerV2.ts +++ b/ts/opengroup/opengroupV2/OpenGroupManagerV2.ts @@ -1,4 +1,3 @@ -import { allowOnlyOneAtATime } from '../../../js/modules/loki_primitives'; import { OpenGroupV2Room, removeV2OpenGroupRoom, @@ -6,6 +5,7 @@ import { } from '../../data/opengroups'; import { ConversationModel, ConversationType } from '../../models/conversation'; import { ConversationController } from '../../session/conversations'; +import { allowOnlyOneAtATime } from '../../session/utils/Promise'; import { getOpenGroupV2ConversationId } from '../utils/OpenGroupUtils'; import { openGroupV2GetRoomInfo } from './OpenGroupAPIV2'; import { OpenGroupPollerV2 } from './OpenGroupPollerV2'; @@ -19,19 +19,19 @@ import { OpenGroupPollerV2 } from './OpenGroupPollerV2'; * To avoid this issue, we allow only a single join of a specific opengroup at a time. */ export async function attemptConnectionV2OneAtATime( - serverURL: string, + serverUrl: string, roomId: string, publicKey: string ): Promise { - const oneAtaTimeStr = `oneAtaTimeOpenGroupV2Join:${serverURL}${roomId}`; + const oneAtaTimeStr = `oneAtaTimeOpenGroupV2Join:${serverUrl}${roomId}`; return allowOnlyOneAtATime(oneAtaTimeStr, async () => { - return attemptConnectionV2(serverURL, roomId, publicKey); + return attemptConnectionV2(serverUrl, roomId, publicKey); }); } /** * - * @param serverURL with protocol, hostname and port included + * @param serverUrl with protocol, hostname and port included */ async function attemptConnectionV2( serverUrl: string, @@ -42,9 +42,7 @@ async function attemptConnectionV2( if (ConversationController.getInstance().get(conversationId)) { // Url incorrect or server not compatible - return new Promise((_resolve, reject) => { - reject(window.i18n('publicChatExists')); - }); + throw new Error(window.i18n('publicChatExists')); } // here, the convo does not exist. Make sure the db is clean too @@ -58,47 +56,31 @@ async function attemptConnectionV2( }; try { - // save the pubkey to the db, the request for room Info will need it and access it from the db + // save the pubkey to the db right now, the request for room Info + // will need it and access it from the db await saveV2OpenGroupRoom(room); - const info = await openGroupV2GetRoomInfo(roomId, serverUrl); + const roomInfos = await openGroupV2GetRoomInfo({ roomId, serverUrl }); const conversation = await ConversationController.getInstance().getOrCreateAndWait( conversationId, ConversationType.OPEN_GROUP ); - conversation.isPublic(); + room.imageID = roomInfos.imageId || undefined; + room.roomName = roomInfos.name || undefined; + await saveV2OpenGroupRoom(room); + console.warn('openGroupRoom info', roomInfos); + + // mark active so it's not in the contacts list but in the conversation list + conversation.set({ + active_at: Date.now(), + }); + await conversation.commit(); - console.warn('openGroupRoom info', info); + return conversation; } catch (e) { window.log.warn('Failed to join open group v2', e); await removeV2OpenGroupRoom(conversationId); + throw new Error(window.i18n('connectToServerFail')); } - - // Get server - // const serverAPI = await window.lokiPublicChatAPI.findOrCreateServer( - // completeServerURL - // ); - // SSL certificate failure or offline - // if (!serverAPI) { - // // Url incorrect or server not compatible - // return new Promise((_resolve, reject) => { - // reject(window.i18n('connectToServerFail')); - // }); - // } - - // // Create conversation - // const conversation = await ConversationController.getInstance().getOrCreateAndWait( - // conversationId, - // 'group' - // ); - - // // Convert conversation to a public one - // await conversation.setPublicSource(completeServerURL, channelId); - - // // and finally activate it - // void conversation.getPublicSendData(); // may want "await" if you want to use the API - - // return conversation; - return undefined; } export class OpenGroupManagerV2 { diff --git a/ts/opengroup/opengroupV2/OpenGroupMessageV2.ts b/ts/opengroup/opengroupV2/OpenGroupMessageV2.ts new file mode 100644 index 000000000..835e49634 --- /dev/null +++ b/ts/opengroup/opengroupV2/OpenGroupMessageV2.ts @@ -0,0 +1,100 @@ +import { getSodium } from '../../session/crypto'; +import { UserUtils } from '../../session/utils'; +import { + fromBase64ToArray, + fromHex, + fromHexToArray, + toHex, +} from '../../session/utils/String'; + +export class OpenGroupMessageV2 { + public serverId?: number; + public sender?: string; + public sentTimestamp: number; + public base64EncodedData: string; + public base64EncodedSignature?: string; + + constructor(messageData: { + serverId?: number; + sender?: string; + sentTimestamp: number; + base64EncodedData: string; + base64EncodedSignature?: string; + }) { + const { + base64EncodedData, + sentTimestamp, + base64EncodedSignature, + sender, + serverId, + } = messageData; + + this.base64EncodedData = base64EncodedData; + this.sentTimestamp = sentTimestamp; + this.base64EncodedSignature = base64EncodedSignature; + this.sender = sender; + this.serverId = serverId; + } + + public async sign() { + const ourKeyPair = await UserUtils.getUserED25519KeyPair(); + if (!ourKeyPair) { + window.log.warn("Couldn't find user X25519 key pair."); + return null; + } + const data = fromBase64ToArray(this.base64EncodedData); + const sodium = await getSodium(); + const signature = sodium.crypto_sign_detached( + data, + fromHexToArray(ourKeyPair.privKey) + ); + if (!signature || signature.length === 0) { + throw new Error("Couldn't sign message"); + } + return new OpenGroupMessageV2({ + base64EncodedData: this.base64EncodedData, + sentTimestamp: this.sentTimestamp, + base64EncodedSignature: toHex(signature), + sender: this.sender, + serverId: this.serverId, + }); + } + + public toJson() { + const json = { + data: this.base64EncodedData, + timestamp: this.sentTimestamp, + } as Record; + if (this.serverId) { + json.server_id = this.serverId; + } + if (this.sender) { + json.public_key = this.sender; + } + if (this.base64EncodedSignature) { + json.signature = this.base64EncodedSignature; + } + } + + public fromJson(json: Record) { + const { + data: base64EncodedData, + timestamp: sentTimestamp, + server_id: serverId, + public_key: sender, + signature: base64EncodedSignature, + } = json; + + if (!base64EncodedData || !sentTimestamp) { + window.log.info('invalid json to build OpenGroupMessageV2'); + return null; + } + return new OpenGroupMessageV2({ + base64EncodedData, + base64EncodedSignature, + sentTimestamp, + serverId, + sender, + }); + } +} diff --git a/ts/opengroup/opengroupV2/OpenGroupPollerV2.ts b/ts/opengroup/opengroupV2/OpenGroupPollerV2.ts index 68f92e45b..d5bd62df1 100644 --- a/ts/opengroup/opengroupV2/OpenGroupPollerV2.ts +++ b/ts/opengroup/opengroupV2/OpenGroupPollerV2.ts @@ -2,20 +2,16 @@ import { AbortController } from 'abort-controller'; import { OpenGroupV2Room } from '../../data/opengroups'; export class OpenGroupPollerV2 { + private static readonly pollForEverythingInterval = 4 * 1000; + private readonly openGroupRoom: OpenGroupV2Room; - private pollForNewMessagesTimer?: NodeJS.Timeout; - private pollForDeletedMessagesTimer?: NodeJS.Timeout; - private pollForModeratorsTimer?: NodeJS.Timeout; + private pollForEverythingTimer?: NodeJS.Timeout; private abortController?: AbortController; private hasStarted = false; - private isPollingForMessages = false; - - private readonly pollForNewMessagesInterval = 4 * 1000; - private readonly pollForDeletedMessagesInterval = 30 * 1000; - private readonly pollForModeratorsInterval = 10 * 60 * 1000; + private isPolling = false; constructor(openGroupRoom: OpenGroupV2Room) { this.openGroupRoom = openGroupRoom; @@ -27,56 +23,30 @@ export class OpenGroupPollerV2 { } this.hasStarted = true; - this.pollForNewMessagesTimer = global.setInterval( - this.pollForNewMessages, - this.pollForNewMessagesInterval - ); - - this.pollForDeletedMessagesTimer = global.setInterval( - this.pollForDeletedMessages, - this.pollForDeletedMessagesInterval - ); - - this.pollForModeratorsTimer = global.setInterval( - this.pollForModerators, - this.pollForModeratorsInterval + this.abortController = new AbortController(); + this.pollForEverythingTimer = global.setInterval( + this.compactPoll, + OpenGroupPollerV2.pollForEverythingInterval ); } public stop() { - if (this.pollForNewMessagesTimer) { - global.clearInterval(this.pollForNewMessagesTimer); - this.pollForNewMessagesTimer = undefined; - } - if (this.pollForDeletedMessagesTimer) { - global.clearInterval(this.pollForDeletedMessagesTimer); - this.pollForDeletedMessagesTimer = undefined; - } - if (this.pollForModeratorsTimer) { - global.clearInterval(this.pollForModeratorsTimer); - this.pollForModeratorsTimer = undefined; + if (this.pollForEverythingTimer) { + global.clearInterval(this.pollForEverythingTimer); + this.abortController?.abort(); + this.abortController = undefined; + this.pollForEverythingTimer = undefined; } } - private async pollForNewMessages() { + private async compactPoll() { // return early if a poll is already in progress - if (this.isPollingForMessages) { + if (this.isPolling) { return; } - this.isPollingForMessages = true; + this.isPolling = true; window.log.warn('pollForNewMessages TODO'); // use abortController and do not trigger new messages if it was canceled - this.isPollingForMessages = false; - } - - // tslint:disable: no-async-without-await - private async pollForModerators() { - window.log.warn('pollForModerators TODO'); - // use abortController - } - - private async pollForDeletedMessages() { - window.log.warn('pollForDeletedMessages TODO'); - // use abortController + this.isPolling = false; } } diff --git a/ts/opengroup/utils/OpenGroupUtils.ts b/ts/opengroup/utils/OpenGroupUtils.ts index 3cf9963dc..6b596f342 100644 --- a/ts/opengroup/utils/OpenGroupUtils.ts +++ b/ts/opengroup/utils/OpenGroupUtils.ts @@ -102,14 +102,14 @@ export function prefixify(server: string, hasSSL: boolean = true): string { /** * No sql access. Just how our open groupv2 url looks like - * @returns `publicChat:${roomId}@${serverURL}` + * @returns `publicChat:${roomId}@${serverUrl}` */ export function getOpenGroupV2ConversationId( - serverURL: string, + serverUrl: string, roomId: string ) { if (roomId.length < 2) { throw new Error('Invalid roomId: too short'); } - return `publicChat:${roomId}@${serverURL}`; + return `publicChat:${roomId}@${serverUrl}`; } diff --git a/ts/session/constants.ts b/ts/session/constants.ts index b87c0e175..7f8b82be3 100644 --- a/ts/session/constants.ts +++ b/ts/session/constants.ts @@ -8,6 +8,12 @@ export const TTL_DEFAULT = { CONFIGURATION_MESSAGE: 4 * DAYS, }; +export const PROTOCOLS = { + // tslint:disable-next-line: no-http-string + HTTP: 'http:', + HTTPS: 'https:', +}; + // User Interface export const CONVERSATION = { DEFAULT_MEDIA_FETCH_COUNT: 50, diff --git a/ts/session/conversations/index.ts b/ts/session/conversations/index.ts index 26ca8a03a..9755df781 100644 --- a/ts/session/conversations/index.ts +++ b/ts/session/conversations/index.ts @@ -17,7 +17,7 @@ import { actions as conversationActions } from '../../state/ducks/conversations' export class ConversationController { private static instance: ConversationController | null; - private readonly conversations: any; + private readonly conversations: ConversationCollection; private _initialFetchComplete: boolean = false; private _initialPromise?: Promise; @@ -159,7 +159,7 @@ export class ConversationController { public isMediumGroup(hexEncodedGroupPublicKey: string): boolean { const convo = this.conversations.get(hexEncodedGroupPublicKey); if (convo) { - return convo.isMediumGroup(); + return !!convo.isMediumGroup(); } return false; } @@ -215,13 +215,15 @@ export class ConversationController { // Close group leaving if (conversation.isClosedGroup()) { await conversation.leaveGroup(); - } else if (conversation.isPublic()) { + } else if (conversation.isPublic() && !conversation.isOpenGroupV2()) { const channelAPI = await conversation.getPublicSendData(); if (channelAPI === null) { window.log.warn(`Could not get API for public conversation ${id}`); } else { - channelAPI.serverAPI.partChannel(channelAPI.channelId); + channelAPI.serverAPI.partChannel((channelAPI as any).channelId); } + } else if (conversation.isOpenGroupV2()) { + window.log.warn('leave open group v2 todo'); } await conversation.destroyMessages(); diff --git a/ts/session/onions/index.ts b/ts/session/onions/index.ts index 2dfc4a1cc..73542d0a5 100644 --- a/ts/session/onions/index.ts +++ b/ts/session/onions/index.ts @@ -1,10 +1,10 @@ -import { allowOnlyOneAtATime } from '../../../js/modules/loki_primitives'; import { getGuardNodes } from '../../../ts/data/data'; import * as SnodePool from '../snode_api/snodePool'; import _ from 'lodash'; import { default as insecureNodeFetch } from 'node-fetch'; import { UserUtils } from '../utils'; import { snodeHttpsAgent } from '../snode_api/onions'; +import { allowOnlyOneAtATime } from '../utils/Promise'; export type Snode = SnodePool.Snode; diff --git a/ts/session/onions/onionSend.ts b/ts/session/onions/onionSend.ts index d00f8e8f2..addf0728a 100644 --- a/ts/session/onions/onionSend.ts +++ b/ts/session/onions/onionSend.ts @@ -25,7 +25,7 @@ const MAX_SEND_ONION_RETRIES = 3; type OnionFetchOptions = { method: string; body?: string; - headers?: Object; + headers?: Record; }; type OnionFetchBasicOptions = { @@ -55,13 +55,14 @@ export const sendViaOnion = async ( options.requestNumber = OnionPaths.getInstance().assignOnionRequestNumber(); } + let tempHeaders = fetchOptions.headers || {}; const payloadObj = { method: fetchOptions.method || 'GET', body: fetchOptions.body || ('' as any), // safety issue with file server, just safer to have this - headers: fetchOptions.headers || {}, // no initial / endpoint: url.pathname.replace(/^\//, ''), + headers: {}, }; if (url.search) { payloadObj.endpoint += url.search; @@ -75,8 +76,8 @@ export const sendViaOnion = async ( ) { const fData = payloadObj.body.getBuffer(); const fHeaders = payloadObj.body.getHeaders(); + tempHeaders = { ...tempHeaders, fHeaders }; // update headers for boundary - payloadObj.headers = { ...payloadObj.headers, ...fHeaders }; // update body with base64 chunk payloadObj.body = { fileUpload: fData.toString('base64'), @@ -108,6 +109,8 @@ export const sendViaOnion = async ( // protocol: url.protocol, // port: url.port, }; + payloadObj.headers = tempHeaders; + console.warn('sendViaOnion payloadObj ==> ', payloadObj); result = await sendOnionRequestLsrpcDest( 0, diff --git a/ts/session/snode_api/onions.ts b/ts/session/snode_api/onions.ts index 76293dc7e..36e7050be 100644 --- a/ts/session/snode_api/onions.ts +++ b/ts/session/snode_api/onions.ts @@ -250,10 +250,9 @@ async function makeOnionRequest( // http errors and attempting to decrypt the body with `sharedKey` // May return false BAD_PATH, indicating that we should try a new path. const processOnionResponse = async ( - reqIdx: any, + reqIdx: number, response: any, - sharedKey: any, - useAesGcm: boolean, + sharedKey: ArrayBuffer, debug: boolean ): Promise => { const { log, libloki, dcodeIO, StringView } = window; @@ -318,21 +317,14 @@ const processOnionResponse = async ( if (debug) { log.debug( `(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertextBuffer`, - StringView.arrayBufferToHex(ciphertextBuffer), - 'useAesGcm', - useAesGcm + StringView.arrayBufferToHex(ciphertextBuffer) ); } - window.log.warn( - 'attempting decrypt with', - StringView.arrayBufferToHex(sharedKey) - ); - const decryptFn = useAesGcm - ? libloki.crypto.DecryptGCM - : libloki.crypto.DHDecrypt; - - const plaintextBuffer = await decryptFn(sharedKey, ciphertextBuffer, debug); + const plaintextBuffer = await libloki.crypto.DecryptAESGCM( + sharedKey, + ciphertextBuffer + ); if (debug) { log.debug( 'lokiRpc::processOnionResponse - plaintextBuffer', @@ -401,19 +393,32 @@ export type DestinationContext = { export type FinalDestinationOptions = { destination_ed25519_hex?: string; - headers?: string; + headers?: Record; body?: string; }; -// finalDestOptions is an object -// FIXME: internally track reqIdx, not externally +/** + * + * Onion request looks like this + * Sender -> 1 -> 2 -> 3 -> Receiver + * 1, 2, 3 = onion Snodes + * + * + * @param reqIdx + * @param nodePath the onion path to use to send the request + * @param destX25519Any + * @param finalDestOptions those are the options for the request from 3 to R. It contains for instance the payload and headers. + * @param finalRelayOptions those are the options 3 will use to make a request to R. It contains for instance the host to make the request to + * @param lsrpcIdx + * @returns + */ const sendOnionRequest = async ( - reqIdx: any, + reqIdx: number, nodePath: Array, destX25519Any: string, finalDestOptions: { destination_ed25519_hex?: string; - headers?: string; + headers?: Record; body?: string; }, finalRelayOptions?: FinalRelayOptions, @@ -449,7 +454,7 @@ const sendOnionRequest = async ( const options = finalDestOptions; // lint // do we need this? if (options.headers === undefined) { - options.headers = ''; + options.headers = {}; } const useV2 = window.lokiFeatureFlags.useOnionRequestsV2; @@ -512,13 +517,7 @@ const sendOnionRequest = async ( const response = await insecureNodeFetch(guardUrl, guardFetchOptions); - return processOnionResponse( - reqIdx, - response, - destCtx.symmetricKey, - true, - false - ); + return processOnionResponse(reqIdx, response, destCtx.symmetricKey, false); }; async function sendOnionRequestSnodeDest( @@ -543,11 +542,11 @@ async function sendOnionRequestSnodeDest( // need relay node's pubkey_x25519_hex // always the same target: /loki/v1/lsrpc export async function sendOnionRequestLsrpcDest( - reqIdx: any, + reqIdx: number, nodePath: Array, - destX25519Any: any, + destX25519Any: string, finalRelayOptions: FinalRelayOptions, - payloadObj: any, + payloadObj: FinalDestinationOptions, lsrpcIdx: number ): Promise { return sendOnionRequest( diff --git a/ts/session/snode_api/serviceNodeAPI.ts b/ts/session/snode_api/serviceNodeAPI.ts index 7a5629cd9..edb05bc66 100644 --- a/ts/session/snode_api/serviceNodeAPI.ts +++ b/ts/session/snode_api/serviceNodeAPI.ts @@ -27,6 +27,7 @@ import { Snode, updateSnodesFor, } from './snodePool'; +import { Constants } from '..'; /** * Currently unused. If we need it again, be sure to update it to onion routing rather @@ -94,10 +95,13 @@ const sha256 = (s: string) => { .digest('base64'); }; -const getSslAgentForSeedNode = (seedNodeHost: string) => { +const getSslAgentForSeedNode = (seedNodeHost: string, isSsl = false) => { let filePrefix = ''; let pubkey256 = ''; let cert256 = ''; + if (!isSsl) { + return undefined; + } switch (seedNodeHost) { case 'storage.seed1.loki.network': @@ -199,8 +203,11 @@ export async function getSnodesFromSeedUrl(urlObj: URL): Promise> { method: 'get_n_service_nodes', params, }; - //FIXME audric - const sslAgent = undefined; //getSslAgentForSeedNode(urlObj.hostname); + + const sslAgent = getSslAgentForSeedNode( + urlObj.hostname, + urlObj.protocol !== Constants.PROTOCOLS.HTTP + ); const fetchOptions = { method: 'POST', diff --git a/ts/session/snode_api/snodePool.ts b/ts/session/snode_api/snodePool.ts index d4bec192b..df9722966 100644 --- a/ts/session/snode_api/snodePool.ts +++ b/ts/session/snode_api/snodePool.ts @@ -1,10 +1,7 @@ import semver from 'semver'; import _ from 'lodash'; -import { - abortableIterator, - allowOnlyOneAtATime, -} from '../../../js/modules/loki_primitives'; +import { abortableIterator } from '../../../js/modules/loki_primitives'; import { getSnodesFromSeedUrl, requestSnodesForPubkey } from './serviceNodeAPI'; @@ -14,6 +11,7 @@ import { } from '../../../ts/data/data'; export type SnodeEdKey = string; +import { allowOnlyOneAtATime } from '../utils/Promise'; const MIN_NODES = 3; diff --git a/ts/session/utils/Promise.ts b/ts/session/utils/Promise.ts index deefd8796..00b598c52 100644 --- a/ts/session/utils/Promise.ts +++ b/ts/session/utils/Promise.ts @@ -13,6 +13,71 @@ export class TaskTimedOutError extends Error { } } +// one action resolves all +const snodeGlobalLocks: any = {}; +export async function allowOnlyOneAtATime( + name: string, + process: any, + timeoutMs?: number +) { + // if currently not in progress + if (snodeGlobalLocks[name] === undefined) { + // set lock + snodeGlobalLocks[name] = new Promise(async (resolve, reject) => { + // set up timeout feature + let timeoutTimer = null; + if (timeoutMs) { + timeoutTimer = setTimeout(() => { + window.log.warn( + `loki_primitives:::allowOnlyOneAtATime - TIMEDOUT after ${timeoutMs}s` + ); + // tslint:disable-next-line: no-dynamic-delete + delete snodeGlobalLocks[name]; // clear lock + reject(); + }, timeoutMs); + } + // do actual work + let innerRetVal; + try { + innerRetVal = await process(); + } catch (e) { + if (typeof e === 'string') { + window.log.error( + `loki_primitives:::allowOnlyOneAtATime - error ${e}` + ); + } else { + window.log.error( + `loki_primitives:::allowOnlyOneAtATime - error ${e.code} ${e.message}` + ); + } + + // clear timeout timer + if (timeoutMs) { + if (timeoutTimer !== null) { + clearTimeout(timeoutTimer); + timeoutTimer = null; + } + } + // tslint:disable-next-line: no-dynamic-delete + delete snodeGlobalLocks[name]; // clear lock + throw e; + } + // clear timeout timer + if (timeoutMs) { + if (timeoutTimer !== null) { + clearTimeout(timeoutTimer); + timeoutTimer = null; + } + } + // tslint:disable-next-line: no-dynamic-delete + delete snodeGlobalLocks[name]; // clear lock + // release the kraken + resolve(innerRetVal); + }); + } + return snodeGlobalLocks[name]; +} + /** * Create a promise which waits until `done` is called or until `timeout` period is reached. * If `timeout` is reached then this will throw an Error. diff --git a/ts/test/session/integration/common.ts b/ts/test/session/integration/common.ts index 0bb5e1d23..210b7982a 100644 --- a/ts/test/session/integration/common.ts +++ b/ts/test/session/integration/common.ts @@ -18,7 +18,6 @@ chai.should(); chai.use(chaiAsPromised as any); chai.config.includeStack = true; -// FIXME audric // From https://github.com/chaijs/chai/issues/200 chai.use((_chai, _) => { _chai.Assertion.addMethod('withMessage', (msg: string) => { @@ -148,9 +147,6 @@ export class Common { )}`, ], }); - // FIXME audric - // chaiAsPromised.transferPromiseness = app1.transferPromiseness; - await app1.start(); await app1.client.waitUntilWindowLoaded(); diff --git a/ts/test/session/unit/utils/Messages_test.ts b/ts/test/session/unit/utils/Messages_test.ts index 4fcea4294..246cd25b7 100644 --- a/ts/test/session/unit/utils/Messages_test.ts +++ b/ts/test/session/unit/utils/Messages_test.ts @@ -9,7 +9,10 @@ import { ClosedGroupVisibleMessage } from '../../../../session/messages/outgoing import { MockConversation } from '../../../test-utils/utils'; import { ConfigurationMessage } from '../../../../session/messages/outgoing/controlMessage/ConfigurationMessage'; -import { ConversationModel } from '../../../../models/conversation'; +import { + ConversationModel, + ConversationType, +} from '../../../../models/conversation'; import chaiAsPromised from 'chai-as-promised'; chai.use(chaiAsPromised as any); @@ -227,17 +230,17 @@ describe('Message Utils', () => { let convos: Array; const mockValidOpenGroup = new MockConversation({ - type: 'public', + type: ConversationType.OPEN_GROUP, id: 'publicChat:1@chat-dev.lokinet.org', }); const mockValidOpenGroup2 = new MockConversation({ - type: 'public', + type: ConversationType.OPEN_GROUP, id: 'publicChat:1@chat-dev2.lokinet.org', }); const mockValidClosedGroup = new MockConversation({ - type: 'group', + type: ConversationType.OPEN_GROUP, }); const mockValidPrivate = {