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.
session-desktop/libtextsecure/message_receiver.js

300 lines
8.5 KiB
JavaScript

/* global window: false */
/* global callWorker: false */
/* global textsecure: false */
/* global libsignal: false */
/* global WebSocket: false */
/* global Event: false */
/* global dcodeIO: false */
/* global lokiPublicChatAPI: false */
/* global feeds: false */
/* global WebAPI: false */
/* eslint-disable more/no-then */
/* eslint-disable no-unreachable */
let openGroupBound = false;
function MessageReceiver(username, password, signalingKey) {
this.count = 0;
this.signalingKey = signalingKey;
this.username = username;
this.password = password;
this.server = WebAPI.connect();
const address = libsignal.SignalProtocolAddress.fromString(username);
this.number = address.getName();
this.deviceId = address.getDeviceId();
this.pending = Promise.resolve();
// only do this once to prevent duplicates
if (lokiPublicChatAPI) {
window.log.info('Binding open group events handler', openGroupBound);
if (!openGroupBound) {
// clear any previous binding
lokiPublicChatAPI.removeAllListeners('publicMessage');
// we only need one MR in the system handling these
// bind events
lokiPublicChatAPI.on(
'publicMessage',
this.handleUnencryptedMessage.bind(this)
);
openGroupBound = true;
}
} else {
window.log.warn('Can not handle open group data, API is not available');
}
}
MessageReceiver.stringToArrayBuffer = string =>
Promise.resolve(dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer());
MessageReceiver.arrayBufferToString = arrayBuffer =>
Promise.resolve(dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary'));
MessageReceiver.stringToArrayBufferBase64 = string =>
callWorker('stringToArrayBufferBase64', string);
MessageReceiver.arrayBufferToStringBase64 = arrayBuffer =>
callWorker('arrayBufferToStringBase64', arrayBuffer);
MessageReceiver.prototype = new textsecure.EventTarget();
MessageReceiver.prototype.extend({
constructor: MessageReceiver,
connect() {
if (this.calledClose) {
return;
}
this.count = 0;
if (this.hasConnected) {
const ev = new Event('reconnect');
this.dispatchEvent(ev);
}
this.hasConnected = true;
// start polling all open group rooms you have registered
// if not registered yet, they'll get started when they're created
if (lokiPublicChatAPI) {
lokiPublicChatAPI.open();
}
// set up pollers for any RSS feeds
feeds.forEach(feed => {
feed.on('rssMessage', this.handleUnencryptedMessage.bind(this));
});
// Ensures that an immediate 'empty' event from the websocket will fire only after
// all cached envelopes are processed.
this.incoming = [this.pending];
},
async handleUnencryptedMessage({ message }) {
const isMe = message.source === textsecure.storage.user.getNumber();
if (!isMe && message.message.profile) {
const conversation = await window.ConversationController.getOrCreateAndWait(
message.source,
'private'
);
await window.NewReceiver.updateProfile(
conversation,
message.message.profile,
message.message.profileKey
);
}
const ourNumber = textsecure.storage.user.getNumber();
const primaryDevice = window.storage.get('primaryDevicePubKey');
const isOurDevice =
message.source &&
(message.source === ourNumber || message.source === primaryDevice);
const isPublicChatMessage =
message.message.group &&
message.message.group.id &&
!!message.message.group.id.match(/^publicChat:/);
let ev;
if (isPublicChatMessage && isOurDevice) {
// Public chat messages from ourselves should be outgoing
ev = new Event('sent');
} else {
ev = new Event('message');
}
ev.confirm = function confirmTerm() {};
ev.data = message;
this.dispatchAndWait(ev);
},
stopProcessing() {
window.log.info('MessageReceiver: stopProcessing requested');
this.stoppingProcessing = true;
return this.close();
},
shutdown() {},
async close() {
window.log.info('MessageReceiver.close()');
this.calledClose = true;
// stop polling all open group rooms
if (lokiPublicChatAPI) {
await lokiPublicChatAPI.close();
}
return this.drain();
},
onopen() {
window.log.info('websocket open');
},
onerror() {
window.log.error('websocket error');
},
dispatchAndWait(event) {
const promise = this.appPromise || Promise.resolve();
const appJobPromise = Promise.all(this.dispatchEvent(event));
const job = () => appJobPromise;
this.appPromise = promise.then(job, job);
return Promise.resolve();
},
onclose(ev) {
window.log.info(
'websocket closed',
ev.code,
ev.reason || '',
'calledClose:',
this.calledClose
);
},
onEmpty() {
const { incoming } = this;
this.incoming = [];
const emitEmpty = () => {
window.log.info("MessageReceiver: emitting 'empty' event");
const ev = new Event('empty');
this.dispatchAndWait(ev);
};
const waitForApplication = async () => {
window.log.info(
"MessageReceiver: finished processing messages after 'empty', now waiting for application"
);
const promise = this.appPromise || Promise.resolve();
this.appPromise = Promise.resolve();
// We don't await here because we don't this to gate future message processing
promise.then(emitEmpty, emitEmpty);
};
const waitForEmptyQueue = () => {
// resetting count to zero so everything queued after this starts over again
this.count = 0;
this.addToQueue(waitForApplication);
};
// We first wait for all recently-received messages (this.incoming) to be queued,
// then we queue a task to wait for the application to finish its processing, then
// finally we emit the 'empty' event to the queue.
Promise.all(incoming).then(waitForEmptyQueue, waitForEmptyQueue);
},
drain() {
const { incoming } = this;
this.incoming = [];
// This promise will resolve when there are no more messages to be processed.
return Promise.all(incoming);
},
updateProgress(count) {
// count by 10s
if (count % 10 !== 0) {
return;
}
const ev = new Event('progress');
ev.count = count;
this.dispatchEvent(ev);
},
getStatus() {
if (this.hasConnected) {
return WebSocket.CLOSED;
}
return -1;
},
unpad(paddedData) {
const paddedPlaintext = new Uint8Array(paddedData);
let plaintext;
for (let i = paddedPlaintext.length - 1; i >= 0; i -= 1) {
if (paddedPlaintext[i] === 0x80) {
plaintext = new Uint8Array(i);
plaintext.set(paddedPlaintext.subarray(0, i));
plaintext = plaintext.buffer;
break;
} else if (paddedPlaintext[i] !== 0x00) {
throw new Error('Invalid padding');
}
}
return plaintext;
},
async decryptPreKeyWhisperMessage(ciphertext, sessionCipher, address) {
const padded = await sessionCipher.decryptPreKeyWhisperMessage(ciphertext);
try {
return this.unpad(padded);
} catch (e) {
if (e.message === 'Unknown identity key') {
// create an error that the UI will pick up and ask the
// user if they want to re-negotiate
const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);
throw new textsecure.IncomingIdentityKeyError(
address.toString(),
buffer.toArrayBuffer(),
e.identityKey
);
}
throw e;
}
},
});
window.textsecure = window.textsecure || {};
textsecure.MessageReceiver = function MessageReceiverWrapper(
username,
password,
signalingKey,
options
) {
const messageReceiver = new MessageReceiver(
username,
password,
signalingKey,
options
);
this.addEventListener = messageReceiver.addEventListener.bind(
messageReceiver
);
this.removeEventListener = messageReceiver.removeEventListener.bind(
messageReceiver
);
this.getStatus = messageReceiver.getStatus.bind(messageReceiver);
this.close = messageReceiver.close.bind(messageReceiver);
this.stopProcessing = messageReceiver.stopProcessing.bind(messageReceiver);
messageReceiver.connect();
};
textsecure.MessageReceiver.prototype = {
constructor: textsecure.MessageReceiver,
};
textsecure.MessageReceiver.stringToArrayBuffer =
MessageReceiver.stringToArrayBuffer;
textsecure.MessageReceiver.arrayBufferToString =
MessageReceiver.arrayBufferToString;
textsecure.MessageReceiver.stringToArrayBufferBase64 =
MessageReceiver.stringToArrayBufferBase64;
textsecure.MessageReceiver.arrayBufferToStringBase64 =
MessageReceiver.arrayBufferToStringBase64;