diff --git a/Gruntfile.js b/Gruntfile.js index 95e327054..97ee125bd 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -102,6 +102,7 @@ module.exports = grunt => { libloki: { src: [ 'libloki/api.js', + 'libloki/friends.js', 'libloki/crypto.js', 'libloki/service_nodes.js', 'libloki/storage.js', diff --git a/app/sql.js b/app/sql.js index 7be7ec038..b64c4feac 100644 --- a/app/sql.js +++ b/app/sql.js @@ -90,6 +90,7 @@ module.exports = { updateConversation, removeConversation, getAllConversations, + getPubKeysWithFriendStatus, getAllConversationIds, getAllPrivateConversations, getAllGroupsInvolvingId, @@ -1127,7 +1128,7 @@ async function getSwarmNodesByPubkey(pubkey) { }); if (!row) { - return null; + return []; } return jsonToObject(row.json).swarmNodes; @@ -1280,6 +1281,18 @@ async function getAllConversations() { return map(rows, row => jsonToObject(row.json)); } +async function getPubKeysWithFriendStatus(status) { + const rows = await db.all( + `SELECT id FROM conversations WHERE + friendRequestStatus = $status + ORDER BY id ASC;`, + { + $status: status, + } + ); + return map(rows, row => row.id); +} + async function getAllConversationIds() { const rows = await db.all('SELECT id FROM conversations ORDER BY id ASC;'); return map(rows, row => row.id); diff --git a/config/default.json b/config/default.json index 3b605aa6d..09b15a766 100644 --- a/config/default.json +++ b/config/default.json @@ -1,6 +1,7 @@ { "serverUrl": "random.snode", "cdnUrl": "random.snode", + "localServerPort": "8081", "messageServerPort": "8080", "swarmServerPort": "8079", "disableAutoUpdate": false, diff --git a/config/development-1.json b/config/development-1.json index 9099ce326..57b4796c5 100644 --- a/config/development-1.json +++ b/config/development-1.json @@ -1,5 +1,6 @@ { "storageProfile": "development1", + "localServerPort": "8082", "disableAutoUpdate": true, "openDevTools": true } diff --git a/js/conversation_controller.js b/js/conversation_controller.js index c92f07c9c..8decd5151 100644 --- a/js/conversation_controller.js +++ b/js/conversation_controller.js @@ -182,11 +182,10 @@ } try { - const swarmNodes = await window.LokiSnodeAPI.getFreshSwarmNodes(id); - conversation.set({ swarmNodes}); await window.Signal.Data.saveConversation(conversation.attributes, { Conversation: Whisper.Conversation, }); + window.LokiSnodeAPI.refreshSwarmNodesForPubKey(id); } catch (error) { window.log.error( 'Conversation save failed! ', diff --git a/js/models/conversations.js b/js/models/conversations.js index d4274e3af..7f510916e 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -41,18 +41,7 @@ } = window.Signal.Migrations; // Possible conversation friend states - const FriendRequestStatusEnum = Object.freeze({ - // New conversation, no messages sent or received - none: 0, - // This state is used to lock the input early while sending - pendingSend: 1, - // Friend request sent, awaiting response - requestSent: 2, - // Friend request received, awaiting user input - requestReceived: 3, - // We did it! - friends: 4, - }); + const FriendRequestStatusEnum = window.friends.friendRequestStatusEnum; // Possible session reset states const SessionResetEnum = Object.freeze({ diff --git a/js/modules/data.js b/js/modules/data.js index e8afe5b14..d4e2fb58e 100644 --- a/js/modules/data.js +++ b/js/modules/data.js @@ -110,6 +110,7 @@ module.exports = { removeAllSessions, getSwarmNodesByPubkey, + saveSwarmNodesForPubKey, getConversationCount, saveConversation, @@ -120,6 +121,7 @@ module.exports = { _removeConversations, getAllConversations, + getPubKeysWithFriendStatus, getAllConversationIds, getAllPrivateConversations, getAllGroupsInvolvingId, @@ -673,6 +675,14 @@ async function getSwarmNodesByPubkey(pubkey) { return channels.getSwarmNodesByPubkey(pubkey); } +async function saveSwarmNodesForPubKey(pubKey, swarmNodes, { Conversation }) { + const conversation = await getConversationById(pubKey, { Conversation }); + conversation.set({ swarmNodes }); + await updateConversation(conversation.id, conversation.attributes, { + Conversation, + }); +} + async function getConversationCount() { return channels.getConversationCount(); } @@ -721,6 +731,10 @@ async function _removeConversations(ids) { await channels.removeConversation(ids); } +async function getPubKeysWithFriendStatus(status) { + return channels.getPubKeysWithFriendStatus(status); +} + async function getAllConversations({ ConversationCollection }) { const conversations = await channels.getAllConversations(); diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index d479c3731..ddd565548 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 */ +/* global log, dcodeIO, window, callWorker, Whisper */ const fetch = require('node-fetch'); const _ = require('lodash'); @@ -105,20 +105,15 @@ class LokiMessageAPI { throw HTTPError('sendMessage: error response', response.status, result); }; - let swarmNodes; - try { - swarmNodes = await window.LokiSnodeAPI.getSwarmNodesByPubkey(pubKey); - } catch (e) { - throw new window.textsecure.EmptySwarmError(pubKey, e); - } + let swarmNodes = await window.Signal.Data.getSwarmNodesByPubkey(pubKey); while (successfulRequests < MINIMUM_SUCCESSFUL_REQUESTS) { if (!canResolve) { throw new window.textsecure.DNSResolutionError('Sending messages'); } - if (!swarmNodes || swarmNodes.length === 0) { + if (swarmNodes.length === 0) { swarmNodes = await window.LokiSnodeAPI.getFreshSwarmNodes(pubKey); swarmNodes = _.difference(swarmNodes, completedNodes); - if (!swarmNodes || swarmNodes.length === 0) { + if (swarmNodes.length === 0) { if (successfulRequests !== 0) { // TODO: Decide how to handle some completed requests but not enough return; @@ -128,7 +123,9 @@ class LokiMessageAPI { new Error('Ran out of swarm nodes to query') ); } - await window.LokiSnodeAPI.saveSwarmNodes(pubKey, swarmNodes); + await window.Signal.Data.saveSwarmNodesForPubKey(pubKey, swarmNodes, { + Conversation: Whisper.Conversation, + }); } const remainingRequests = MINIMUM_SUCCESSFUL_REQUESTS - completedNodes.length; diff --git a/js/modules/loki_p2p_api.js b/js/modules/loki_p2p_api.js new file mode 100644 index 000000000..fff8e30d1 --- /dev/null +++ b/js/modules/loki_p2p_api.js @@ -0,0 +1,24 @@ +class LokiP2pAPI { + constructor() { + this.contactP2pDetails = {}; + } + + addContactP2pDetails(pubKey, address, port) { + this.contactP2pDetails[pubKey] = { + address, + port, + }; + } + + getContactP2pDetails(pubKey) { + return this.contactP2pDetails[pubKey] || null; + } + + removeContactP2pDetails(pubKey) { + delete this.contactP2pDetails[pubKey]; + } +} + +module.exports = { + LokiP2pAPI, +}; diff --git a/js/modules/loki_snode_api.js b/js/modules/loki_snode_api.js index d6000c3e8..8d1dc501b 100644 --- a/js/modules/loki_snode_api.js +++ b/js/modules/loki_snode_api.js @@ -106,24 +106,11 @@ class LokiSnodeAPI { return this.ourSwarmNodes; } - async getSwarmNodesByPubkey(pubKey) { - const swarmNodes = await window.Signal.Data.getSwarmNodesByPubkey(pubKey); - if (swarmNodes) { - return swarmNodes; - } - return []; - } - - async saveSwarmNodes(pubKey, swarmNodes) { - const conversation = window.ConversationController.get(pubKey); - conversation.set({ swarmNodes }); - await window.Signal.Data.updateConversation( - conversation.id, - conversation.attributes, - { - Conversation: Whisper.Conversation, - } - ); + async refreshSwarmNodesForPubKey(pubKey) { + const newNodes = await this.getFreshSwarmNodes(pubKey); + await window.Signal.Data.saveSwarmNodesForPubKey(pubKey, newNodes, { + Conversation: Whisper.Conversation, + }); } async getFreshSwarmNodes(pubKey) { diff --git a/libloki/api.js b/libloki/api.js index 6c5c8365f..876cd15e2 100644 --- a/libloki/api.js +++ b/libloki/api.js @@ -8,6 +8,53 @@ return sendEmptyMessage(pubKey, true); } + async function broadcastOnlineStatus() { + const friendKeys = await window.Signal.Data.getPubKeysWithFriendStatus( + window.friends.friendRequestStatusEnum.friends + ); + await Promise.all( + friendKeys.map(async pubKey => { + try { + await sendOnlineBroadcastMessage(pubKey); + } catch (e) { + log.warn(`Failed to send online broadcast message to ${pubKey}`); + } + }) + ); + } + + async function sendOnlineBroadcastMessage(pubKey) { + // TODO: Make this actually get a loki address rather than junk string + const lokiAddressMessage = new textsecure.protobuf.LokiAddressMessage({ + p2pAddress: 'testAddress', + p2pPort: parseInt(window.localServerPort, 10), + }); + const content = new textsecure.protobuf.Content({ + lokiAddressMessage, + }); + + // will be called once the transmission succeeded or failed + const callback = res => { + if (res.errors.length > 0) { + res.errors.forEach(error => log.error(error)); + } else { + log.info('Online broadcast message sent successfully'); + } + }; + const options = { messageType: 'onlineBroadcast' }; + // Send a empty message with information about how to contact us directly + const outgoingMessage = new textsecure.OutgoingMessage( + null, // server + Date.now(), // timestamp, + [pubKey], // numbers + content, // message + true, // silent + callback, // callback + options + ); + await outgoingMessage.sendToNumber(pubKey); + } + async function sendEmptyMessage(pubKey, sendContentMessage = false) { const options = {}; // send an empty message. @@ -52,5 +99,7 @@ window.libloki.api = { sendFriendRequestAccepted, sendEmptyMessage, + sendOnlineBroadcastMessage, + broadcastOnlineStatus, }; })(); diff --git a/libloki/friends.js b/libloki/friends.js new file mode 100644 index 000000000..95b210382 --- /dev/null +++ b/libloki/friends.js @@ -0,0 +1,22 @@ +/* global window */ + +// eslint-disable-next-line func-names +(function() { + // Possible conversation friend states + const friendRequestStatusEnum = Object.freeze({ + // New conversation, no messages sent or received + none: 0, + // This state is used to lock the input early while sending + pendingSend: 1, + // Friend request sent, awaiting response + requestSent: 2, + // Friend request received, awaiting user input + requestReceived: 3, + // We did it! + friends: 4, + }); + + window.friends = { + friendRequestStatusEnum, + }; +})(); diff --git a/libloki/local_loki_server.js b/libloki/local_loki_server.js index 3bfe40887..ed9bf6c03 100644 --- a/libloki/local_loki_server.js +++ b/libloki/local_loki_server.js @@ -68,6 +68,7 @@ class LocalLokiServer extends EventEmitter { // Async wrapper for http server close close() { + this.removeAllListeners(); if (this.server) { return new Promise(res => { this.server.close(() => res()); diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index e8a2dcb71..5d66d9435 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -23,7 +23,7 @@ function MessageReceiver(username, password, signalingKey, options = {}) { this.username = username; this.password = password; this.lokiMessageAPI = window.LokiMessageAPI; - this.localServer = new window.LocalLokiServer(); + this.localServer = window.LocalLokiServer; if (!options.serverTrustRoot) { throw new Error('Server trust root is required!'); @@ -82,15 +82,11 @@ MessageReceiver.prototype.extend({ } }); - this.localServer.removeAllListeners(); - this.localServer.on('message', this.httpPollingResource.handleMessage); - - // Passing 0 as the port will automatically assign an unused port - this.localServer - .start(0) - .then(port => - window.log.info(`Local Server started at localhost:${port}`) - ); + this.localServer.start(window.localServerPort).then(port => { + window.log.info(`Local Server started at localhost:${port}`); + window.libloki.api.broadcastOnlineStatus(); + this.localServer.on('message', this.httpPollingResource.handleMessage); + }); // TODO: Rework this socket stuff to work with online messaging const useWebSocket = false; @@ -135,7 +131,10 @@ MessageReceiver.prototype.extend({ } if (this.localServer) { - this.localServer.removeAllListeners(); + this.localServer.removeListener( + 'message', + this.httpPollingResource.handleMessage + ); this.localServer = null; } }, @@ -899,6 +898,15 @@ MessageReceiver.prototype.extend({ }) ); }, + async handleLokiAddressMessage(envelope, lokiAddressMessage) { + const { p2pAddress, p2pPort } = lokiAddressMessage; + window.LokiP2pAPI.addContactP2pDetails( + envelope.source, + p2pAddress, + p2pPort + ); + return this.removeFromCache(envelope); + }, handleDataMessage(envelope, msg) { window.log.info('data message from', this.getEnvelopeId(envelope)); let p = Promise.resolve(); @@ -1013,6 +1021,11 @@ MessageReceiver.prototype.extend({ envelope.source, content.preKeyBundleMessage ); + if (content.lokiAddressMessage) + return this.handleLokiAddressMessage( + envelope, + content.lokiAddressMessage + ); if (content.syncMessage) return this.handleSyncMessage(envelope, content.syncMessage); if (content.dataMessage) diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index 668e4067e..885de9250 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -336,8 +336,19 @@ OutgoingMessage.prototype = { dcodeIO.ByteBuffer.wrap(ciphertext.body, 'binary').toArrayBuffer() ); } + let ttl; + if (this.messageType === 'friend-request') { + ttl = 4 * 24 * 60 * 60; // 4 days for friend request message + } else if (this.messageType === 'onlineBroadcast') { + ttl = 10 * 60; // 10 minutes for online broadcast message + } else { + const hours = window.getMessageTTL() || 24; // 1 day default for any other message + ttl = hours * 60 * 60; + } + return { type: ciphertext.type, // FallBackSessionCipher sets this to FRIEND_REQUEST + ttl, ourKey, sourceDevice: 1, destinationRegistrationId: ciphertext.registrationId, @@ -349,17 +360,12 @@ OutgoingMessage.prototype = { // TODO: handle multiple devices/messages per transmit const outgoingObject = outgoingObjects[0]; const socketMessage = await this.wrapInWebsocketMessage(outgoingObject); - let ttl; - if ( - outgoingObject.type === - textsecure.protobuf.Envelope.Type.FRIEND_REQUEST - ) { - ttl = 4 * 24 * 60 * 60; // 4 days for friend request message - } else { - const hours = window.getMessageTTL() || 24; // 1 day default for any other message - ttl = hours * 60 * 60; - } - await this.transmitMessage(number, socketMessage, this.timestamp, ttl); + await this.transmitMessage( + number, + socketMessage, + this.timestamp, + outgoingObject.ttl + ); this.successfulNumbers[this.successfulNumbers.length] = number; this.numberCompleted(); }) diff --git a/main.js b/main.js index 751904140..42445e529 100644 --- a/main.js +++ b/main.js @@ -146,6 +146,7 @@ function prepareURL(pathSegments, moreKeys) { cdnUrl: config.get('cdnUrl'), messageServerPort: config.get('messageServerPort'), swarmServerPort: config.get('swarmServerPort'), + localServerPort: config.get('localServerPort'), certificateAuthority: config.get('certificateAuthority'), environment: config.environment, node_version: process.versions.node, diff --git a/preload.js b/preload.js index 1a7b85b07..67affe39b 100644 --- a/preload.js +++ b/preload.js @@ -276,6 +276,10 @@ window.LokiSnodeAPI = new LokiSnodeAPI({ swarmServerPort: config.swarmServerPort, }); +const { LokiP2pAPI } = require('./js/modules/loki_p2p_api'); + +window.LokiP2pAPI = new LokiP2pAPI(); + const { LokiMessageAPI } = require('./js/modules/loki_message_api'); window.LokiMessageAPI = new LokiMessageAPI({ @@ -285,7 +289,8 @@ window.LokiMessageAPI = new LokiMessageAPI({ const { LocalLokiServer } = require('./libloki/local_loki_server'); -window.LocalLokiServer = LocalLokiServer; +window.localServerPort = config.localServerPort; +window.LocalLokiServer = new LocalLokiServer(); window.mnemonic = require('./libloki/mnemonic'); const { WorkerInterface } = require('./js/modules/util_worker_interface'); diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 61d6fa91d..f71b3b1b1 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -35,6 +35,12 @@ message Content { optional ReceiptMessage receiptMessage = 5; optional TypingMessage typingMessage = 6; optional PreKeyBundleMessage preKeyBundleMessage = 101; + optional LokiAddressMessage lokiAddressMessage = 102; +} + +message LokiAddressMessage { + optional string p2pAddress = 1; + optional uint32 p2pPort = 2; } message PreKeyBundleMessage { diff --git a/test/index.html b/test/index.html index 80e223b1c..c541e717b 100644 --- a/test/index.html +++ b/test/index.html @@ -360,6 +360,7 @@ +