From c83661ce3f1b4d3e63bc47ea6e3385f2624a134f Mon Sep 17 00:00:00 2001 From: Beaudan Date: Mon, 21 Jan 2019 17:34:27 +1100 Subject: [PATCH] Added DNS resolution error for when lokinet isn't working, now keeping track of the nodes that have been queried and not trying them again --- js/modules/loki_message_api.js | 75 +++++++++++++++++++++++++++------- js/modules/loki_snode_api.js | 5 ++- libtextsecure/errors.js | 11 +++++ 3 files changed, 75 insertions(+), 16 deletions(-) diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index 55a674894..358dd33c0 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -5,6 +5,7 @@ const fetch = require('node-fetch'); // Will be raised (to 3?) when we get more nodes const MINIMUM_SUCCESSFUL_REQUESTS = 2; + class LokiMessageAPI { constructor({ messageServerPort }) { this.messageServerPort = messageServerPort ? `:${messageServerPort}` : ''; @@ -33,7 +34,8 @@ class LokiMessageAPI { // Something went horribly wrong throw err; } - let completedRequests = 0; + const completedNodes = []; + let canResolve = true; const doRequest = async nodeUrl => { // TODO: Confirm sensible timeout @@ -60,8 +62,15 @@ class LokiMessageAPI { try { response = await fetch(options.url, fetchOptions); } catch (e) { + if (e.code === 'ENOTFOUND') { + // TODO: Handle the case where lokinet is not working + canResolve = false; + return; + } log.error(options.type, options.url, 0, 'Error sending message'); - window.LokiSnodeAPI.unreachableNode(pubKey, nodeUrl); + if (window.LokiSnodeAPI.unreachableNode(pubKey, nodeUrl)) { + completedNodes.push(nodeUrl); + } return; } @@ -78,7 +87,7 @@ class LokiMessageAPI { } if (response.status >= 0 && response.status < 400) { - completedRequests += 1; + completedNodes.push(nodeUrl); return; } log.error( @@ -91,14 +100,28 @@ class LokiMessageAPI { }; let swarmNodes; - while (completedRequests < MINIMUM_SUCCESSFUL_REQUESTS) { + while (completedNodes.length < MINIMUM_SUCCESSFUL_REQUESTS) { + if (!canResolve) { + throw new window.textsecure.DNSResolutionError('Sending messages'); + } try { swarmNodes = await window.LokiSnodeAPI.getSwarmNodesByPubkey(pubKey); + // Filter out the nodes we have already got responses from + swarmNodes = Object.keys(swarmNodes) + .filter(node => !(node in completedNodes)) + .reduce( + // eslint-disable-next-line no-loop-func + (obj, node) => ({ + ...obj, + [node]: swarmNodes[node], + }), + {} + ); } catch (e) { throw new window.textsecure.EmptySwarmError(pubKey, e); } if (!swarmNodes || swarmNodes.size === 0) { - if (completedRequests !== 0) { + if (completedNodes.length !== 0) { // TODO: Decide how to handle some completed requests but not enough return; } @@ -108,7 +131,8 @@ class LokiMessageAPI { ); } - const remainingRequests = MINIMUM_SUCCESSFUL_REQUESTS - completedRequests; + const remainingRequests = + MINIMUM_SUCCESSFUL_REQUESTS - completedNodes.length; await Promise.all( Array.from(swarmNodes) .splice(0, remainingRequests) @@ -119,7 +143,8 @@ class LokiMessageAPI { async retrieveMessages(callback) { const ourKey = window.textsecure.storage.user.getNumber(); - let completedRequests = 0; + const completedNodes = []; + let canResolve = true; const doRequest = async (nodeUrl, nodeData) => { // TODO: Confirm sensible timeout @@ -147,6 +172,11 @@ class LokiMessageAPI { try { response = await fetch(options.url, fetchOptions); } catch (e) { + if (e.code === 'ENOTFOUND') { + // TODO: Handle the case where lokinet is not working + canResolve = false; + return; + } // TODO: Maybe we shouldn't immediately delete? // And differentiate between different connectivity issues log.error( @@ -155,7 +185,9 @@ class LokiMessageAPI { 0, `Error retrieving messages from ${nodeUrl}` ); - window.LokiSnodeAPI.unreachableNode(ourKey, nodeUrl); + if (window.LokiSnodeAPI.unreachableNode(ourKey, nodeUrl)) { + completedNodes.push(nodeUrl); + } return; } @@ -170,7 +202,7 @@ class LokiMessageAPI { } else { result = await response.text(); } - completedRequests += 1; + completedNodes.push(nodeUrl); if (response.status === 200) { if (result.lastHash) { @@ -183,28 +215,43 @@ class LokiMessageAPI { log.error(options.type, options.url, response.status, 'Error'); }; - while (completedRequests < MINIMUM_SUCCESSFUL_REQUESTS) { + while (completedNodes.length < MINIMUM_SUCCESSFUL_REQUESTS) { + if (!canResolve) { + throw new window.textsecure.DNSResolutionError('Retrieving messages'); + } let ourSwarmNodes; try { ourSwarmNodes = await window.LokiSnodeAPI.getOurSwarmNodes(); + // Filter out the nodes we have already got responses from + ourSwarmNodes = Object.keys(ourSwarmNodes) + .filter(node => !(node in completedNodes)) + .reduce( + // eslint-disable-next-line no-loop-func + (obj, node) => ({ + ...obj, + [node]: ourSwarmNodes[node], + }), + {} + ); } catch (e) { - throw window.textsecure.EmptySwarmError( + throw new window.textsecure.EmptySwarmError( window.textsecure.storage.user.getNumber(), e ); } if (!ourSwarmNodes || Object.keys(ourSwarmNodes).length === 0) { - if (completedRequests !== 0) { + if (completedNodes.length !== 0) { // TODO: Decide how to handle some completed requests but not enough return; } - throw window.textsecure.EmptySwarmError( + throw new window.textsecure.EmptySwarmError( window.textsecure.storage.user.getNumber(), new Error('Ran out of swarm nodes to query') ); } - const remainingRequests = MINIMUM_SUCCESSFUL_REQUESTS - completedRequests; + const remainingRequests = + MINIMUM_SUCCESSFUL_REQUESTS - completedNodes.length; await Promise.all( Object.entries(ourSwarmNodes) .splice(0, remainingRequests) diff --git a/js/modules/loki_snode_api.js b/js/modules/loki_snode_api.js index 3ec6554a8..7dd4cffa6 100644 --- a/js/modules/loki_snode_api.js +++ b/js/modules/loki_snode_api.js @@ -45,7 +45,7 @@ class LokiSnodeAPI { if (this.ourSwarmNodes[nodeUrl].failureCount >= FAILURE_THRESHOLD) { delete this.ourSwarmNodes[nodeUrl]; } - return; + return false; } if (!this.contactSwarmNodes[nodeUrl]) { this.contactSwarmNodes[nodeUrl] = { @@ -55,7 +55,7 @@ class LokiSnodeAPI { this.contactSwarmNodes[nodeUrl].failureCount += 1; } if (this.contactSwarmNodes[nodeUrl].failureCount < FAILURE_THRESHOLD) { - return; + return false; } const conversation = window.ConversationController.get(pubKey); const swarmNodes = conversation.get('swarmNodes'); @@ -70,6 +70,7 @@ class LokiSnodeAPI { ); delete this.contactSwarmNodes[nodeUrl]; } + return true; } updateLastHash(nodeUrl, hash) { diff --git a/libtextsecure/errors.js b/libtextsecure/errors.js index a621ebf6f..f76fb3795 100644 --- a/libtextsecure/errors.js +++ b/libtextsecure/errors.js @@ -157,6 +157,16 @@ } inherit(ReplayableError, PoWError); + function DNSResolutionError(message) { + // eslint-disable-next-line prefer-destructuring + + ReplayableError.call(this, { + name: 'DNSResolutionError', + message: `Error resolving url: ${message}`, + }); + } + inherit(ReplayableError, DNSResolutionError); + window.textsecure.UnregisteredUserError = UnregisteredUserError; window.textsecure.SendMessageNetworkError = SendMessageNetworkError; window.textsecure.IncomingIdentityKeyError = IncomingIdentityKeyError; @@ -167,4 +177,5 @@ window.textsecure.SignedPreKeyRotationError = SignedPreKeyRotationError; window.textsecure.PoWError = PoWError; window.textsecure.EmptySwarmError = EmptySwarmError; + window.textsecure.DNSResolutionError = DNSResolutionError; })();