From 02320334f0edb02486a81db4413d57253af130c8 Mon Sep 17 00:00:00 2001 From: Maxim Shishmarev Date: Tue, 28 Jan 2020 14:19:39 +1100 Subject: [PATCH] Remove P2P --- js/background.js | 30 --- js/models/conversations.js | 13 -- js/models/messages.js | 14 -- js/modules/loki_message_api.js | 44 +--- js/modules/loki_p2p_api.js | 122 ----------- js/modules/loki_snode_api.js | 59 ------ libloki/api.js | 46 +--- libloki/modules/local_loki_server.js | 219 -------------------- libloki/test/node/local_loki_server_test.js | 111 ---------- libloki/test/node/loki_p2p_api_test.js | 193 ----------------- libtextsecure/errors.js | 34 --- libtextsecure/message_receiver.js | 84 +------- package.json | 3 - preload.js | 18 -- test/_test.js | 1 - ts/components/conversation/Message.tsx | 9 +- yarn.lock | 37 +--- 17 files changed, 17 insertions(+), 1020 deletions(-) delete mode 100644 js/modules/loki_p2p_api.js delete mode 100644 libloki/modules/local_loki_server.js delete mode 100644 libloki/test/node/local_loki_server_test.js delete mode 100644 libloki/test/node/loki_p2p_api_test.js diff --git a/js/background.js b/js/background.js index 65d13da77..883501299 100644 --- a/js/background.js +++ b/js/background.js @@ -258,13 +258,6 @@ } // are there limits on tracking, is this unneeded? // window.mixpanel.track("Desktop boot"); - window.lokiP2pAPI = new window.LokiP2pAPI(ourKey); - window.lokiP2pAPI.on('pingContact', pubKey => { - const isPing = true; - libloki.api.sendOnlineBroadcastMessage(pubKey, isPing); - }); - window.lokiP2pAPI.on('online', ConversationController._handleOnline); - window.lokiP2pAPI.on('offline', ConversationController._handleOffline); window.initialisedAPI = true; if (storage.get('isSecondaryDevice')) { @@ -286,14 +279,6 @@ } } - function startLocalLokiServer() { - if (window.localLokiServer) { - return; - } - const pems = window.getSelfSignedCert(); - window.localLokiServer = new window.LocalLokiServer(pems); - } - // We need this 'first' check because we don't want to start the app up any other time // than the first time. And storage.fetch() will cause onready() to fire. let first = true; @@ -391,8 +376,6 @@ }, shutdown: async () => { - await window.localLokiServer.close(); - // Stop background processing window.Signal.AttachmentDownloads.stop(); if (idleDetector) { @@ -1262,15 +1245,6 @@ } }); - Whisper.events.on('p2pMessageSent', ({ pubKey, timestamp }) => { - try { - const conversation = ConversationController.get(pubKey); - conversation.onP2pMessageSent(pubKey, timestamp); - } catch (e) { - window.log.error('Error setting p2p on message'); - } - }); - Whisper.events.on( 'publicMessageSent', ({ pubKey, timestamp, serverId }) => { @@ -1418,7 +1392,6 @@ window.lokiFileServerAPI = await window.lokiFileServerAPIFactory.establishHomeConnection( window.getDefaultFileServer() ); - window.localLokiServer = null; window.lokiPublicChatAPI = null; window.feeds = []; messageReceiver = new textsecure.MessageReceiver( @@ -1436,8 +1409,6 @@ return; } - // initialize the socket and start listening for messages - startLocalLokiServer(); await initAPIs(); await initSpecialConversations(); messageReceiver = new textsecure.MessageReceiver( @@ -2054,7 +2025,6 @@ unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived, type: 'incoming', unread: 1, - isP2p: data.isP2p, isPublic: data.isPublic, isRss: data.isRss, }; diff --git a/js/models/conversations.js b/js/models/conversations.js index bc7bcde4a..17278914d 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -12,7 +12,6 @@ profileImages, clipboard, BlockedNumberController, - lokiP2pAPI, lokiPublicChatAPI, JobQueue */ @@ -182,13 +181,6 @@ if (this.id === this.ourNumber) { this.set({ friendRequestStatus: FriendRequestStatusEnum.friends }); - } else if (typeof lokiP2pAPI !== 'undefined') { - // Online status handling, only for contacts that aren't us - this.set({ isOnline: lokiP2pAPI.isOnline(this.id) }); - } else { - window.log.warn( - 'lokiP2pAPI not initialised when spawning conversation!' - ); } this.messageSendQueue = new JobQueue(); @@ -484,11 +476,6 @@ await Promise.all(messages.map(m => m.setCalculatingPoW())); }, - async onP2pMessageSent(pubKey, timestamp) { - const messages = this._getMessagesWithTimestamp(pubKey, timestamp); - await Promise.all(messages.map(m => m.setIsP2p(true))); - }, - async onPublicMessageSent(pubKey, timestamp, serverId) { const messages = this._getMessagesWithTimestamp(pubKey, timestamp); await Promise.all( diff --git a/js/models/messages.js b/js/models/messages.js index e1786bfda..80a8052c8 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -694,7 +694,6 @@ expirationTimestamp, selected: this.selected, multiSelectMode: conversation && conversation.selectedMessages.size > 0, - isP2p: !!this.get('isP2p'), isPublic: !!this.get('isPublic'), isRss: !!this.get('isRss'), senderIsModerator: @@ -1360,19 +1359,6 @@ Message: Whisper.Message, }); }, - async setIsP2p(isP2p) { - if (_.isEqual(this.get('isP2p'), isP2p)) { - return; - } - - this.set({ - isP2p: !!isP2p, - }); - - await window.Signal.Data.saveMessage(this.attributes, { - Message: Whisper.Message, - }); - }, getServerId() { return this.get('serverId'); }, diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index ce4d55b81..ca42bb468 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -1,6 +1,6 @@ /* eslint-disable no-await-in-loop */ /* eslint-disable no-loop-func */ -/* global log, dcodeIO, window, callWorker, lokiP2pAPI, lokiSnodeAPI, textsecure */ +/* global log, dcodeIO, window, callWorker, lokiSnodeAPI, textsecure */ const _ = require('lodash'); const { lokiRpc } = require('./loki_rpc'); @@ -38,38 +38,6 @@ const calcNonce = (messageEventData, pubKey, data64, timestamp, ttl) => { return callWorker('calcPoW', timestamp, ttl, pubKey, data64, difficulty); }; -const trySendP2p = async (pubKey, data64, isPing, messageEventData) => { - if (typeof lokiP2pAPI === 'undefined') { - return false; - } - const p2pDetails = lokiP2pAPI.getContactP2pDetails(pubKey); - if (!p2pDetails || (!isPing && !p2pDetails.isOnline)) { - return false; - } - try { - await lokiRpc(p2pDetails.address, p2pDetails.port, 'store', { - data: data64, - }); - lokiP2pAPI.setContactOnline(pubKey); - window.Whisper.events.trigger('p2pMessageSent', messageEventData); - if (isPing) { - log.info(`Successfully pinged ${pubKey}`); - } else { - log.info(`Successful p2p message to ${pubKey}`); - } - return true; - } catch (e) { - lokiP2pAPI.setContactOffline(pubKey); - if (isPing) { - // If this was just a ping, we don't bother sending to storage server - log.warn('Ping failed, contact marked offline', e); - return true; - } - log.warn('Failed to send P2P message, falling back to storage', e); - return false; - } -}; - class LokiMessageAPI { constructor(ourKey) { this.jobQueue = new window.JobQueue(); @@ -79,7 +47,6 @@ class LokiMessageAPI { async sendMessage(pubKey, data, messageTimeStamp, ttl, options = {}) { const { - isPing = false, isPublic = false, numConnections = DEFAULT_CONNECTIONS, publicSendData = null, @@ -108,15 +75,6 @@ class LokiMessageAPI { } const data64 = dcodeIO.ByteBuffer.wrap(data).toString('base64'); - const p2pSuccess = await trySendP2p( - pubKey, - data64, - isPing, - messageEventData - ); - if (p2pSuccess) { - return; - } const timestamp = Date.now(); const nonce = await calcNonce( diff --git a/js/modules/loki_p2p_api.js b/js/modules/loki_p2p_api.js deleted file mode 100644 index 7f9900091..000000000 --- a/js/modules/loki_p2p_api.js +++ /dev/null @@ -1,122 +0,0 @@ -/* global setTimeout, clearTimeout */ - -const EventEmitter = require('events'); -const { isEmpty } = require('lodash'); - -const offlinePingTime = 2 * 60 * 1000; // 2 minutes - -class LokiP2pAPI extends EventEmitter { - constructor(ourKey) { - super(); - this.contactP2pDetails = {}; - this.ourKey = ourKey; - } - - reset() { - Object.keys(this.contactP2pDetails).forEach(key => { - clearTimeout(this.contactP2pDetails[key].pingTimer); - delete this.contactP2pDetails[key]; - }); - } - - updateContactP2pDetails(pubKey, address, port, isP2PMessage = false) { - // Stagger the timers so the friends don't ping each other at the same time - const timerDuration = - pubKey < this.ourKey - ? 60 * 1000 // 1 minute - : 2 * 60 * 1000; // 2 minutes - - // Get the current contact details - // This will be empty if we don't have them - const baseDetails = { ...(this.contactP2pDetails[pubKey] || {}) }; - - // Always set the new contact details - this.contactP2pDetails[pubKey] = { - address, - port, - timerDuration, - pingTimer: null, - isOnline: false, - }; - - const contactExists = !isEmpty(baseDetails); - const { isOnline } = baseDetails; - const detailsChanged = - baseDetails.address !== address || baseDetails.port !== port; - - // If we had the contact details - // And we got a P2P message - // And the contact was online - // And the new details that we got matched the old - // Then we don't need to bother pinging - if (contactExists && isP2PMessage && isOnline && !detailsChanged) { - // We also need to set the current contact details to show online - // because they get reset to `false` above - this.setContactOnline(pubKey); - return; - } - - /* - Ping the contact. - This happens in the following scenarios: - 1. We didn't have the contact, we need to ping them to let them know our details. - 2. isP2PMessage = false, so we assume the contact doesn't have our details. - 3. We had the contact marked as offline, - we need to make sure that we can reach their server. - 4. The other contact details have changed, - we need to make sure that we can reach their new server. - */ - this.pingContact(pubKey); - } - - getContactP2pDetails(pubKey) { - if (!this.contactP2pDetails[pubKey]) { - return null; - } - return { ...this.contactP2pDetails[pubKey] }; - } - - setContactOffline(pubKey) { - this.emit('offline', pubKey); - if (!this.contactP2pDetails[pubKey]) { - return; - } - clearTimeout(this.contactP2pDetails[pubKey].pingTimer); - this.contactP2pDetails[pubKey].pingTimer = setTimeout( - this.pingContact.bind(this), - offlinePingTime, - pubKey - ); - this.contactP2pDetails[pubKey].isOnline = false; - } - - setContactOnline(pubKey) { - if (!this.contactP2pDetails[pubKey]) { - return; - } - this.emit('online', pubKey); - clearTimeout(this.contactP2pDetails[pubKey].pingTimer); - this.contactP2pDetails[pubKey].isOnline = true; - this.contactP2pDetails[pubKey].pingTimer = setTimeout( - this.pingContact.bind(this), - this.contactP2pDetails[pubKey].timerDuration, - pubKey - ); - } - - isOnline(pubKey) { - return !!( - this.contactP2pDetails[pubKey] && this.contactP2pDetails[pubKey].isOnline - ); - } - - pingContact(pubKey) { - if (!this.contactP2pDetails[pubKey]) { - // Don't ping if we don't have their details - return; - } - this.emit('pingContact', pubKey); - } -} - -module.exports = LokiP2pAPI; diff --git a/js/modules/loki_snode_api.js b/js/modules/loki_snode_api.js index 53ee50c1e..bb6eb9946 100644 --- a/js/modules/loki_snode_api.js +++ b/js/modules/loki_snode_api.js @@ -2,32 +2,7 @@ /* global window, ConversationController, _, log */ const is = require('@sindresorhus/is'); -const dns = require('dns'); -const process = require('process'); const { lokiRpc } = 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 }) { @@ -38,40 +13,6 @@ class LokiSnodeAPI { 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() { diff --git a/libloki/api.js b/libloki/api.js index 858a247de..9b658a07f 100644 --- a/libloki/api.js +++ b/libloki/api.js @@ -1,4 +1,4 @@ -/* global window, textsecure, log, Whisper, dcodeIO, StringView, ConversationController */ +/* global window, textsecure, Whisper, dcodeIO, StringView, ConversationController */ // eslint-disable-next-line func-names (function() { @@ -8,24 +8,6 @@ return sendOnlineBroadcastMessage(pubKey); } - async function broadcastOnlineStatus() { - const friendKeys = await window.Signal.Data.getPubKeysWithFriendStatus( - window.friends.friendRequestStatusEnum.friends - ); - await Promise.all( - friendKeys.map(async pubKey => { - if (pubKey === textsecure.storage.user.getNumber()) { - return; - } - try { - await sendOnlineBroadcastMessage(pubKey); - } catch (e) { - log.warn(`Failed to send online broadcast message to ${pubKey}`); - } - }) - ); - } - async function sendOnlineBroadcastMessage(pubKey, isPing = false) { const authorisation = await window.libloki.storage.getGrantAuthorisationForSecondaryPubKey( pubKey @@ -34,27 +16,10 @@ sendOnlineBroadcastMessage(authorisation.primaryDevicePubKey); return; } - let p2pAddress = null; - let p2pPort = null; - let type; - - let myIp; - if (window.localLokiServer && window.localLokiServer.isListening()) { - try { - // clearnet change: getMyLokiAddress -> getMyClearIP - // const myLokiAddress = await window.lokiSnodeAPI.getMyLokiAddress(); - myIp = await window.lokiSnodeAPI.getMyClearIp(); - } catch (e) { - log.warn(`Failed to get clear IP for local server ${e}`); - } - } - if (myIp) { - p2pAddress = `https://${myIp}`; - p2pPort = window.localLokiServer.getPublicPort(); - type = textsecure.protobuf.LokiAddressMessage.Type.HOST_REACHABLE; - } else { - type = textsecure.protobuf.LokiAddressMessage.Type.HOST_UNREACHABLE; - } + const p2pAddress = null; + const p2pPort = null; + // We result loki address message for sending "background" messages + const type = textsecure.protobuf.LokiAddressMessage.Type.HOST_UNREACHABLE; const lokiAddressMessage = new textsecure.protobuf.LokiAddressMessage({ p2pAddress, @@ -252,7 +217,6 @@ window.libloki.api = { sendBackgroundMessage, sendOnlineBroadcastMessage, - broadcastOnlineStatus, sendPairingAuthorisation, createPairingAuthorisationProtoMessage, sendUnpairingMessageToSecondary, diff --git a/libloki/modules/local_loki_server.js b/libloki/modules/local_loki_server.js deleted file mode 100644 index aac6419fd..000000000 --- a/libloki/modules/local_loki_server.js +++ /dev/null @@ -1,219 +0,0 @@ -/* global textsecure */ -const https = require('https'); -const EventEmitter = require('events'); -const natUpnp = require('nat-upnp'); - -const STATUS = { - OK: 200, - BAD_REQUEST: 400, - NOT_FOUND: 404, - METHOD_NOT_ALLOWED: 405, - INTERNAL_SERVER_ERROR: 500, -}; - -class LocalLokiServer extends EventEmitter { - /** - * Creates an instance of LocalLokiServer. - * Sends out a `message` event when a new message is received. - */ - constructor(pems, options = {}) { - super(); - const httpsOptions = { - key: pems.private, - cert: pems.cert, - }; - if (!options.skipUpnp) { - this.upnpClient = natUpnp.createClient(); - } - this.server = https.createServer(httpsOptions, (req, res) => { - let body = []; - - const sendResponse = (statusCode, message = null) => { - const headers = message && { - 'Content-Type': 'text/plain', - }; - res.writeHead(statusCode, headers); - res.end(message); - }; - - if (req.method !== 'POST') { - sendResponse(STATUS.METHOD_NOT_ALLOWED); - return; - } - - // Check endpoints - req - .on('error', () => { - // Internal server error - sendResponse(STATUS.INTERNAL_SERVER_ERROR); - }) - .on('data', chunk => { - body.push(chunk); - }) - .on('end', () => { - try { - body = Buffer.concat(body).toString(); - } catch (e) { - // Internal server error: failed to convert body to string - sendResponse(STATUS.INTERNAL_SERVER_ERROR); - } - - // Check endpoints here - if (req.url === '/storage_rpc/v1') { - try { - const bodyObject = JSON.parse(body); - if (bodyObject.method !== 'store') { - sendResponse(STATUS.NOT_FOUND, 'Invalid endpoint!'); - return; - } - this.emit('message', { - message: bodyObject.params.data, - onSuccess: () => sendResponse(STATUS.OK), - onFailure: () => sendResponse(STATUS.NOT_FOUND), - }); - } catch (e) { - // Bad Request: Failed to decode json - sendResponse(STATUS.BAD_REQUEST, 'Failed to decode JSON'); - } - } else { - sendResponse(STATUS.NOT_FOUND, 'Invalid endpoint!'); - } - }); - }); - } - - async start(port, ip) { - // Close the old server - await this.close(); - - // Start a listening on new server - return new Promise((res, rej) => { - this.server.listen(port, ip, async err => { - if (err) { - rej(err); - } else if (this.upnpClient) { - try { - const publicPort = await this.punchHole(); - res(publicPort); - } catch (e) { - if (e instanceof textsecure.HolePunchingError) { - await this.close(); - } - rej(e); - } - } else { - res(port); - } - }); - }); - } - - async punchHole() { - const privatePort = this.server.address().port; - const portStart = 22100; - const portEnd = 22200; - const ttl = 60 * 15; // renew upnp every 15 minutes - const publicPortsInUse = await new Promise((resolve, reject) => { - this.upnpClient.getMappings({ local: true }, (err, results) => { - if (err) { - // We assume an error here means upnp not enabled - reject( - new textsecure.HolePunchingError( - 'Could not get mapping from upnp. Upnp not available?', - err - ) - ); - } else { - // remove the current private port from the current mapping - // to allow reusing that port. - resolve( - results - .filter(entry => entry.private.port !== privatePort) - .map(entry => entry.public.port) - ); - } - }); - }); - - for (let publicPort = portStart; publicPort <= portEnd; publicPort += 1) { - if (publicPortsInUse.includes(publicPort)) { - // eslint-disable-next-line no-continue - continue; - } - const p = new Promise((resolve, reject) => { - this.upnpClient.portMapping( - { - public: publicPort, - private: privatePort, - ttl, - }, - err => { - if (err) { - reject(err); - } else { - resolve(); - } - } - ); - }); - try { - // eslint-disable-next-line no-await-in-loop - await p; - this.publicPort = publicPort; - this.timerHandler = setTimeout(async () => { - try { - this.publicPort = await this.punchHole(); - } catch (e) { - this.close(); - } - }, ttl * 1000); - return publicPort; - } catch (e) { - throw new textsecure.HolePunchingError( - 'Could not punch hole. Disabled upnp?', - e - ); - } - } - const e = new Error(); - throw new textsecure.HolePunchingError( - `Could not punch hole: no available port. Public ports: ${portStart}-${portEnd}`, - e - ); - } - // Async wrapper for http server close - close() { - clearInterval(this.timerHandler); - if (this.upnpClient) { - this.upnpClient.portUnmapping({ - public: this.publicPort, - }); - this.publicPort = null; - } - if (this.server) { - return new Promise(res => { - this.server.close(() => res()); - }); - } - - return Promise.resolve(); - } - - getPort() { - if (this.server.listening) { - return this.server.address().port; - } - - return null; - } - - getPublicPort() { - return this.publicPort; - } - - isListening() { - return this.server.listening; - } -} - -module.exports = LocalLokiServer; diff --git a/libloki/test/node/local_loki_server_test.js b/libloki/test/node/local_loki_server_test.js deleted file mode 100644 index 0367ecf18..000000000 --- a/libloki/test/node/local_loki_server_test.js +++ /dev/null @@ -1,111 +0,0 @@ -const axios = require('axios'); -const { assert } = require('chai'); -const LocalLokiServer = require('../../modules/local_loki_server'); -const selfsigned = require('selfsigned'); -const https = require('https'); - -class HolePunchingError extends Error { - constructor(message, err) { - super(message); - this.name = 'HolePunchingError'; - this.error = err; - } -} - -describe('LocalLokiServer', () => { - before(async () => { - const attrs = [{ name: 'commonName', value: 'mypubkey' }]; - const pems = selfsigned.generate(attrs, { days: 365 * 10 }); - global.textsecure = {}; - global.textsecure.HolePunchingError = HolePunchingError; - this.server = new LocalLokiServer(pems, { skipUpnp: true }); - await this.server.start(8000); - this.axiosClient = axios.create({ - httpsAgent: new https.Agent({ - rejectUnauthorized: false, - }), - }); - }); - - after(async () => { - await this.server.close(); - }); - - it('should return 405 if not a POST request', async () => { - try { - await this.axiosClient.get('https://localhost:8000'); - assert.fail('Got a successful response'); - } catch (error) { - if (error.response) { - assert.equal(405, error.response.status); - return; - } - assert.isNotOk(error, 'Another error was receieved'); - } - }); - - it('should return 404 if no endpoint provided', async () => { - try { - await this.axiosClient.post('https://localhost:8000', { name: 'Test' }); - assert.fail('Got a successful response'); - } catch (error) { - if (error.response) { - assert.equal(404, error.response.status); - return; - } - assert.isNotOk(error, 'Another error was receieved'); - } - }); - - it('should return 404 and a string if invalid enpoint is provided', async () => { - try { - await this.axiosClient.post('https://localhost:8000/invalid', { - name: 'Test', - }); - assert.fail('Got a successful response'); - } catch (error) { - if (error.response) { - assert.equal(404, error.response.status); - assert.equal('Invalid endpoint!', error.response.data); - return; - } - assert.isNotOk(error, 'Another error was receieved'); - } - }); - - describe('/store', async () => { - it('should pass the POSTed data to the callback', async () => { - const attrs = [{ name: 'commonName', value: 'mypubkey' }]; - const pems = selfsigned.generate(attrs, { days: 365 * 10 }); - const server = new LocalLokiServer(pems, { skipUpnp: true }); - await server.start(8001); - const messageData = { - method: 'store', - params: { - data: 'This is data', - }, - }; - - const promise = new Promise(res => { - server.on('message', eventData => { - const { message, onSuccess } = eventData; - assert.equal(message, 'This is data'); - onSuccess(); - server.close(); - res(); - }); - }); - - try { - await this.axiosClient.post( - 'https://localhost:8001/storage_rpc/v1', - messageData - ); - } catch (error) { - assert.isNotOk(error, 'Error occured'); - } - - return promise; - }); - }); -}); diff --git a/libloki/test/node/loki_p2p_api_test.js b/libloki/test/node/loki_p2p_api_test.js deleted file mode 100644 index a0272a8c0..000000000 --- a/libloki/test/node/loki_p2p_api_test.js +++ /dev/null @@ -1,193 +0,0 @@ -const { assert } = require('chai'); -const LokiP2pAPI = require('../../../js/modules/loki_p2p_api'); - -describe('LokiP2pAPI', () => { - const usedKey = 'aPubKey'; - const usedAddress = 'anAddress'; - const usedPort = 'aPort'; - - const usedDetails = { - address: usedAddress, - port: usedPort, - timerDuration: 100, - pingTimer: null, - isOnline: false, - }; - - beforeEach(() => { - this.lokiP2pAPI = new LokiP2pAPI(); - }); - - afterEach(() => { - this.lokiP2pAPI.removeAllListeners(); - this.lokiP2pAPI.reset(); - }); - - describe('getContactP2pDetails', () => { - it('Should return null if no contact details exist', () => { - const details = this.lokiP2pAPI.getContactP2pDetails(usedKey); - assert.isNull(details); - }); - - it('Should return the exact same object if contact details exist', () => { - this.lokiP2pAPI.contactP2pDetails[usedKey] = usedDetails; - const details = this.lokiP2pAPI.getContactP2pDetails(usedKey); - assert.deepEqual(details, usedDetails); - }); - }); - - describe('pingContact', () => { - it("Should not emit a pingContact event if that contact doesn't exits", () => { - this.lokiP2pAPI.on('pingContact', () => { - assert.fail(); - }); - this.lokiP2pAPI.pingContact('not stored'); - }); - }); - - describe('updateContactP2pDetails', () => { - it("Shouldn't ping a contact if contact exists, p2p message was sent, contact was online and details didn't change", () => { - this.lokiP2pAPI.on('pingContact', () => { - assert.fail(); - }); - - // contact exists - const details = { ...usedDetails }; - // P2p message - const isP2P = true; - // Contact was online - details.isOnline = true; - // details were the same - const { address, port } = details; - - this.lokiP2pAPI.contactP2pDetails[usedKey] = details; - this.lokiP2pAPI.updateContactP2pDetails(usedKey, address, port, isP2P); - - // They should also be marked as online - assert.isTrue(this.lokiP2pAPI.isOnline(usedKey)); - }); - - it("Should ping a contact if we don't have details for it", done => { - this.lokiP2pAPI.on('pingContact', pubKey => { - assert.strictEqual(pubKey, usedKey); - assert.isFalse(this.lokiP2pAPI.isOnline(usedKey)); - done(); - }); - this.lokiP2pAPI.updateContactP2pDetails( - usedKey, - usedAddress, - usedPort, - true - ); - }); - - it("Should ping a contact if a P2P message wasn't received", done => { - // The precondition for this is that we had the contact stored - this.lokiP2pAPI.contactP2pDetails[usedKey] = { ...usedDetails }; - - this.lokiP2pAPI.on('pingContact', pubKey => { - assert.strictEqual(pubKey, usedKey); - assert.isFalse(this.lokiP2pAPI.isOnline(usedKey)); - done(); - }); - this.lokiP2pAPI.updateContactP2pDetails( - usedKey, - usedAddress, - usedPort, - false // We didn't get a p2p message - ); - }); - - it('Should ping a contact if they were marked as offline', done => { - // The precondition for this is that we had the contact stored - // And that p2p message was true - this.lokiP2pAPI.contactP2pDetails[usedKey] = { ...usedDetails }; - - this.lokiP2pAPI.on('pingContact', pubKey => { - assert.strictEqual(pubKey, usedKey); - assert.isFalse(this.lokiP2pAPI.isOnline(usedKey)); - done(); - }); - this.lokiP2pAPI.updateContactP2pDetails( - usedKey, - usedAddress, - usedPort, - true // We got a p2p message - ); - }); - - it('Should ping a contact if the address was different', done => { - // The precondition for this is that we had the contact stored - // And that p2p message was true - // And that the user was online - this.lokiP2pAPI.contactP2pDetails[usedKey] = { ...usedDetails }; - this.lokiP2pAPI.contactP2pDetails[usedKey].isOnline = true; - - this.lokiP2pAPI.on('pingContact', pubKey => { - assert.strictEqual(pubKey, usedKey); - done(); - }); - this.lokiP2pAPI.updateContactP2pDetails( - usedKey, - 'different address', - usedPort, - true // We got a p2p message - ); - }); - - it('Should ping a contact if the port was different', done => { - // The precondition for this is that we had the contact stored - // And that p2p message was true - // And that the user was online - this.lokiP2pAPI.contactP2pDetails[usedKey] = { ...usedDetails }; - this.lokiP2pAPI.contactP2pDetails[usedKey].isOnline = true; - - this.lokiP2pAPI.on('pingContact', pubKey => { - assert.strictEqual(pubKey, usedKey); - done(); - }); - this.lokiP2pAPI.updateContactP2pDetails( - usedKey, - usedAddress, - 'different port', - true // We got a p2p message - ); - }); - - it('Should emit an online event if the contact is online', done => { - this.lokiP2pAPI.on('online', pubKey => { - assert.strictEqual(pubKey, usedKey); - done(); - }); - this.lokiP2pAPI.contactP2pDetails[usedKey] = { ...usedDetails }; - this.lokiP2pAPI.setContactOnline(usedKey); - }).timeout(1000); - - it('Should store a contacts p2p details', () => { - this.lokiP2pAPI.updateContactP2pDetails( - usedKey, - usedAddress, - usedPort, - true - ); - const p2pDetails = this.lokiP2pAPI.getContactP2pDetails(usedKey); - assert.strictEqual(usedAddress, p2pDetails.address); - assert.strictEqual(usedPort, p2pDetails.port); - }); - - it('Should set a contact as offline and online', () => { - this.lokiP2pAPI.contactP2pDetails[usedKey] = { ...usedDetails }; - let p2pDetails = this.lokiP2pAPI.getContactP2pDetails(usedKey); - assert.isNotNull(p2pDetails); - assert.isFalse(p2pDetails.isOnline); - this.lokiP2pAPI.setContactOnline(usedKey); - - p2pDetails = this.lokiP2pAPI.getContactP2pDetails(usedKey); - assert.isTrue(p2pDetails.isOnline); - this.lokiP2pAPI.setContactOffline(usedKey); - - p2pDetails = this.lokiP2pAPI.getContactP2pDetails(usedKey); - assert.isFalse(p2pDetails.isOnline); - }); - }); -}); diff --git a/libtextsecure/errors.js b/libtextsecure/errors.js index 0343a836f..7c5fbe050 100644 --- a/libtextsecure/errors.js +++ b/libtextsecure/errors.js @@ -163,38 +163,6 @@ } inherit(ReplayableError, DNSResolutionError); - function HolePunchingError(message, error) { - this.name = 'HolePunchingError'; - this.message = message; - this.error = error; - - Error.call(this, message); - - // Maintains proper stack trace, where our error was thrown (only available on V8) - // via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error - if (Error.captureStackTrace) { - Error.captureStackTrace(this); - } - - appendStack(this, error); - } - - function LokiIpError(message, resolutionError) { - this.name = 'LokiIpError'; - this.message = message; - this.error = resolutionError; - - Error.call(this, message); - - // Maintains proper stack trace, where our error was thrown (only available on V8) - // via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error - if (Error.captureStackTrace) { - Error.captureStackTrace(this); - } - - appendStack(this, resolutionError); - } - function NotFoundError(message, error) { this.name = 'NotFoundError'; this.message = message; @@ -307,8 +275,6 @@ window.textsecure.EmptySwarmError = EmptySwarmError; window.textsecure.SeedNodeError = SeedNodeError; window.textsecure.DNSResolutionError = DNSResolutionError; - window.textsecure.LokiIpError = LokiIpError; - window.textsecure.HolePunchingError = HolePunchingError; window.textsecure.HTTPError = HTTPError; window.textsecure.NotFoundError = NotFoundError; window.textsecure.WrongSwarmError = WrongSwarmError; diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index bd2ea72b8..895fd81b1 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -12,11 +12,8 @@ /* global ContactBuffer: false */ /* global GroupBuffer: false */ /* global WebSocketResource: false */ -/* global localLokiServer: false */ /* global lokiPublicChatAPI: false */ -/* global localServerPort: false */ /* global lokiMessageAPI: false */ -/* global lokiP2pAPI: false */ /* global feeds: false */ /* global Whisper: false */ /* global lokiFileServerAPI: false */ @@ -82,9 +79,6 @@ MessageReceiver.prototype.extend({ handleRequest: this.handleRequest.bind(this), }); this.httpPollingResource.pollServer(); - if (localLokiServer) { - localLokiServer.on('message', this.handleP2pMessage.bind(this)); - } if (lokiPublicChatAPI) { lokiPublicChatAPI.on( 'publicMessage', @@ -95,7 +89,6 @@ MessageReceiver.prototype.extend({ feeds.forEach(feed => { feed.on('rssMessage', this.handleUnencryptedMessage.bind(this)); }); - this.startLocalServer(); // TODO: Rework this socket stuff to work with online messaging const useWebSocket = false; @@ -126,45 +119,6 @@ MessageReceiver.prototype.extend({ // all cached envelopes are processed. this.incoming = [this.pending]; }, - async startLocalServer() { - if (!localLokiServer) { - return; - } - try { - // clearnet change: getMyLokiIp -> getMyClearIp - // const myLokiIp = await window.lokiSnodeAPI.getMyLokiIp(); - const myLokiIp = '0.0.0.0'; - const myServerPort = await localLokiServer.start( - localServerPort, - myLokiIp - ); - window.log.info(`Local Server started at ${myLokiIp}:${myServerPort}`); - libloki.api.broadcastOnlineStatus(); - } catch (e) { - if (e instanceof textsecure.HolePunchingError) { - window.log.warn(e.message); - window.log.warn('Abdandoning starting p2p server.'); - return; - } else if (e instanceof textsecure.LokiIpError) { - window.log.warn( - 'Failed to get my loki address to bind server to, will retry in 30 seconds' - ); - } else { - window.log.warn( - 'Failed to start local loki server, will retry in 30 seconds' - ); - } - setTimeout(this.startLocalServer.bind(this), 30 * 1000); - } - }, - handleP2pMessage({ message, onSuccess, onFailure }) { - const options = { - isP2p: true, - onSuccess, - onFailure, - }; - this.httpPollingResource.handleMessage(message, options); - }, async handleUnencryptedMessage({ message }) { const isMe = message.source === textsecure.storage.user.getNumber(); if (!isMe && message.message.profile) { @@ -201,13 +155,6 @@ MessageReceiver.prototype.extend({ this.wsr.removeEventListener('close', this._onClose); this.wsr = null; } - - if (localLokiServer) { - localLokiServer.removeListener( - 'message', - this.handleP2pMessage.bind(this) - ); - } }, async close() { window.log.info('MessageReceiver.close()'); @@ -219,10 +166,6 @@ MessageReceiver.prototype.extend({ this.wsr.close(3000, 'called close'); } - if (localLokiServer) { - localLokiServer.close(); - } - if (lokiPublicChatAPI) { await lokiPublicChatAPI.close(); } @@ -279,7 +222,7 @@ MessageReceiver.prototype.extend({ // }); }, handleRequest(request, options) { - const { isP2p, onSuccess, onFailure } = options; + const { onSuccess, onFailure } = options; this.incoming = this.incoming || []; const lastPromise = _.last(this.incoming); @@ -299,9 +242,6 @@ MessageReceiver.prototype.extend({ const promise = Promise.resolve(request.body.toArrayBuffer()) // textsecure.crypto .then(plaintext => { const envelope = textsecure.protobuf.Envelope.decode(plaintext); - if (isP2p) { - lokiP2pAPI.setContactOnline(envelope.source); - } // After this point, decoding errors are not the server's // fault, and we should handle them gracefully and tell the // user they received an invalid message @@ -311,7 +251,6 @@ MessageReceiver.prototype.extend({ } envelope.id = envelope.serverGuid || window.getGuid(); - envelope.isP2p = isP2p; envelope.serverTimestamp = envelope.serverTimestamp ? envelope.serverTimestamp.toNumber() : null; @@ -1089,16 +1028,8 @@ MessageReceiver.prototype.extend({ }) ); }, - async handleLokiAddressMessage(envelope, lokiAddressMessage) { - const { p2pAddress, p2pPort, type } = lokiAddressMessage; - if (type === textsecure.protobuf.LokiAddressMessage.Type.HOST_REACHABLE) { - lokiP2pAPI.updateContactP2pDetails( - envelope.source, - p2pAddress, - p2pPort, - envelope.isP2p - ); - } + async handleLokiAddressMessage(envelope) { + window.log.warn('Ignoring a Loki address message'); return this.removeFromCache(envelope); }, async handlePairingRequest(envelope, pairingRequest) { @@ -1319,14 +1250,6 @@ MessageReceiver.prototype.extend({ await conversation.setLokiProfile(newProfile); }, handleDataMessage(envelope, msg) { - if (!envelope.isP2p) { - const timestamp = envelope.timestamp.toNumber(); - const now = Date.now(); - const ageInSeconds = (now - timestamp) / 1000; - if (ageInSeconds <= 120) { - lokiP2pAPI.pingContact(envelope.source); - } - } window.log.info('data message from', this.getEnvelopeId(envelope)); let p = Promise.resolve(); // eslint-disable-next-line no-bitwise @@ -1465,7 +1388,6 @@ MessageReceiver.prototype.extend({ timestamp: envelope.timestamp.toNumber(), receivedAt: envelope.receivedAt, unidentifiedDeliveryReceived: envelope.unidentifiedDeliveryReceived, - isP2p: envelope.isP2p, message, }; return this.dispatchAndWait(ev); diff --git a/package.json b/package.json index bb6a4e3ba..c0e0fd125 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,6 @@ "mkdirp": "0.5.1", "moment": "2.21.0", "mustache": "2.3.0", - "nat-upnp": "^1.1.1", "node-fetch": "2.3.0", "node-gyp": "3.8.0", "node-sass": "4.9.3", @@ -120,7 +119,6 @@ "redux-promise-middleware": "6.1.0", "reselect": "4.0.0", "rimraf": "2.6.2", - "selfsigned": "^1.10.4", "semver": "5.4.1", "spellchecker": "3.5.1", "tar": "4.4.8", @@ -160,7 +158,6 @@ "@types/uuid": "3.4.4", "arraybuffer-loader": "1.0.3", "asar": "0.14.0", - "axios": "0.18.0", "bower": "1.8.2", "chai": "4.1.2", "dashdash": "1.14.1", diff --git a/preload.js b/preload.js index f2ac8ea28..005b2318b 100644 --- a/preload.js +++ b/preload.js @@ -3,7 +3,6 @@ const path = require('path'); const electron = require('electron'); const semver = require('semver'); -const selfsigned = require('selfsigned'); const { deferredToPromise } = require('./js/modules/deferred_to_promise'); const { JobQueue } = require('./js/modules/job_queue'); @@ -74,19 +73,6 @@ window.versionInfo = { // temporary clearnet fix process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; -window.getSelfSignedCert = () => { - let pems = window.storage.get('self-signed-certificate', null); - if (!pems) { - const pubKey = window.storage.get('number_id'); - const attrs = [{ name: 'commonName', value: pubKey }]; - pems = selfsigned.generate(attrs, { days: 365 * 10 }); - window.storage.put('self-signed-certificate', pems); - window.log.info(`Created PEM for p2p:\n${pems}`); - } else { - window.log.info(`Found existing PEM for p2p:\n${pems}`); - } - return pems; -}; window.wrapDeferred = deferredToPromise; @@ -377,8 +363,6 @@ window.lokiSnodeAPI = new LokiSnodeAPI({ localUrl: config.localUrl, }); -window.LokiP2pAPI = require('./js/modules/loki_p2p_api'); - window.LokiMessageAPI = require('./js/modules/loki_message_api'); window.LokiPublicChatAPI = require('./js/modules/loki_public_chat_api'); @@ -391,8 +375,6 @@ const LokiMixpanelAPI = require('./js/modules/loki_mixpanel.js'); window.mixpanel = new LokiMixpanelAPI(); -window.LocalLokiServer = require('./libloki/modules/local_loki_server'); - window.localServerPort = config.localServerPort; window.mnemonic = require('./libloki/modules/mnemonic'); diff --git a/test/_test.js b/test/_test.js index 2573aca10..6e1e03ae2 100644 --- a/test/_test.js +++ b/test/_test.js @@ -82,6 +82,5 @@ window.clearDatabase = async () => { await window.storage.fetch(); }; -window.lokiP2pAPI = new window.LokiP2pAPI('ourKey'); window.Whisper = window.Whisper || {}; window.Whisper.events = _.clone(Backbone.Events); diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 50c51969f..7570db100 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -97,7 +97,6 @@ export interface Props { expirationLength?: number; expirationTimestamp?: number; convoId: string; - isP2p?: boolean; isPublic?: boolean; isRss?: boolean; selected: boolean; @@ -214,13 +213,9 @@ export class Message extends React.PureComponent { } public renderMetadataBadges() { - const { direction, isP2p, isPublic, senderIsModerator } = this.props; + const { direction, isPublic, senderIsModerator } = this.props; - const badges = [ - isPublic && 'Public', - isP2p && 'P2p', - senderIsModerator && 'Mod', - ]; + const badges = [isPublic && 'Public', senderIsModerator && 'Mod']; return badges .map(badgeText => { diff --git a/yarn.lock b/yarn.lock index b7bd6eabc..daed795cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -907,7 +907,7 @@ async@^1.5.0, async@~1.5.2: resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= -async@^2.0.0, async@^2.1.4, async@^2.1.5, async@^2.6.2: +async@^2.0.0, async@^2.1.4, async@^2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== @@ -951,14 +951,6 @@ aws4@^1.6.0, aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== -axios@0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102" - integrity sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI= - dependencies: - follow-redirects "^1.3.0" - is-buffer "^1.1.5" - babel-code-frame@6.26.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" @@ -3792,7 +3784,7 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" -follow-redirects@^1.0.0, follow-redirects@^1.3.0: +follow-redirects@^1.0.0: version "1.9.0" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.9.0.tgz#8d5bcdc65b7108fe1508649c79c12d732dcedb4f" integrity sha512-CRcPzsSIbXyVDl0QI01muNDu69S8trU4jArW9LpOt2WtC6LyUJetcIrmfHsRBx7/Jb6GHJUiuqyYxPooFfNt6A== @@ -4989,7 +4981,7 @@ ip-regex@^1.0.1: resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-1.0.3.tgz#dc589076f659f419c222039a33316f1c7387effd" integrity sha1-3FiQdvZZ9BnCIgOaMzFvHHOH7/0= -ip@1.1.5, ip@^1.1.0, ip@^1.1.4, ip@^1.1.5: +ip@1.1.5, ip@^1.1.0, ip@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= @@ -6575,16 +6567,6 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" -nat-upnp@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/nat-upnp/-/nat-upnp-1.1.1.tgz#b18365e4faf44652549bb593c69e6b690df22043" - integrity sha512-b1Q+sf9fHGCXhlWErNgTTEto8A02MnNysw3vx3kD1657+/Ae23vPEAB6QBh+9RqLL4+xw/LmjVTiLy6A7Cx0xw== - dependencies: - async "^2.1.5" - ip "^1.1.4" - request "^2.79.0" - xml2js "~0.1.14" - native-or-lie@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/native-or-lie/-/native-or-lie-1.0.2.tgz#c870ee0ba0bf0ff11350595d216cfea68a6d8086" @@ -8867,7 +8849,7 @@ request@2.87.0: tunnel-agent "^0.6.0" uuid "^3.1.0" -request@2.x, request@^2.45.0, request@^2.65.0, request@^2.79.0, request@^2.81.0, request@^2.83.0, request@^2.87.0: +request@2.x, request@^2.45.0, request@^2.65.0, request@^2.81.0, request@^2.83.0, request@^2.87.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== @@ -9114,7 +9096,7 @@ sass-graph@^2.2.4: scss-tokenizer "^0.2.3" yargs "^7.0.0" -sax@>=0.1.1, sax@>=0.6.0, sax@^1.2.4, sax@~1.2.1: +sax@>=0.6.0, sax@^1.2.4, sax@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -9148,7 +9130,7 @@ select-hose@^2.0.0: resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= -selfsigned@^1.10.4, selfsigned@^1.9.1: +selfsigned@^1.9.1: version "1.10.7" resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.7.tgz#da5819fd049d5574f28e88a9bcc6dbc6e6f3906b" integrity sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA== @@ -11104,13 +11086,6 @@ xml2js@^0.4.5: sax ">=0.6.0" xmlbuilder "~11.0.0" -xml2js@~0.1.14: - version "0.1.14" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.1.14.tgz#5274e67f5a64c5f92974cd85139e0332adc6b90c" - integrity sha1-UnTmf1pkxfkpdM2FE54DMq3GuQw= - dependencies: - sax ">=0.1.1" - xmlbuilder@~11.0.0: version "11.0.1" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"