From 1ccf3b6b9517d0e5de71d262f6caa997cf904207 Mon Sep 17 00:00:00 2001 From: Beaudan Date: Wed, 7 Nov 2018 11:49:56 +1100 Subject: [PATCH 1/2] Added function to loki_message_api to do a single request call to storage server. Currently just gets called once when the application starts --- js/modules/loki_message_api.js | 47 +++++++++++++++++++++++++++++++ libtextsecure/message_receiver.js | 11 ++++++-- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index 4bd984ba8..9ce734790 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -20,6 +20,7 @@ function initialize({ url }) { function connect() { return { sendMessage, + retrieveMessages, }; function getPoWNonce(timestamp, ttl, pubKey, data) { @@ -52,6 +53,52 @@ function initialize({ url }) { }); } + async function retrieveMessages(pubKey) { + const options = { + url: `${url}/retrieve`, + type: 'GET', + responseType: undefined, + timeout: undefined, + }; + + log.info(options.type, options.url); + + const fetchOptions = { + method: options.type, + headers: { + 'X-Loki-recipient': pubKey, + }, + timeout: options.timeout, + }; + + let response; + try { + response = await fetch(options.url, fetchOptions); + } catch (e) { + log.error(options.type, options.url, 0, 'Error'); + throw HTTPError('fetch error', 0, e.toString()); + } + + let result; + if ( + options.responseType === 'json' && + response.headers.get('Content-Type') === 'application/json' + ) { + result = await response.json(); + } else if (options.responseType === 'arraybuffer') { + result = await response.buffer(); + } else { + result = await response.text(); + } + + if (response.status >= 0 && response.status < 400) { + log.info(options.type, options.url, response.status, 'Success'); + return [result, response.status]; + } + log.error(options.type, options.url, response.status, 'Error'); + throw HTTPError('retrieveMessages: error response', response.status, result); + } + async function sendMessage(pubKey, data, ttl) { const timestamp = Math.floor(Date.now() / 1000); // Nonce is returned as a base64 string to include in header diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 6a072a616..e462aa74e 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -121,7 +121,7 @@ function MessageReceiver(username, password, signalingKey, options = {}) { this.signalingKey = signalingKey; this.username = username; this.password = password; - this.server = WebAPI.connect({ username, password }); + this.lokiserver = window.LokiAPI.connect(); const address = libsignal.SignalProtocolAddress.fromString(username); this.number = address.getName(); @@ -147,7 +147,7 @@ MessageReceiver.arrayBufferToStringBase64 = arrayBuffer => MessageReceiver.prototype = new textsecure.EventTarget(); MessageReceiver.prototype.extend({ constructor: MessageReceiver, - connect() { + async connect() { if (this.calledClose) { return; } @@ -159,7 +159,12 @@ MessageReceiver.prototype.extend({ } this.hasConnected = true; + const myKeys = await textsecure.storage.protocol.getIdentityKeyPair(); + const result = await this.lokiserver.retrieveMessages(myKeys); + return; + + // TODO: Rework this socket stuff to work with online messaging if (this.socket && this.socket.readyState !== WebSocket.CLOSED) { this.socket.close(); this.wsr.close(); @@ -1136,6 +1141,8 @@ MessageReceiver.prototype.extend({ return textsecure.storage.get('blocked-groups', []).indexOf(groupId) >= 0; }, handleAttachment(attachment) { + console.log("Not handling attachments."); + return; // eslint-disable-next-line no-param-reassign attachment.id = attachment.id.toString(); // eslint-disable-next-line no-param-reassign From c59b196487111623753c8503f4fb191abc06841b Mon Sep 17 00:00:00 2001 From: Beaudan Date: Thu, 8 Nov 2018 14:28:48 +1100 Subject: [PATCH 2/2] Now reading messages and adding them to conversations. Some cleaning of unreachable code. Modified the message data to be encoded as base64 string before sending to server Sending our public key in header of message Now attaching our key to the source field when sending messages, allows messages to be decrypted with the fallback cypher Now polling the server for messages every 5 seconds Sending the source device with messages Added mock respond function to request to leave it that same as the websocket stuff. RetrieveMessages now just returns the result Polling now continues if the server responds with an error. Returning only the result from sendMessage and retrieveMessages Revert commenting of unreachable code Refactored http logic into own file Revert a change to websocket-resources --- Gruntfile.js | 1 + js/modules/loki_message_api.js | 17 ++++--- libloki/proof-of-work.js | 12 ++--- libtextsecure/http-resources.js | 83 +++++++++++++++++++++++++++++++ libtextsecure/message_receiver.js | 20 ++++---- libtextsecure/outgoing_message.js | 16 +++--- 6 files changed, 114 insertions(+), 35 deletions(-) create mode 100644 libtextsecure/http-resources.js diff --git a/Gruntfile.js b/Gruntfile.js index a1ccc8082..5c392b946 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -76,6 +76,7 @@ module.exports = grunt => { 'libtextsecure/event_target.js', 'libtextsecure/account_manager.js', 'libtextsecure/websocket-resources.js', + 'libtextsecure/http-resources.js', 'libtextsecure/message_receiver.js', 'libtextsecure/outgoing_message.js', 'libtextsecure/sendmessage.js', diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index 9ce734790..ebc4917a5 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -1,4 +1,4 @@ -/* global log */ +/* global log, dcodeIO */ const fetch = require('node-fetch'); const is = require('@sindresorhus/is'); @@ -33,7 +33,7 @@ function initialize({ url }) { timestamp, ttl, pubKey, - data: Array.from(data), + data, }); // Handle child process error (should never happen) @@ -57,7 +57,7 @@ function initialize({ url }) { const options = { url: `${url}/retrieve`, type: 'GET', - responseType: undefined, + responseType: 'json', timeout: undefined, }; @@ -93,18 +93,20 @@ function initialize({ url }) { if (response.status >= 0 && response.status < 400) { log.info(options.type, options.url, response.status, 'Success'); - return [result, response.status]; + return result; } log.error(options.type, options.url, response.status, 'Error'); throw HTTPError('retrieveMessages: error response', response.status, result); } async function sendMessage(pubKey, data, ttl) { + const data64 = dcodeIO.ByteBuffer.wrap(data).toString('base64'); + const timestamp = Math.floor(Date.now() / 1000); // Nonce is returned as a base64 string to include in header let nonce; try { - nonce = await getPoWNonce(timestamp, ttl, pubKey, data); + nonce = await getPoWNonce(timestamp, ttl, pubKey, data64); } catch (err) { // Something went horribly wrong // TODO: Handle gracefully @@ -122,13 +124,12 @@ function initialize({ url }) { const fetchOptions = { method: options.type, - body: data, + body: data64, headers: { 'X-Loki-pow-nonce': nonce, 'X-Loki-timestamp': timestamp.toString(), 'X-Loki-ttl': ttl.toString(), 'X-Loki-recipient': pubKey, - 'Content-Length': data.byteLength, }, timeout: options.timeout, }; @@ -155,7 +156,7 @@ function initialize({ url }) { if (response.status >= 0 && response.status < 400) { log.info(options.type, options.url, response.status, 'Success'); - return [result, response.status]; + return result; } log.error(options.type, options.url, response.status, 'Error'); throw HTTPError('sendMessage: error response', response.status, result); diff --git a/libloki/proof-of-work.js b/libloki/proof-of-work.js index 01a413955..e9bdafb1f 100644 --- a/libloki/proof-of-work.js +++ b/libloki/proof-of-work.js @@ -58,15 +58,9 @@ function greaterThan(arr1, arr2) { // Return nonce that hashes together with payload lower than the target function calcPoW(timestamp, ttl, pubKey, data) { - const leadingString = timestamp.toString() + ttl.toString() + pubKey; - const leadingArray = new Uint8Array( - bb.wrap(leadingString, 'binary').toArrayBuffer() + const payload = new Uint8Array( + bb.wrap(timestamp.toString() + ttl.toString() + pubKey + data, 'binary').toArrayBuffer() ); - // Payload constructed from concatenating timestamp, ttl and pubkey strings, - // converting to Uint8Array and then appending to the message data array - const payload = new Uint8Array(leadingArray.length + data.length); - payload.set(leadingArray); - payload.set(data, leadingArray.length); // payloadLength + NONCE_LEN const totalLen = new BigInteger(payload.length.toString()).add( @@ -118,7 +112,7 @@ process.on('message', msg => { msg.timestamp, msg.ttl, msg.pubKey, - new Uint8Array(msg.data) + msg.data ), }); }); diff --git a/libtextsecure/http-resources.js b/libtextsecure/http-resources.js new file mode 100644 index 000000000..8dc4212a1 --- /dev/null +++ b/libtextsecure/http-resources.js @@ -0,0 +1,83 @@ +/* global window, dcodeIO, textsecure, StringView */ + +// eslint-disable-next-line func-names +(function() { + let server; + + function stringToArrayBufferBase64(string) { + return dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer(); + } + + const Response = function Response(options) { + this.verb = options.verb || options.type; + this.path = options.path || options.url; + this.body = options.body || options.data; + this.success = options.success; + this.error = options.error; + this.id = options.id; + + if (this.id === undefined) { + const bits = new Uint32Array(2); + window.crypto.getRandomValues(bits); + this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true); + } + + if (this.body === undefined) { + this.body = null; + } + }; + + const IncomingHttpResponse = function IncomingHttpResponse(options) { + const request = new Response(options); + + this.verb = request.verb; + this.path = request.path; + this.body = request.body; + + this.respond = (status, message) => { + // Mock websocket response + window.log.info(status, message); + }; + }; + + + window.HttpResource = function HttpResource(_server, opts = {}) { + server = _server; + let { handleRequest } = opts; + if (typeof handleRequest !== 'function') { + handleRequest = request => request.respond(404, 'Not found'); + }; + + this.startPolling = async function pollServer() { + const myKeys = await textsecure.storage.protocol.getIdentityKeyPair(); + const pubKey = StringView.arrayBufferToHex(myKeys.pubKey) + let result; + try { + result = await server.retrieveMessages(pubKey); + } catch(err) { + setTimeout(() => { pollServer(); }, 5000); + return; + } + if (!result.messages) { + setTimeout(() => { pollServer(); }, 5000); + return; + } + result.messages.forEach(async message => { + const { data } = message; + const dataPlaintext = stringToArrayBufferBase64(data); + const messageBuf = textsecure.protobuf.WebSocketMessage.decode(dataPlaintext); + if (messageBuf.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST) { + handleRequest( + new IncomingHttpResponse({ + verb: messageBuf.request.verb, + path: messageBuf.request.path, + body: messageBuf.request.body, + id: messageBuf.request.id, + }) + ); + } + }); + setTimeout(() => { pollServer(); }, 5000); + }; + }; +})(); diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index e462aa74e..26ec30507 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -1,12 +1,12 @@ /* global window: false */ /* global textsecure: false */ -/* global WebAPI: false */ +/* global StringView: false */ /* global libsignal: false */ -/* global WebSocketResource: false */ /* global WebSocket: false */ /* global Event: false */ /* global dcodeIO: false */ /* global _: false */ +/* global HttpResource: false */ /* global ContactBuffer: false */ /* global GroupBuffer: false */ /* global Worker: false */ @@ -147,7 +147,7 @@ MessageReceiver.arrayBufferToStringBase64 = arrayBuffer => MessageReceiver.prototype = new textsecure.EventTarget(); MessageReceiver.prototype.extend({ constructor: MessageReceiver, - async connect() { + connect() { if (this.calledClose) { return; } @@ -159,12 +159,13 @@ MessageReceiver.prototype.extend({ } this.hasConnected = true; - const myKeys = await textsecure.storage.protocol.getIdentityKeyPair(); - const result = await this.lokiserver.retrieveMessages(myKeys); - + this.hr = new HttpResource(this.lokiserver, { + handleRequest: this.handleRequest.bind(this), + }); + this.hr.startPolling(); + // TODO: Rework this socket stuff to work with online messaging return; - // TODO: Rework this socket stuff to work with online messaging if (this.socket && this.socket.readyState !== WebSocket.CLOSED) { this.socket.close(); this.wsr.close(); @@ -234,7 +235,6 @@ MessageReceiver.prototype.extend({ ); // TODO: handle properly return; - this.shutdown(); if (this.calledClose) { @@ -274,8 +274,8 @@ MessageReceiver.prototype.extend({ return; } - const promise = Promise.resolve(request.body.toArrayBuffer()) //textsecure.crypto - //.decryptWebsocketMessage(request.body, this.signalingKey) + const promise = Promise.resolve(request.body.toArrayBuffer()) // textsecure.crypto + // .decryptWebsocketMessage(request.body, this.signalingKey) .then(plaintext => { const envelope = textsecure.protobuf.Envelope.decode(plaintext); // After this point, decoding errors are not the server's diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index ca1b82180..f00fc7136 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -166,8 +166,8 @@ OutgoingMessage.prototype = { async transmitMessage(number, data, timestamp, ttl = 24 * 60 * 60) { const pubKey = number; try { - const [response] = await this.lokiserver.sendMessage(pubKey, data, ttl); - return response; + const result = await this.lokiserver.sendMessage(pubKey, data, ttl); + return result; } catch (e) { if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) { // 409 and 410 should bubble and be handled by doSendMessage @@ -209,8 +209,8 @@ OutgoingMessage.prototype = { async wrapInWebsocketMessage(outgoingObject) { const messageEnvelope = new textsecure.protobuf.Envelope({ type: outgoingObject.type, - source: outgoingObject.address.getName(), - sourceDevice: outgoingObject.address.getDeviceId(), + source: outgoingObject.ourKey, + sourceDevice: outgoingObject.sourceDevice, timestamp: this.timestamp, content: outgoingObject.content, }); @@ -236,11 +236,11 @@ OutgoingMessage.prototype = { deviceIds.map(async deviceId => { const address = new libsignal.SignalProtocolAddress(number, deviceId); - const ourNumber = textsecure.storage.user.getNumber(); + const ourKey = textsecure.storage.user.getNumber(); const options = {}; // No limit on message keys if we're communicating with our other devices - if (ourNumber === number) { + if (ourKey === number) { options.messageKeysLimit = false; } @@ -270,8 +270,8 @@ OutgoingMessage.prototype = { }) .then(ciphertext => ({ type: ciphertext.type, - address, - destinationDeviceId: address.getDeviceId(), + ourKey, + sourceDevice: 1, destinationRegistrationId: ciphertext.registrationId, content: ciphertext.body, }));