mirror of
https://github.com/oxen-io/session-desktop.git
synced 2023-12-14 02:12:57 +01:00
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,
|
|
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;
|