diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index 22208663d..8c7de6553 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -113,6 +113,11 @@ class LokiMessageAPI { // eslint-disable-next-line more/no-then snode = await primitives.firstTrue(promises); } catch (e) { + log.warn( + `loki_message:::sendMessage - ${e.code} ${e.message} to ${pubKey} via ${ + snode.ip + }:${snode.port}` + ); if (e instanceof textsecure.WrongDifficultyError) { // Force nonce recalculation // NOTE: Currently if there are snodes with conflicting difficulties we @@ -194,6 +199,9 @@ class LokiMessageAPI { '/storage_rpc/v1', targetNode ); + // succcessful messages should look like + // `{\"difficulty\":1}` + // but so does invalid pow, so be careful! // do not return true if we get false here... if (result === false) { diff --git a/js/modules/loki_primitives.js b/js/modules/loki_primitives.js index 48c1c0568..ca413af00 100644 --- a/js/modules/loki_primitives.js +++ b/js/modules/loki_primitives.js @@ -109,7 +109,6 @@ function abortableIterator(array, iterator) { start: async serially => { let item = destructableList.pop(); while (item && !abortIteration) { - // console.log('iterating on item', item); if (serially) { try { // eslint-disable-next-line no-await-in-loop diff --git a/js/modules/loki_rpc.js b/js/modules/loki_rpc.js index 561e01db3..0734428bd 100644 --- a/js/modules/loki_rpc.js +++ b/js/modules/loki_rpc.js @@ -78,20 +78,30 @@ const BAD_PATH = 'bad_path'; // May return false BAD_PATH, indicating that we should try a new const sendOnionRequest = async (reqIdx, nodePath, targetNode, plaintext) => { - log.debug('Sending an onion request'); - - const ctx1 = await encryptForDestination(targetNode, plaintext); - const ctx2 = await encryptForRelay(nodePath[2], targetNode, ctx1); - const ctx3 = await encryptForRelay(nodePath[1], nodePath[2], ctx2); - const ctx4 = await encryptForRelay(nodePath[0], nodePath[1], ctx3); + const ctxes = [await encryptForDestination(targetNode, plaintext)]; + // from (3) 2 to 0 + const firstPos = nodePath.length - 1; + + for (let i = firstPos; i > -1; i -= 1) { + // this nodePath points to the previous (i + 1) context + ctxes.push( + // eslint-disable-next-line no-await-in-loop + await encryptForRelay( + nodePath[i], + i === firstPos ? targetNode : nodePath[i + 1], + ctxes[ctxes.length - 1] + ) + ); + } + const guardCtx = ctxes[ctxes.length - 1]; // last ctx - const ciphertextBase64 = dcodeIO.ByteBuffer.wrap(ctx4.ciphertext).toString( - 'base64' - ); + const ciphertextBase64 = dcodeIO.ByteBuffer.wrap( + guardCtx.ciphertext + ).toString('base64'); const payload = { ciphertext: ciphertextBase64, - ephemeral_key: StringView.arrayBufferToHex(ctx4.ephemeral_key), + ephemeral_key: StringView.arrayBufferToHex(guardCtx.ephemeral_key), }; const fetchOptions = { @@ -106,13 +116,13 @@ const sendOnionRequest = async (reqIdx, nodePath, targetNode, plaintext) => { const response = await nodeFetch(url, fetchOptions); process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1'; - return processOnionResponse(reqIdx, response, ctx1.symmetricKey, true); + return processOnionResponse(reqIdx, response, ctxes[0].symmetricKey, true); }; // Process a response as it arrives from `nodeFetch`, handling // http errors and attempting to decrypt the body with `sharedKey` const processOnionResponse = async (reqIdx, response, sharedKey, useAesGcm) => { - log.debug(`(${reqIdx}) [path] processing onion response`); + // FIXME: 401/500 handling? // detect SNode is not ready (not in swarm; not done syncing) if (response.status === 503) { @@ -464,6 +474,31 @@ const lokiFetch = async (url, options = {}, targetNode = null) => { method, }; + async function checkResponse(response, type) { + // Wrong swarm + if (response.status === 421) { + const result = await response.json(); + log.warn( + `lokirpc:::lokiFetch ${type} - wrong swarm, now looking at snodes`, + result.snode + ); + const newSwarm = result.snodes ? result.snodes : []; + throw new textsecure.WrongSwarmError(newSwarm); + } + + // Wrong PoW difficulty + if (response.status === 432) { + const result = await response.json(); + throw new textsecure.WrongDifficultyError(result.difficulty); + } + + if (response.status === 406) { + throw new textsecure.TimestampError( + 'Invalid Timestamp (check your clock)' + ); + } + } + try { // Absence of targetNode indicates that we want a direct connection // (e.g. to connect to a seed node for the first time) @@ -477,14 +512,6 @@ const lokiFetch = async (url, options = {}, targetNode = null) => { const thisIdx = onionReqIdx; onionReqIdx += 1; - log.debug( - `(${thisIdx}) using path ${path[0].ip}:${path[0].port} -> ${ - path[1].ip - }:${path[1].port} -> ${path[2].ip}:${path[2].port} => ${ - targetNode.ip - }:${targetNode.port}` - ); - // eslint-disable-next-line no-await-in-loop const result = await sendOnionRequest( thisIdx, @@ -493,12 +520,36 @@ const lokiFetch = async (url, options = {}, targetNode = null) => { fetchOptions.body ); + const getPathString = pathObjArr => + pathObjArr.map(node => `${node.ip}:${node.port}`).join(', '); + if (result === BAD_PATH) { - log.error('[path] Error on the path'); + log.error( + `[path] Error on the path: ${getPathString(path)} to ${ + targetNode.ip + }:${targetNode.port}` + ); lokiSnodeAPI.markPathAsBad(path); + return false; + } else if (result) { + // not bad_path + // will throw if there's a problem + // eslint-disable-next-line no-await-in-loop + await checkResponse(result, 'onion'); } else { - return result ? result.json() : false; + // not truish and not bad_path + // false could mean, fail to parse results + // or status code wasn't 200 + // or can't decrypt + // it's not a bad_path, so we don't need to mark the path as bad + log.error( + `[path] sendOnionRequest gave false for path: ${getPathString( + path + )} to ${targetNode.ip}:${targetNode.port}` + ); } + + return result ? result.json() : false; } } @@ -526,9 +577,15 @@ const lokiFetch = async (url, options = {}, targetNode = null) => { */ // pass the false value up return false; - } + } else if (result) { + // will throw if there's a problem + await checkResponse(result, 'proxy'); + } // result is not truish and not explicitly false + // if not result, maybe we should throw?? - return result ? result.json() : {}; + // [] would make _retrieveNextMessages return undefined + // which would break messages.length + return result ? result.json() : false; } if (url.match(/https:\/\//)) { @@ -542,31 +599,14 @@ const lokiFetch = async (url, options = {}, targetNode = null) => { // restore TLS checking process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1'; - let result; - // Wrong swarm - if (response.status === 421) { - result = await response.json(); - const newSwarm = result.snodes ? result.snodes : []; - throw new textsecure.WrongSwarmError(newSwarm); - } - - // Wrong PoW difficulty - if (response.status === 432) { - result = await response.json(); - const { difficulty } = result; - throw new textsecure.WrongDifficultyError(difficulty); - } - - if (response.status === 406) { - throw new textsecure.TimestampError( - 'Invalid Timestamp (check your clock)' - ); - } + // will throw if there's a problem + await checkResponse(response, 'direct'); if (!response.ok) { throw new textsecure.HTTPError('Loki_rpc error', response); } + let result; if (response.headers.get('Content-Type') === 'application/json') { result = await response.json(); } else if (options.responseType === 'arraybuffer') { diff --git a/js/modules/loki_snode_api.js b/js/modules/loki_snode_api.js index dfb9d94fc..7d120ff4e 100644 --- a/js/modules/loki_snode_api.js +++ b/js/modules/loki_snode_api.js @@ -348,19 +348,30 @@ class LokiSnodeAPI { const guards = _.shuffle(this.guardNodes); // Create path for every guard node: - - // Each path needs 2 nodes in addition to the guard node: - const maxPath = Math.floor(Math.min(guards.length, otherNodes.length / 2)); + const nodesNeededPerPaths = window.lokiFeatureFlags.onionRequestHops - 1; + + // Each path needs X (nodesNeededPerPaths) nodes in addition to the guard node: + const maxPath = Math.floor( + Math.min( + guards.length, + nodesNeededPerPaths + ? otherNodes.length / nodesNeededPerPaths + : otherNodes.length + ) + ); // TODO: might want to keep some of the existing paths this.onionPaths = []; for (let i = 0; i < maxPath; i += 1) { - const path = [guards[i], otherNodes[i * 2], otherNodes[i * 2 + 1]]; + const path = [guards[i]]; + for (let j = 0; j < nodesNeededPerPaths; j += 1) { + path.push(otherNodes[i * nodesNeededPerPaths + j]); + } this.onionPaths.push({ path, bad: false }); } - log.info('Built onion paths: ', this.onionPaths); + log.info(`Built ${this.onionPaths.length} onion paths`, this.onionPaths); } async getRandomSnodeAddress() { diff --git a/preload.js b/preload.js index 4d513718c..fb435a44b 100644 --- a/preload.js +++ b/preload.js @@ -414,7 +414,8 @@ window.lokiFeatureFlags = { privateGroupChats: true, useSnodeProxy: !process.env.USE_STUBBED_NETWORK, useSealedSender: true, - useOnionRequests: false, + useOnionRequests: true, + onionRequestHops: 1, }; // eslint-disable-next-line no-extend-native,func-names @@ -440,14 +441,7 @@ if ( }; /* eslint-enable global-require, import/no-extraneous-dependencies */ window.lokiFeatureFlags = {}; - window.lokiSnodeAPI = { - refreshSwarmNodesForPubKey: () => [], - getFreshSwarmNodes: () => [], - updateSwarmNodes: () => {}, - updateLastHash: () => {}, - getSwarmNodesForPubKey: () => [], - buildNewOnionPaths: () => [], - }; + window.lokiSnodeAPI = {}; // no need stub out each function here } if (config.environment.includes('test-integration')) { window.lokiFeatureFlags = {