/* eslint-disable class-methods-use-this */ /* global window, ConversationController, _ */ const is = require('@sindresorhus/is'); const dns = require('dns'); const process = require('process'); const { rpc } = require('./loki_rpc'); const natUpnp = require('nat-upnp'); const resolve4 = url => new Promise((resolve, reject) => { dns.resolve4(url, (err, ip) => { if (err) { reject(err); } else { resolve(ip); } }); }); const resolveCname = url => new Promise((resolve, reject) => { dns.resolveCname(url, (err, address) => { if (err) { reject(err); } else { resolve(address[0]); } }); }); class LokiSnodeAPI { constructor({ serverUrl, localUrl }) { if (!is.string(serverUrl)) { throw new Error('WebAPI.initialize: Invalid server url'); } this.serverUrl = serverUrl; this.localUrl = localUrl; this.randomSnodePool = []; this.swarmsPendingReplenish = {}; // When we package lokinet with messenger we can ensure this ip is correct if (process.platform === 'win32') { dns.setServers(['127.0.0.1']); } } async getMyClearIp() { const upnpClient = natUpnp.createClient(); return new Promise((resolve, reject) => { upnpClient.externalIp((err, ip) => { if (err) { reject(err); } else { resolve(ip); } }); }); } async getMyLokiIp() { try { const address = await resolveCname(this.localUrl); return resolve4(address); } catch (e) { throw new window.textsecure.LokiIpError( 'Failed to resolve localhost.loki', e ); } } getMyLokiAddress() { /* resolve our local loki address */ return resolveCname(this.localUrl); } async getRandomSnodeAddress() { /* resolve random snode */ if (this.randomSnodePool.length === 0) { await this.initialiseRandomPool(); } if (this.randomSnodePool.length === 0) { throw new window.textsecure.SeedNodeError('Invalid seed node response'); } return this.randomSnodePool[ Math.floor(Math.random() * this.randomSnodePool.length) ]; } async initialiseRandomPool(seedNodes = [...window.seedNodeList]) { const params = { limit: 20, active_only: true, fields: { public_ip: true, storage_port: true, }, }; const seedNode = seedNodes.splice( Math.floor(Math.random() * seedNodes.length), 1 )[0]; try { const result = await rpc( `http://${seedNode.ip}`, seedNode.port, 'get_n_service_nodes', params, {}, // Options '/json_rpc' // Seed request endpoint ); // Filter 0.0.0.0 nodes which haven't submitted uptime proofs const snodes = result.result.service_node_states.filter( snode => snode.public_ip !== '0.0.0.0' ); this.randomSnodePool = snodes.map(snode => ({ ip: snode.public_ip, port: snode.storage_port, })); } catch (e) { if (seedNodes.length === 0) { throw new window.textsecure.SeedNodeError( 'Failed to contact seed node' ); } this.initialiseRandomPool(seedNodes); } } async unreachableNode(pubKey, nodeUrl) { const conversation = ConversationController.get(pubKey); const swarmNodes = [...conversation.get('swarmNodes')]; const filteredNodes = swarmNodes.filter( node => node.address !== nodeUrl && node.ip !== nodeUrl ); await conversation.updateSwarmNodes(filteredNodes); } async updateLastHash(nodeUrl, lastHash, expiresAt) { await window.Signal.Data.updateLastHash({ nodeUrl, lastHash, expiresAt }); } getSwarmNodesForPubKey(pubKey) { try { const conversation = ConversationController.get(pubKey); const swarmNodes = [...conversation.get('swarmNodes')]; return swarmNodes; } catch (e) { throw new window.textsecure.ReplayableError({ message: 'Could not get conversation', }); } } async updateSwarmNodes(pubKey, newNodes) { try { const filteredNodes = newNodes.filter(snode => snode.ip !== '0.0.0.0'); const conversation = ConversationController.get(pubKey); await conversation.updateSwarmNodes(filteredNodes); } catch (e) { throw new window.textsecure.ReplayableError({ message: 'Could not get conversation', }); } } async refreshSwarmNodesForPubKey(pubKey) { const newNodes = await this.getFreshSwarmNodes(pubKey); this.updateSwarmNodes(pubKey, newNodes); } async getFreshSwarmNodes(pubKey) { if (!(pubKey in this.swarmsPendingReplenish)) { this.swarmsPendingReplenish[pubKey] = new Promise(async resolve => { let newSwarmNodes; try { newSwarmNodes = await this.getSwarmNodes(pubKey); } catch (e) { // TODO: Handle these errors sensibly newSwarmNodes = []; } resolve(newSwarmNodes); }); } const newSwarmNodes = await this.swarmsPendingReplenish[pubKey]; delete this.swarmsPendingReplenish[pubKey]; return newSwarmNodes; } async getSwarmNodes(pubKey) { // TODO: Hit multiple random nodes and merge lists? const { ip, port } = await this.getRandomSnodeAddress(); try { const result = await rpc(`https://${ip}`, port, 'get_snodes_for_pubkey', { pubKey, }); const snodes = result.snodes.filter(snode => snode.ip !== '0.0.0.0'); return snodes; } catch (e) { this.randomSnodePool = _.without( this.randomSnodePool, _.find(this.randomSnodePool, { ip }) ); return this.getSwarmNodes(pubKey); } } } module.exports = LokiSnodeAPI;