You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			208 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			JavaScript
		
	
			
		
		
	
	
			208 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			JavaScript
		
	
| /* 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,
 | |
|       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;
 |