From 0bbd265d52fcd8b2828805929ef83dca2f8593d1 Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Sun, 19 Apr 2020 22:21:49 -0700 Subject: [PATCH 1/9] log any sendMessage exceptions, other notes --- js/modules/loki_message_api.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index 22208663d..7df26a324 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) { @@ -297,6 +305,7 @@ class LokiMessageAPI { // won't include parsing failures... successiveFailures = 0; if (messages.length) { + // log.debug(`loki_message:::_openRetrieveConnection - received ${messages.length} messages from ${nodeData.ip}:${nodeData.port}`) const lastMessage = _.last(messages); nodeData.lastHash = lastMessage.hash; await lokiSnodeAPI.updateLastHash( From a3f51648a547e618b7bb906e4a42c6be8487d460 Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Sun, 19 Apr 2020 22:22:07 -0700 Subject: [PATCH 2/9] dead code lint --- js/modules/loki_primitives.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/modules/loki_primitives.js b/js/modules/loki_primitives.js index 48c1c0568..ec651034a 100644 --- a/js/modules/loki_primitives.js +++ b/js/modules/loki_primitives.js @@ -109,7 +109,7 @@ function abortableIterator(array, iterator) { start: async serially => { let item = destructableList.pop(); while (item && !abortIteration) { - // console.log('iterating on item', item); + // log.debug('iterating on item', item); if (serially) { try { // eslint-disable-next-line no-await-in-loop From 203da4544a1a45df38cc52a45e4bd729e0c8bae3 Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Sun, 19 Apr 2020 22:23:51 -0700 Subject: [PATCH 3/9] add window.lokiFeatureFlags.onionRequestPaths and turn on onion_requests using one hop --- preload.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/preload.js b/preload.js index 4d513718c..8d4ccbea7 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, + onionRequestPaths: 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 = { From cc4664e27f8c7775cdff75f85a1acc004bc63f13 Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Sun, 19 Apr 2020 22:24:41 -0700 Subject: [PATCH 4/9] buildNewOnionPaths() now uses onionRequestPaths control, include count of paths in log --- js/modules/loki_snode_api.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/js/modules/loki_snode_api.js b/js/modules/loki_snode_api.js index dfb9d94fc..f82d0e293 100644 --- a/js/modules/loki_snode_api.js +++ b/js/modules/loki_snode_api.js @@ -348,19 +348,28 @@ 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 needPaths = window.lokiFeatureFlags.onionRequestPaths - 1; + + // Each path needs X (needPaths) nodes in addition to the guard node: + const maxPath = Math.floor( + Math.min( + guards.length, + needPaths ? otherNodes.length / needPaths : 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 < needPaths; j += 1) { + path.push(otherNodes[i * needPaths + 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() { From 5ef17375c1950d57e9df6f5eee26fd38932ce648 Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Sun, 19 Apr 2020 22:26:47 -0700 Subject: [PATCH 5/9] sendOnionRequest() now uses onionRequestPaths control, lokiFetch checkResponse() so we handle wrong PoW/Timestamp/Swarm in proxy and onion requests --- js/modules/loki_rpc.js | 135 ++++++++++++++++++++++++++++------------- 1 file changed, 92 insertions(+), 43 deletions(-) diff --git a/js/modules/loki_rpc.js b/js/modules/loki_rpc.js index 561e01db3..e866e8cd5 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, @@ -494,11 +521,41 @@ const lokiFetch = async (url, options = {}, targetNode = null) => { ); if (result === BAD_PATH) { - log.error('[path] Error on the path'); + const pathArr = []; + path.forEach(node => { + pathArr.push(`${node.ip}:${node.port}`); + }); + log.error( + `[path] Error on the path: ${pathArr.join(', ')} to ${ + targetNode.ip + }:${targetNode.port}` + ); lokiSnodeAPI.markPathAsBad(path); + return false; + } + + // result maybe false + if (result) { + // 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; + // 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 + const pathArr = []; + path.forEach(node => { + pathArr.push(`${node.ip}:${node.port}`); + }); + log.error( + `[path] sendOnionRequest gave false for path: ${pathArr.join( + ', ' + )} to ${targetNode.ip}:${targetNode.port}` + ); } + + return result ? result.json() : false; } } @@ -527,8 +584,17 @@ const lokiFetch = async (url, options = {}, targetNode = null) => { // pass the false value up return false; } + + // result maybe false + if (result) { + // will throw if there's a problem + await checkResponse(result, 'proxy'); + } + // 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 +608,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') { From cd8f67a93a10733f3ed578e40bfcf5497dbb007e Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Sun, 19 Apr 2020 22:47:35 -0700 Subject: [PATCH 6/9] refactor getPathString() out, clean up if result branching --- js/modules/loki_rpc.js | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/js/modules/loki_rpc.js b/js/modules/loki_rpc.js index e866e8cd5..001ae5c36 100644 --- a/js/modules/loki_rpc.js +++ b/js/modules/loki_rpc.js @@ -520,37 +520,36 @@ const lokiFetch = async (url, options = {}, targetNode = null) => { fetchOptions.body ); - if (result === BAD_PATH) { + const getPathString = pathObjArr => { const pathArr = []; - path.forEach(node => { + pathObjArr.forEach(node => { pathArr.push(`${node.ip}:${node.port}`); }); + return pathArr.join(', '); + }; + + if (result === BAD_PATH) { log.error( - `[path] Error on the path: ${pathArr.join(', ')} to ${ + `[path] Error on the path: ${getPathString(path)} to ${ targetNode.ip }:${targetNode.port}` ); lokiSnodeAPI.markPathAsBad(path); return false; - } - - // result maybe false - if (result) { + } 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 { + // 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 - const pathArr = []; - path.forEach(node => { - pathArr.push(`${node.ip}:${node.port}`); - }); log.error( - `[path] sendOnionRequest gave false for path: ${pathArr.join( - ', ' + `[path] sendOnionRequest gave false for path: ${getPathString( + path )} to ${targetNode.ip}:${targetNode.port}` ); } @@ -583,13 +582,10 @@ const lokiFetch = async (url, options = {}, targetNode = null) => { */ // pass the false value up return false; - } - - // result maybe false - if (result) { + } 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?? // [] would make _retrieveNextMessages return undefined From 2907f3d154a3f2d72a73c77f27f959638d8dfb30 Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Sun, 19 Apr 2020 23:02:29 -0700 Subject: [PATCH 7/9] remove dead code --- js/modules/loki_message_api.js | 1 - js/modules/loki_primitives.js | 1 - 2 files changed, 2 deletions(-) diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index 7df26a324..8c7de6553 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -305,7 +305,6 @@ class LokiMessageAPI { // won't include parsing failures... successiveFailures = 0; if (messages.length) { - // log.debug(`loki_message:::_openRetrieveConnection - received ${messages.length} messages from ${nodeData.ip}:${nodeData.port}`) const lastMessage = _.last(messages); nodeData.lastHash = lastMessage.hash; await lokiSnodeAPI.updateLastHash( diff --git a/js/modules/loki_primitives.js b/js/modules/loki_primitives.js index ec651034a..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) { - // log.debug('iterating on item', item); if (serially) { try { // eslint-disable-next-line no-await-in-loop From 06e1a0eec317684dccd70ad35e10215b78a2a1c5 Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Sun, 19 Apr 2020 23:03:34 -0700 Subject: [PATCH 8/9] window.lokiFeatureFlags.onionRequestPaths => onionRequestHops, needPaths => nodesNeededPerPaths --- js/modules/loki_snode_api.js | 12 +++++++----- preload.js | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/js/modules/loki_snode_api.js b/js/modules/loki_snode_api.js index f82d0e293..7d120ff4e 100644 --- a/js/modules/loki_snode_api.js +++ b/js/modules/loki_snode_api.js @@ -348,13 +348,15 @@ class LokiSnodeAPI { const guards = _.shuffle(this.guardNodes); // Create path for every guard node: - const needPaths = window.lokiFeatureFlags.onionRequestPaths - 1; + const nodesNeededPerPaths = window.lokiFeatureFlags.onionRequestHops - 1; - // Each path needs X (needPaths) nodes in addition to the guard node: + // Each path needs X (nodesNeededPerPaths) nodes in addition to the guard node: const maxPath = Math.floor( Math.min( guards.length, - needPaths ? otherNodes.length / needPaths : otherNodes.length + nodesNeededPerPaths + ? otherNodes.length / nodesNeededPerPaths + : otherNodes.length ) ); @@ -363,8 +365,8 @@ class LokiSnodeAPI { for (let i = 0; i < maxPath; i += 1) { const path = [guards[i]]; - for (let j = 0; j < needPaths; j += 1) { - path.push(otherNodes[i * needPaths + j]); + for (let j = 0; j < nodesNeededPerPaths; j += 1) { + path.push(otherNodes[i * nodesNeededPerPaths + j]); } this.onionPaths.push({ path, bad: false }); } diff --git a/preload.js b/preload.js index 8d4ccbea7..fb435a44b 100644 --- a/preload.js +++ b/preload.js @@ -415,7 +415,7 @@ window.lokiFeatureFlags = { useSnodeProxy: !process.env.USE_STUBBED_NETWORK, useSealedSender: true, useOnionRequests: true, - onionRequestPaths: 1, + onionRequestHops: 1, }; // eslint-disable-next-line no-extend-native,func-names From e38dff54484deccfe5971cf1468de3a5fc48208a Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Sun, 19 Apr 2020 23:03:53 -0700 Subject: [PATCH 9/9] make getPathString functional --- js/modules/loki_rpc.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/js/modules/loki_rpc.js b/js/modules/loki_rpc.js index 001ae5c36..0734428bd 100644 --- a/js/modules/loki_rpc.js +++ b/js/modules/loki_rpc.js @@ -520,13 +520,8 @@ const lokiFetch = async (url, options = {}, targetNode = null) => { fetchOptions.body ); - const getPathString = pathObjArr => { - const pathArr = []; - pathObjArr.forEach(node => { - pathArr.push(`${node.ip}:${node.port}`); - }); - return pathArr.join(', '); - }; + const getPathString = pathObjArr => + pathObjArr.map(node => `${node.ip}:${node.port}`).join(', '); if (result === BAD_PATH) { log.error(