Move handleAttachment and processDecrypted

Make these internal methods on MessageReceiver. Todo: refactor
identity key errors to use a given message receiver.

// FREEBIE
This commit is contained in:
lilia 2015-09-01 16:59:06 -07:00
parent a8f4bac2f7
commit c8a1e090d2
3 changed files with 754 additions and 776 deletions

View file

@ -38628,114 +38628,6 @@ window.textsecure.utils = function() {
return self; return self;
}(); }();
function handleAttachment(attachment) {
function getAttachment() {
return TextSecureServer.getAttachment(attachment.id.toString());
}
function decryptAttachment(encrypted) {
return textsecure.crypto.decryptAttachment(
encrypted,
attachment.key.toArrayBuffer()
);
}
function updateAttachment(data) {
attachment.data = data;
}
return getAttachment().
then(decryptAttachment).
then(updateAttachment);
}
function processDecrypted(decrypted, source) {
// Now that its decrypted, validate the message and clean it up for consumer processing
// Note that messages may (generally) only perform one action and we ignore remaining fields
// after the first action.
if (decrypted.flags == null)
decrypted.flags = 0;
if ((decrypted.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION)
== textsecure.protobuf.DataMessage.Flags.END_SESSION) {
decrypted.body = null;
decrypted.attachments = [];
decrypted.group = null;
return Promise.resolve(decrypted);
}
if (decrypted.flags != 0) {
throw new Error("Unknown flags in message");
}
var promises = [];
if (decrypted.group !== null) {
decrypted.group.id = getString(decrypted.group.id);
if (decrypted.group.type == textsecure.protobuf.GroupContext.Type.UPDATE) {
if (decrypted.group.avatar !== null) {
promises.push(handleAttachment(decrypted.group.avatar));
}
}
promises.push(textsecure.storage.groups.getNumbers(decrypted.group.id).then(function(existingGroup) {
if (existingGroup === undefined) {
if (decrypted.group.type != textsecure.protobuf.GroupContext.Type.UPDATE) {
throw new Error("Got message for unknown group");
}
return textsecure.storage.groups.createNewGroup(decrypted.group.members, decrypted.group.id);
} else {
var fromIndex = existingGroup.indexOf(source);
if (fromIndex < 0) {
//TODO: This could be indication of a race...
throw new Error("Sender was not a member of the group they were sending from");
}
switch(decrypted.group.type) {
case textsecure.protobuf.GroupContext.Type.UPDATE:
return textsecure.storage.groups.updateNumbers(
decrypted.group.id, decrypted.group.members
).then(function(added) {
decrypted.group.added = added;
if (decrypted.group.avatar === null &&
decrypted.group.added.length == 0 &&
decrypted.group.name === null) {
return;
}
decrypted.body = null;
decrypted.attachments = [];
});
break;
case textsecure.protobuf.GroupContext.Type.QUIT:
decrypted.body = null;
decrypted.attachments = [];
return textsecure.storage.groups.removeNumber(decrypted.group.id, source);
case textsecure.protobuf.GroupContext.Type.DELIVER:
decrypted.group.name = null;
decrypted.group.members = [];
decrypted.group.avatar = null;
break;
default:
throw new Error("Unknown group message type");
}
}
}));
}
for (var i in decrypted.attachments) {
promises.push(handleAttachment(decrypted.attachments[i]));
}
return Promise.all(promises).then(function() {
return decrypted;
});
}
/* /*
* vim: ts=4:sw=4:expandtab * vim: ts=4:sw=4:expandtab
@ -39289,303 +39181,400 @@ var TextSecureServer = (function() {
* vim: ts=4:sw=4:expandtab * vim: ts=4:sw=4:expandtab
*/ */
;(function () { function MessageReceiver(url, username, password, signalingKey) {
'use strict'; this.url = url;
window.textsecure = window.textsecure || {}; this.signalingKey = signalingKey;
this.username = username;
this.password = password;
this.server = new TextSecureServer(url, username, password);
function MessageReceiver(url, username, password, signalingKey) { var unencoded = textsecure.utils.unencodeNumber(username);
this.url = url; this.number = unencoded[0];
this.signalingKey = signalingKey; this.deviceId = unencoded[1];
this.username = username; }
this.password = password; MessageReceiver.prototype = {
this.server = new TextSecureServer(url, username, password); constructor: MessageReceiver,
connect: function() {
var unencoded = textsecure.utils.unencodeNumber(username); if (this.socket && this.socket.readyState !== WebSocket.CLOSED) {
this.number = unencoded[0]; this.socket.close();
this.deviceId = unencoded[1]; }
this.connect(); console.log('opening websocket');
} // initialize the socket and start listening for messages
this.socket = this.server.getMessageSocket();
MessageReceiver.prototype = { this.socket.onclose = this.onclose.bind(this);
constructor: MessageReceiver, this.socket.onerror = this.onerror.bind(this);
connect: function() { this.socket.onopen = this.onopen.bind(this);
if (this.socket && this.socket.readyState !== WebSocket.CLOSED) { this.wsr = new WebSocketResource(this.socket, {
this.socket.close(); handleRequest: this.handleRequest.bind(this),
} keepalive: { path: '/v1/keepalive', disconnect: true }
console.log('opening websocket'); });
// initialize the socket and start listening for messages },
this.socket = this.server.getMessageSocket(); close: function() {
this.socket.onclose = this.onclose.bind(this); this.wsr.close();
this.socket.onerror = this.onerror.bind(this); delete this.listeners;
this.socket.onopen = this.onopen.bind(this); },
this.wsr = new WebSocketResource(this.socket, { onopen: function() {
handleRequest: this.handleRequest.bind(this), console.log('websocket open');
keepalive: { path: '/v1/keepalive', disconnect: true } },
}); onerror: function(error) {
}, console.log('websocket error');
close: function() { },
this.wsr.close(); onclose: function(ev) {
delete this.listeners; var eventTarget = this;
}, console.log('websocket closed', ev.code);
onopen: function() { // possible 403 or network issue. Make an request to confirm
console.log('websocket open'); this.server.getDevices(this.number).
}, then(this.connect.bind(this)). // No HTTP error? Reconnect
onerror: function(error) { catch(function(e) {
console.log('websocket error');
},
onclose: function(ev) {
var eventTarget = this;
console.log('websocket closed', ev.code);
// possible 403 or network issue. Make an request to confirm
this.server.getDevices(this.number).
then(this.connect.bind(this)). // No HTTP error? Reconnect
catch(function(e) {
var ev = new Event('error');
ev.error = e;
eventTarget.dispatchEvent(ev);
});
},
handleRequest: function(request) {
this.wsr.resetKeepAliveTimer();
// TODO: handle different types of requests. for now we only expect
// PUT /messages <encrypted IncomingPushMessageSignal>
textsecure.crypto.decryptWebsocketMessage(request.body, this.signalingKey).then(function(plaintext) {
var envelope = textsecure.protobuf.Envelope.decode(plaintext);
// 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
request.respond(200, 'OK');
if (envelope.type === textsecure.protobuf.Envelope.Type.RECEIPT) {
this.onDeliveryReceipt(envelope);
} else if (envelope.content) {
this.handleContentMessage(envelope);
} else if (envelope.legacyMessage) {
this.handleLegacyMessage(envelope);
} else {
throw new Error('Received message with no content and no legacyMessage');
}
}.bind(this)).catch(function(e) {
request.respond(500, 'Bad encrypted websocket message');
console.log("Error handling incoming message:", e);
var ev = new Event('error'); var ev = new Event('error');
ev.error = e; ev.error = e;
this.dispatchEvent(ev); eventTarget.dispatchEvent(ev);
}.bind(this)); });
}, },
getStatus: function() { handleRequest: function(request) {
if (this.socket) { this.wsr.resetKeepAliveTimer();
return this.socket.readyState; // TODO: handle different types of requests. for now we only expect
// PUT /messages <encrypted IncomingPushMessageSignal>
textsecure.crypto.decryptWebsocketMessage(request.body, this.signalingKey).then(function(plaintext) {
var envelope = textsecure.protobuf.Envelope.decode(plaintext);
// 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
request.respond(200, 'OK');
if (envelope.type === textsecure.protobuf.Envelope.Type.RECEIPT) {
this.onDeliveryReceipt(envelope);
} else if (envelope.content) {
this.handleContentMessage(envelope);
} else if (envelope.legacyMessage) {
this.handleLegacyMessage(envelope);
} else { } else {
return -1; throw new Error('Received message with no content and no legacyMessage');
} }
},
onDeliveryReceipt: function (envelope) { }.bind(this)).catch(function(e) {
var ev = new Event('receipt'); request.respond(500, 'Bad encrypted websocket message');
console.log("Error handling incoming message:", e);
var ev = new Event('error');
ev.error = e;
this.dispatchEvent(ev);
}.bind(this));
},
getStatus: function() {
if (this.socket) {
return this.socket.readyState;
} else {
return -1;
}
},
onDeliveryReceipt: function (envelope) {
var ev = new Event('receipt');
ev.proto = envelope;
this.dispatchEvent(ev);
},
decrypt: function(envelope, ciphertext) {
return textsecure.protocol_wrapper.decrypt(
envelope.source,
envelope.sourceDevice,
envelope.type,
ciphertext
).catch(function(error) {
var ev = new Event('error');
ev.error = error;
ev.proto = envelope; ev.proto = envelope;
this.dispatchEvent(ev); this.dispatchEvent(ev);
}, throw error; // reject this promise
decrypt: function(envelope, ciphertext) { }.bind(this));
return textsecure.protocol_wrapper.decrypt( },
envelope.source, handleSentMessage: function(destination, timestamp, message) {
envelope.sourceDevice, return this.processDecrypted(message, this.number).then(function(message) {
envelope.type, var ev = new Event('sent');
ciphertext ev.data = {
).catch(function(error) { destination : destination,
var ev = new Event('error'); timestamp : timestamp.toNumber(),
ev.error = error; message : message
ev.proto = envelope; };
this.dispatchEvent(ev); this.dispatchEvent(ev);
throw error; // reject this promise }.bind(this));
}.bind(this)); },
}, handleDataMessage: function(envelope, message, close_session) {
handleSentMessage: function(destination, timestamp, message) { if ((message.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) ==
return processDecrypted(message, this.number).then(function(message) { textsecure.protobuf.DataMessage.Flags.END_SESSION ) {
var ev = new Event('sent'); close_session();
ev.data = { }
destination : destination, return this.processDecrypted(message, envelope.source).then(function(message) {
timestamp : timestamp.toNumber(), var ev = new Event('message');
message : message ev.data = {
}; source : envelope.source,
this.dispatchEvent(ev); timestamp : envelope.timestamp.toNumber(),
}.bind(this)); message : message
}, };
handleDataMessage: function(envelope, message, close_session) { this.dispatchEvent(ev);
if ((message.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) == }.bind(this));
textsecure.protobuf.DataMessage.Flags.END_SESSION ) { },
close_session(); handleLegacyMessage: function (envelope) {
} return this.decrypt(envelope, envelope.legacyMessage).then(function(result) {
return processDecrypted(message, envelope.source).then(function(message) { var plaintext = result[0]; // array buffer
var ev = new Event('message'); var close_session = result[1]; // function
ev.data = { var message = textsecure.protobuf.DataMessage.decode(plaintext);
source : envelope.source, return this.handleDataMessage(envelope, message, close_session);
timestamp : envelope.timestamp.toNumber(), }.bind(this));
message : message },
}; handleContentMessage: function (envelope) {
this.dispatchEvent(ev); return this.decrypt(envelope, envelope.content).then(function(result) {
}.bind(this)); var plaintext = result[0]; // array buffer
}, var close_session = result[1]; // function
handleLegacyMessage: function (envelope) { var content = textsecure.protobuf.Content.decode(plaintext);
return this.decrypt(envelope, envelope.legacyMessage).then(function(result) { if (content.syncMessage) {
var plaintext = result[0]; // array buffer return this.handleSyncMessage(envelope, content.syncMessage);
var close_session = result[1]; // function } else if (content.dataMessage) {
var message = textsecure.protobuf.DataMessage.decode(plaintext); return this.handleDataMessage(envelope, content.dataMessage, close_session);
return this.handleDataMessage(envelope, message, close_session);
}.bind(this));
},
handleContentMessage: function (envelope) {
return this.decrypt(envelope, envelope.content).then(function(result) {
var plaintext = result[0]; // array buffer
var close_session = result[1]; // function
var content = textsecure.protobuf.Content.decode(plaintext);
if (content.syncMessage) {
return this.handleSyncMessage(envelope, content.syncMessage);
} else if (content.dataMessage) {
return this.handleDataMessage(envelope, content.dataMessage, close_session);
} else {
throw new Error('Got Content message with no dataMessage and no syncMessage');
}
}.bind(this));
},
handleSyncMessage: function(envelope, syncMessage) {
if (envelope.source !== this.number) {
throw new Error('Received sync message from another number');
}
if (envelope.sourceDevice == this.deviceId) {
throw new Error('Received sync message from our own device');
}
if (syncMessage.sent) {
var sentMessage = syncMessage.sent;
return this.handleSentMessage(
sentMessage.destination,
sentMessage.timestamp,
sentMessage.message
);
} else if (syncMessage.contacts) {
this.handleContacts(syncMessage.contacts);
} else if (syncMessage.groups) {
this.handleGroups(syncMessage.groups);
} else { } else {
throw new Error('Got SyncMessage with no sent, contacts, or groups'); throw new Error('Got Content message with no dataMessage and no syncMessage');
} }
}, }.bind(this));
handleContacts: function(contacts) { },
var eventTarget = this; handleSyncMessage: function(envelope, syncMessage) {
var attachmentPointer = contacts.blob; if (envelope.source !== this.number) {
return handleAttachment(attachmentPointer).then(function() { throw new Error('Received sync message from another number');
var contactBuffer = new ContactBuffer(attachmentPointer.data); }
var contactDetails = contactBuffer.next(); if (envelope.sourceDevice == this.deviceId) {
while (contactDetails !== undefined) { throw new Error('Received sync message from our own device');
var ev = new Event('contact'); }
ev.contactDetails = contactDetails; if (syncMessage.sent) {
eventTarget.dispatchEvent(ev); var sentMessage = syncMessage.sent;
contactDetails = contactBuffer.next(); return this.handleSentMessage(
} sentMessage.destination,
eventTarget.dispatchEvent(new Event('contactsync')); sentMessage.timestamp,
}); sentMessage.message
}, );
handleGroups: function(groups) { } else if (syncMessage.contacts) {
var eventTarget = this; this.handleContacts(syncMessage.contacts);
var attachmentPointer = groups.blob; } else if (syncMessage.groups) {
return handleAttachment(attachmentPointer).then(function() { this.handleGroups(syncMessage.groups);
var groupBuffer = new GroupBuffer(attachmentPointer.data); } else {
var groupDetails = groupBuffer.next(); throw new Error('Got SyncMessage with no sent, contacts, or groups');
while (groupDetails !== undefined) { }
(function(groupDetails) { },
groupDetails.id = getString(groupDetails.id); handleContacts: function(contacts) {
textsecure.storage.groups.getGroup(groupDetails.id). var eventTarget = this;
then(function(existingGroup) { var attachmentPointer = contacts.blob;
if (existingGroup === undefined) { return this.handleAttachment(attachmentPointer).then(function() {
return textsecure.storage.groups.createNewGroup( var contactBuffer = new ContactBuffer(attachmentPointer.data);
groupDetails.members, groupDetails.id var contactDetails = contactBuffer.next();
); while (contactDetails !== undefined) {
} else { var ev = new Event('contact');
return textsecure.storage.groups.updateNumbers( ev.contactDetails = contactDetails;
groupDetails.id, groupDetails.members eventTarget.dispatchEvent(ev);
); contactDetails = contactBuffer.next();
}
}).then(function() {
var ev = new Event('group');
ev.groupDetails = groupDetails;
eventTarget.dispatchEvent(ev);
});
})(groupDetails);
groupDetails = groupBuffer.next();
}
});
},
/* Implements EventTarget */
dispatchEvent: function(ev) {
if (!(ev instanceof Event)) {
throw new Error('Expects an event');
} }
if (this.listeners === null || typeof this.listeners !== 'object') { eventTarget.dispatchEvent(new Event('contactsync'));
this.listeners = {}; });
},
handleGroups: function(groups) {
var eventTarget = this;
var attachmentPointer = groups.blob;
return this.handleAttachment(attachmentPointer).then(function() {
var groupBuffer = new GroupBuffer(attachmentPointer.data);
var groupDetails = groupBuffer.next();
while (groupDetails !== undefined) {
(function(groupDetails) {
groupDetails.id = getString(groupDetails.id);
textsecure.storage.groups.getGroup(groupDetails.id).
then(function(existingGroup) {
if (existingGroup === undefined) {
return textsecure.storage.groups.createNewGroup(
groupDetails.members, groupDetails.id
);
} else {
return textsecure.storage.groups.updateNumbers(
groupDetails.id, groupDetails.members
);
}
}).then(function() {
var ev = new Event('group');
ev.groupDetails = groupDetails;
eventTarget.dispatchEvent(ev);
});
})(groupDetails);
groupDetails = groupBuffer.next();
} }
var listeners = this.listeners[ev.type]; });
if (typeof listeners === 'object') { },
for (var i=0; i < listeners.length; ++i) { handleAttachment: function(attachment) {
if (typeof listeners[i] === 'function') { function decryptAttachment(encrypted) {
listeners[i].call(null, ev); return textsecure.crypto.decryptAttachment(
} encrypted,
} attachment.key.toArrayBuffer()
} );
},
addEventListener: function(eventName, callback) {
if (typeof eventName !== 'string') {
throw new Error('First argument expects a string');
}
if (typeof callback !== 'function') {
throw new Error('Second argument expects a function');
}
if (this.listeners === null || typeof this.listeners !== 'object') {
this.listeners = {};
}
var listeners = this.listeners[eventName];
if (typeof listeners !== 'object') {
listeners = [];
}
listeners.push(callback);
this.listeners[eventName] = listeners;
},
removeEventListener: function(eventName, callback) {
if (typeof eventName !== 'string') {
throw new Error('First argument expects a string');
}
if (typeof callback !== 'function') {
throw new Error('Second argument expects a function');
}
if (this.listeners === null || typeof this.listeners !== 'object') {
this.listeners = {};
}
var listeners = this.listeners[eventName];
for (var i=0; i < listeners.length; ++ i) {
if (listeners[i] === callback) {
listeners.splice(i, 1);
return;
}
}
this.listeners[eventName] = listeners;
} }
}; function updateAttachment(data) {
attachment.data = data;
}
textsecure.MessageReceiver = function(url, username, password, signalingKey) { return this.server.getAttachment(attachment.id.toString()).
var messageReceiver = new MessageReceiver(url, username, password, signalingKey); then(decryptAttachment).
then(updateAttachment);
},
processDecrypted: function(decrypted, source) {
// Now that its decrypted, validate the message and clean it up for consumer processing
// Note that messages may (generally) only perform one action and we ignore remaining fields
// after the first action.
this.addEventListener = messageReceiver.addEventListener.bind(messageReceiver); if (decrypted.flags == null)
this.removeEventListener = messageReceiver.removeEventListener.bind(messageReceiver); decrypted.flags = 0;
this.getStatus = messageReceiver.getStatus.bind(messageReceiver);
this.close = messageReceiver.close.bind(messageReceiver);
};
textsecure.MessageReceiver.prototype = { if ((decrypted.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION)
constructor: textsecure.MessageReceiver == textsecure.protobuf.DataMessage.Flags.END_SESSION) {
}; decrypted.body = null;
decrypted.attachments = [];
decrypted.group = null;
return Promise.resolve(decrypted);
}
if (decrypted.flags != 0) {
throw new Error("Unknown flags in message");
}
var promises = [];
if (decrypted.group !== null) {
decrypted.group.id = getString(decrypted.group.id);
if (decrypted.group.type == textsecure.protobuf.GroupContext.Type.UPDATE) {
if (decrypted.group.avatar !== null) {
promises.push(this.handleAttachment(decrypted.group.avatar));
}
}
promises.push(textsecure.storage.groups.getNumbers(decrypted.group.id).then(function(existingGroup) {
if (existingGroup === undefined) {
if (decrypted.group.type != textsecure.protobuf.GroupContext.Type.UPDATE) {
throw new Error("Got message for unknown group");
}
return textsecure.storage.groups.createNewGroup(decrypted.group.members, decrypted.group.id);
} else {
var fromIndex = existingGroup.indexOf(source);
if (fromIndex < 0) {
//TODO: This could be indication of a race...
throw new Error("Sender was not a member of the group they were sending from");
}
switch(decrypted.group.type) {
case textsecure.protobuf.GroupContext.Type.UPDATE:
return textsecure.storage.groups.updateNumbers(
decrypted.group.id, decrypted.group.members
).then(function(added) {
decrypted.group.added = added;
if (decrypted.group.avatar === null &&
decrypted.group.added.length == 0 &&
decrypted.group.name === null) {
return;
}
decrypted.body = null;
decrypted.attachments = [];
});
break;
case textsecure.protobuf.GroupContext.Type.QUIT:
decrypted.body = null;
decrypted.attachments = [];
return textsecure.storage.groups.removeNumber(decrypted.group.id, source);
case textsecure.protobuf.GroupContext.Type.DELIVER:
decrypted.group.name = null;
decrypted.group.members = [];
decrypted.group.avatar = null;
break;
default:
throw new Error("Unknown group message type");
}
}
}));
}
for (var i in decrypted.attachments) {
promises.push(this.handleAttachment(decrypted.attachments[i]));
}
return Promise.all(promises).then(function() {
return decrypted;
});
},
/* Implements EventTarget */
dispatchEvent: function(ev) {
if (!(ev instanceof Event)) {
throw new Error('Expects an event');
}
if (this.listeners === null || typeof this.listeners !== 'object') {
this.listeners = {};
}
var listeners = this.listeners[ev.type];
if (typeof listeners === 'object') {
for (var i=0; i < listeners.length; ++i) {
if (typeof listeners[i] === 'function') {
listeners[i].call(null, ev);
}
}
}
},
addEventListener: function(eventName, callback) {
if (typeof eventName !== 'string') {
throw new Error('First argument expects a string');
}
if (typeof callback !== 'function') {
throw new Error('Second argument expects a function');
}
if (this.listeners === null || typeof this.listeners !== 'object') {
this.listeners = {};
}
var listeners = this.listeners[eventName];
if (typeof listeners !== 'object') {
listeners = [];
}
listeners.push(callback);
this.listeners[eventName] = listeners;
},
removeEventListener: function(eventName, callback) {
if (typeof eventName !== 'string') {
throw new Error('First argument expects a string');
}
if (typeof callback !== 'function') {
throw new Error('Second argument expects a function');
}
if (this.listeners === null || typeof this.listeners !== 'object') {
this.listeners = {};
}
var listeners = this.listeners[eventName];
for (var i=0; i < listeners.length; ++ i) {
if (listeners[i] === callback) {
listeners.splice(i, 1);
return;
}
}
this.listeners[eventName] = listeners;
}
};
window.textsecure = window.textsecure || {};
textsecure.MessageReceiver = function(url, username, password, signalingKey) {
var messageReceiver = new MessageReceiver(url, username, password, signalingKey);
this.addEventListener = messageReceiver.addEventListener.bind(messageReceiver);
this.removeEventListener = messageReceiver.removeEventListener.bind(messageReceiver);
this.getStatus = messageReceiver.getStatus.bind(messageReceiver);
this.close = messageReceiver.close.bind(messageReceiver);
messageReceiver.connect();
};
textsecure.MessageReceiver.prototype = {
constructor: textsecure.MessageReceiver
};
}());
/* /*
* vim: ts=4:sw=4:expandtab * vim: ts=4:sw=4:expandtab

View file

@ -113,111 +113,3 @@ window.textsecure.utils = function() {
return self; return self;
}(); }();
function handleAttachment(attachment) {
function getAttachment() {
return TextSecureServer.getAttachment(attachment.id.toString());
}
function decryptAttachment(encrypted) {
return textsecure.crypto.decryptAttachment(
encrypted,
attachment.key.toArrayBuffer()
);
}
function updateAttachment(data) {
attachment.data = data;
}
return getAttachment().
then(decryptAttachment).
then(updateAttachment);
}
function processDecrypted(decrypted, source) {
// Now that its decrypted, validate the message and clean it up for consumer processing
// Note that messages may (generally) only perform one action and we ignore remaining fields
// after the first action.
if (decrypted.flags == null)
decrypted.flags = 0;
if ((decrypted.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION)
== textsecure.protobuf.DataMessage.Flags.END_SESSION) {
decrypted.body = null;
decrypted.attachments = [];
decrypted.group = null;
return Promise.resolve(decrypted);
}
if (decrypted.flags != 0) {
throw new Error("Unknown flags in message");
}
var promises = [];
if (decrypted.group !== null) {
decrypted.group.id = getString(decrypted.group.id);
if (decrypted.group.type == textsecure.protobuf.GroupContext.Type.UPDATE) {
if (decrypted.group.avatar !== null) {
promises.push(handleAttachment(decrypted.group.avatar));
}
}
promises.push(textsecure.storage.groups.getNumbers(decrypted.group.id).then(function(existingGroup) {
if (existingGroup === undefined) {
if (decrypted.group.type != textsecure.protobuf.GroupContext.Type.UPDATE) {
throw new Error("Got message for unknown group");
}
return textsecure.storage.groups.createNewGroup(decrypted.group.members, decrypted.group.id);
} else {
var fromIndex = existingGroup.indexOf(source);
if (fromIndex < 0) {
//TODO: This could be indication of a race...
throw new Error("Sender was not a member of the group they were sending from");
}
switch(decrypted.group.type) {
case textsecure.protobuf.GroupContext.Type.UPDATE:
return textsecure.storage.groups.updateNumbers(
decrypted.group.id, decrypted.group.members
).then(function(added) {
decrypted.group.added = added;
if (decrypted.group.avatar === null &&
decrypted.group.added.length == 0 &&
decrypted.group.name === null) {
return;
}
decrypted.body = null;
decrypted.attachments = [];
});
break;
case textsecure.protobuf.GroupContext.Type.QUIT:
decrypted.body = null;
decrypted.attachments = [];
return textsecure.storage.groups.removeNumber(decrypted.group.id, source);
case textsecure.protobuf.GroupContext.Type.DELIVER:
decrypted.group.name = null;
decrypted.group.members = [];
decrypted.group.avatar = null;
break;
default:
throw new Error("Unknown group message type");
}
}
}));
}
for (var i in decrypted.attachments) {
promises.push(handleAttachment(decrypted.attachments[i]));
}
return Promise.all(promises).then(function() {
return decrypted;
});
}

View file

@ -2,300 +2,397 @@
* vim: ts=4:sw=4:expandtab * vim: ts=4:sw=4:expandtab
*/ */
;(function () { function MessageReceiver(url, username, password, signalingKey) {
'use strict'; this.url = url;
window.textsecure = window.textsecure || {}; this.signalingKey = signalingKey;
this.username = username;
this.password = password;
this.server = new TextSecureServer(url, username, password);
function MessageReceiver(url, username, password, signalingKey) { var unencoded = textsecure.utils.unencodeNumber(username);
this.url = url; this.number = unencoded[0];
this.signalingKey = signalingKey; this.deviceId = unencoded[1];
this.username = username; }
this.password = password; MessageReceiver.prototype = {
this.server = new TextSecureServer(url, username, password); constructor: MessageReceiver,
connect: function() {
var unencoded = textsecure.utils.unencodeNumber(username); if (this.socket && this.socket.readyState !== WebSocket.CLOSED) {
this.number = unencoded[0]; this.socket.close();
this.deviceId = unencoded[1]; }
this.connect(); console.log('opening websocket');
} // initialize the socket and start listening for messages
this.socket = this.server.getMessageSocket();
MessageReceiver.prototype = { this.socket.onclose = this.onclose.bind(this);
constructor: MessageReceiver, this.socket.onerror = this.onerror.bind(this);
connect: function() { this.socket.onopen = this.onopen.bind(this);
if (this.socket && this.socket.readyState !== WebSocket.CLOSED) { this.wsr = new WebSocketResource(this.socket, {
this.socket.close(); handleRequest: this.handleRequest.bind(this),
} keepalive: { path: '/v1/keepalive', disconnect: true }
console.log('opening websocket'); });
// initialize the socket and start listening for messages },
this.socket = this.server.getMessageSocket(); close: function() {
this.socket.onclose = this.onclose.bind(this); this.wsr.close();
this.socket.onerror = this.onerror.bind(this); delete this.listeners;
this.socket.onopen = this.onopen.bind(this); },
this.wsr = new WebSocketResource(this.socket, { onopen: function() {
handleRequest: this.handleRequest.bind(this), console.log('websocket open');
keepalive: { path: '/v1/keepalive', disconnect: true } },
}); onerror: function(error) {
}, console.log('websocket error');
close: function() { },
this.wsr.close(); onclose: function(ev) {
delete this.listeners; var eventTarget = this;
}, console.log('websocket closed', ev.code);
onopen: function() { // possible 403 or network issue. Make an request to confirm
console.log('websocket open'); this.server.getDevices(this.number).
}, then(this.connect.bind(this)). // No HTTP error? Reconnect
onerror: function(error) { catch(function(e) {
console.log('websocket error');
},
onclose: function(ev) {
var eventTarget = this;
console.log('websocket closed', ev.code);
// possible 403 or network issue. Make an request to confirm
this.server.getDevices(this.number).
then(this.connect.bind(this)). // No HTTP error? Reconnect
catch(function(e) {
var ev = new Event('error');
ev.error = e;
eventTarget.dispatchEvent(ev);
});
},
handleRequest: function(request) {
this.wsr.resetKeepAliveTimer();
// TODO: handle different types of requests. for now we only expect
// PUT /messages <encrypted IncomingPushMessageSignal>
textsecure.crypto.decryptWebsocketMessage(request.body, this.signalingKey).then(function(plaintext) {
var envelope = textsecure.protobuf.Envelope.decode(plaintext);
// 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
request.respond(200, 'OK');
if (envelope.type === textsecure.protobuf.Envelope.Type.RECEIPT) {
this.onDeliveryReceipt(envelope);
} else if (envelope.content) {
this.handleContentMessage(envelope);
} else if (envelope.legacyMessage) {
this.handleLegacyMessage(envelope);
} else {
throw new Error('Received message with no content and no legacyMessage');
}
}.bind(this)).catch(function(e) {
request.respond(500, 'Bad encrypted websocket message');
console.log("Error handling incoming message:", e);
var ev = new Event('error'); var ev = new Event('error');
ev.error = e; ev.error = e;
this.dispatchEvent(ev); eventTarget.dispatchEvent(ev);
}.bind(this)); });
}, },
getStatus: function() { handleRequest: function(request) {
if (this.socket) { this.wsr.resetKeepAliveTimer();
return this.socket.readyState; // TODO: handle different types of requests. for now we only expect
// PUT /messages <encrypted IncomingPushMessageSignal>
textsecure.crypto.decryptWebsocketMessage(request.body, this.signalingKey).then(function(plaintext) {
var envelope = textsecure.protobuf.Envelope.decode(plaintext);
// 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
request.respond(200, 'OK');
if (envelope.type === textsecure.protobuf.Envelope.Type.RECEIPT) {
this.onDeliveryReceipt(envelope);
} else if (envelope.content) {
this.handleContentMessage(envelope);
} else if (envelope.legacyMessage) {
this.handleLegacyMessage(envelope);
} else { } else {
return -1; throw new Error('Received message with no content and no legacyMessage');
} }
},
onDeliveryReceipt: function (envelope) { }.bind(this)).catch(function(e) {
var ev = new Event('receipt'); request.respond(500, 'Bad encrypted websocket message');
console.log("Error handling incoming message:", e);
var ev = new Event('error');
ev.error = e;
this.dispatchEvent(ev);
}.bind(this));
},
getStatus: function() {
if (this.socket) {
return this.socket.readyState;
} else {
return -1;
}
},
onDeliveryReceipt: function (envelope) {
var ev = new Event('receipt');
ev.proto = envelope;
this.dispatchEvent(ev);
},
decrypt: function(envelope, ciphertext) {
return textsecure.protocol_wrapper.decrypt(
envelope.source,
envelope.sourceDevice,
envelope.type,
ciphertext
).catch(function(error) {
var ev = new Event('error');
ev.error = error;
ev.proto = envelope; ev.proto = envelope;
this.dispatchEvent(ev); this.dispatchEvent(ev);
}, throw error; // reject this promise
decrypt: function(envelope, ciphertext) { }.bind(this));
return textsecure.protocol_wrapper.decrypt( },
envelope.source, handleSentMessage: function(destination, timestamp, message) {
envelope.sourceDevice, return this.processDecrypted(message, this.number).then(function(message) {
envelope.type, var ev = new Event('sent');
ciphertext ev.data = {
).catch(function(error) { destination : destination,
var ev = new Event('error'); timestamp : timestamp.toNumber(),
ev.error = error; message : message
ev.proto = envelope; };
this.dispatchEvent(ev); this.dispatchEvent(ev);
throw error; // reject this promise }.bind(this));
}.bind(this)); },
}, handleDataMessage: function(envelope, message, close_session) {
handleSentMessage: function(destination, timestamp, message) { if ((message.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) ==
return processDecrypted(message, this.number).then(function(message) { textsecure.protobuf.DataMessage.Flags.END_SESSION ) {
var ev = new Event('sent'); close_session();
ev.data = { }
destination : destination, return this.processDecrypted(message, envelope.source).then(function(message) {
timestamp : timestamp.toNumber(), var ev = new Event('message');
message : message ev.data = {
}; source : envelope.source,
this.dispatchEvent(ev); timestamp : envelope.timestamp.toNumber(),
}.bind(this)); message : message
}, };
handleDataMessage: function(envelope, message, close_session) { this.dispatchEvent(ev);
if ((message.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) == }.bind(this));
textsecure.protobuf.DataMessage.Flags.END_SESSION ) { },
close_session(); handleLegacyMessage: function (envelope) {
} return this.decrypt(envelope, envelope.legacyMessage).then(function(result) {
return processDecrypted(message, envelope.source).then(function(message) { var plaintext = result[0]; // array buffer
var ev = new Event('message'); var close_session = result[1]; // function
ev.data = { var message = textsecure.protobuf.DataMessage.decode(plaintext);
source : envelope.source, return this.handleDataMessage(envelope, message, close_session);
timestamp : envelope.timestamp.toNumber(), }.bind(this));
message : message },
}; handleContentMessage: function (envelope) {
this.dispatchEvent(ev); return this.decrypt(envelope, envelope.content).then(function(result) {
}.bind(this)); var plaintext = result[0]; // array buffer
}, var close_session = result[1]; // function
handleLegacyMessage: function (envelope) { var content = textsecure.protobuf.Content.decode(plaintext);
return this.decrypt(envelope, envelope.legacyMessage).then(function(result) { if (content.syncMessage) {
var plaintext = result[0]; // array buffer return this.handleSyncMessage(envelope, content.syncMessage);
var close_session = result[1]; // function } else if (content.dataMessage) {
var message = textsecure.protobuf.DataMessage.decode(plaintext); return this.handleDataMessage(envelope, content.dataMessage, close_session);
return this.handleDataMessage(envelope, message, close_session);
}.bind(this));
},
handleContentMessage: function (envelope) {
return this.decrypt(envelope, envelope.content).then(function(result) {
var plaintext = result[0]; // array buffer
var close_session = result[1]; // function
var content = textsecure.protobuf.Content.decode(plaintext);
if (content.syncMessage) {
return this.handleSyncMessage(envelope, content.syncMessage);
} else if (content.dataMessage) {
return this.handleDataMessage(envelope, content.dataMessage, close_session);
} else {
throw new Error('Got Content message with no dataMessage and no syncMessage');
}
}.bind(this));
},
handleSyncMessage: function(envelope, syncMessage) {
if (envelope.source !== this.number) {
throw new Error('Received sync message from another number');
}
if (envelope.sourceDevice == this.deviceId) {
throw new Error('Received sync message from our own device');
}
if (syncMessage.sent) {
var sentMessage = syncMessage.sent;
return this.handleSentMessage(
sentMessage.destination,
sentMessage.timestamp,
sentMessage.message
);
} else if (syncMessage.contacts) {
this.handleContacts(syncMessage.contacts);
} else if (syncMessage.groups) {
this.handleGroups(syncMessage.groups);
} else { } else {
throw new Error('Got SyncMessage with no sent, contacts, or groups'); throw new Error('Got Content message with no dataMessage and no syncMessage');
} }
}, }.bind(this));
handleContacts: function(contacts) { },
var eventTarget = this; handleSyncMessage: function(envelope, syncMessage) {
var attachmentPointer = contacts.blob; if (envelope.source !== this.number) {
return handleAttachment(attachmentPointer).then(function() { throw new Error('Received sync message from another number');
var contactBuffer = new ContactBuffer(attachmentPointer.data); }
var contactDetails = contactBuffer.next(); if (envelope.sourceDevice == this.deviceId) {
while (contactDetails !== undefined) { throw new Error('Received sync message from our own device');
var ev = new Event('contact'); }
ev.contactDetails = contactDetails; if (syncMessage.sent) {
eventTarget.dispatchEvent(ev); var sentMessage = syncMessage.sent;
contactDetails = contactBuffer.next(); return this.handleSentMessage(
} sentMessage.destination,
eventTarget.dispatchEvent(new Event('contactsync')); sentMessage.timestamp,
}); sentMessage.message
}, );
handleGroups: function(groups) { } else if (syncMessage.contacts) {
var eventTarget = this; this.handleContacts(syncMessage.contacts);
var attachmentPointer = groups.blob; } else if (syncMessage.groups) {
return handleAttachment(attachmentPointer).then(function() { this.handleGroups(syncMessage.groups);
var groupBuffer = new GroupBuffer(attachmentPointer.data); } else {
var groupDetails = groupBuffer.next(); throw new Error('Got SyncMessage with no sent, contacts, or groups');
while (groupDetails !== undefined) { }
(function(groupDetails) { },
groupDetails.id = getString(groupDetails.id); handleContacts: function(contacts) {
textsecure.storage.groups.getGroup(groupDetails.id). var eventTarget = this;
then(function(existingGroup) { var attachmentPointer = contacts.blob;
if (existingGroup === undefined) { return this.handleAttachment(attachmentPointer).then(function() {
return textsecure.storage.groups.createNewGroup( var contactBuffer = new ContactBuffer(attachmentPointer.data);
groupDetails.members, groupDetails.id var contactDetails = contactBuffer.next();
); while (contactDetails !== undefined) {
} else { var ev = new Event('contact');
return textsecure.storage.groups.updateNumbers( ev.contactDetails = contactDetails;
groupDetails.id, groupDetails.members eventTarget.dispatchEvent(ev);
); contactDetails = contactBuffer.next();
}
}).then(function() {
var ev = new Event('group');
ev.groupDetails = groupDetails;
eventTarget.dispatchEvent(ev);
});
})(groupDetails);
groupDetails = groupBuffer.next();
}
});
},
/* Implements EventTarget */
dispatchEvent: function(ev) {
if (!(ev instanceof Event)) {
throw new Error('Expects an event');
} }
if (this.listeners === null || typeof this.listeners !== 'object') { eventTarget.dispatchEvent(new Event('contactsync'));
this.listeners = {}; });
},
handleGroups: function(groups) {
var eventTarget = this;
var attachmentPointer = groups.blob;
return this.handleAttachment(attachmentPointer).then(function() {
var groupBuffer = new GroupBuffer(attachmentPointer.data);
var groupDetails = groupBuffer.next();
while (groupDetails !== undefined) {
(function(groupDetails) {
groupDetails.id = getString(groupDetails.id);
textsecure.storage.groups.getGroup(groupDetails.id).
then(function(existingGroup) {
if (existingGroup === undefined) {
return textsecure.storage.groups.createNewGroup(
groupDetails.members, groupDetails.id
);
} else {
return textsecure.storage.groups.updateNumbers(
groupDetails.id, groupDetails.members
);
}
}).then(function() {
var ev = new Event('group');
ev.groupDetails = groupDetails;
eventTarget.dispatchEvent(ev);
});
})(groupDetails);
groupDetails = groupBuffer.next();
} }
var listeners = this.listeners[ev.type]; });
if (typeof listeners === 'object') { },
for (var i=0; i < listeners.length; ++i) { handleAttachment: function(attachment) {
if (typeof listeners[i] === 'function') { function decryptAttachment(encrypted) {
listeners[i].call(null, ev); return textsecure.crypto.decryptAttachment(
} encrypted,
} attachment.key.toArrayBuffer()
} );
},
addEventListener: function(eventName, callback) {
if (typeof eventName !== 'string') {
throw new Error('First argument expects a string');
}
if (typeof callback !== 'function') {
throw new Error('Second argument expects a function');
}
if (this.listeners === null || typeof this.listeners !== 'object') {
this.listeners = {};
}
var listeners = this.listeners[eventName];
if (typeof listeners !== 'object') {
listeners = [];
}
listeners.push(callback);
this.listeners[eventName] = listeners;
},
removeEventListener: function(eventName, callback) {
if (typeof eventName !== 'string') {
throw new Error('First argument expects a string');
}
if (typeof callback !== 'function') {
throw new Error('Second argument expects a function');
}
if (this.listeners === null || typeof this.listeners !== 'object') {
this.listeners = {};
}
var listeners = this.listeners[eventName];
for (var i=0; i < listeners.length; ++ i) {
if (listeners[i] === callback) {
listeners.splice(i, 1);
return;
}
}
this.listeners[eventName] = listeners;
} }
}; function updateAttachment(data) {
attachment.data = data;
}
textsecure.MessageReceiver = function(url, username, password, signalingKey) { return this.server.getAttachment(attachment.id.toString()).
var messageReceiver = new MessageReceiver(url, username, password, signalingKey); then(decryptAttachment).
then(updateAttachment);
},
processDecrypted: function(decrypted, source) {
// Now that its decrypted, validate the message and clean it up for consumer processing
// Note that messages may (generally) only perform one action and we ignore remaining fields
// after the first action.
this.addEventListener = messageReceiver.addEventListener.bind(messageReceiver); if (decrypted.flags == null)
this.removeEventListener = messageReceiver.removeEventListener.bind(messageReceiver); decrypted.flags = 0;
this.getStatus = messageReceiver.getStatus.bind(messageReceiver);
this.close = messageReceiver.close.bind(messageReceiver);
};
textsecure.MessageReceiver.prototype = { if ((decrypted.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION)
constructor: textsecure.MessageReceiver == textsecure.protobuf.DataMessage.Flags.END_SESSION) {
}; decrypted.body = null;
decrypted.attachments = [];
decrypted.group = null;
return Promise.resolve(decrypted);
}
if (decrypted.flags != 0) {
throw new Error("Unknown flags in message");
}
var promises = [];
if (decrypted.group !== null) {
decrypted.group.id = getString(decrypted.group.id);
if (decrypted.group.type == textsecure.protobuf.GroupContext.Type.UPDATE) {
if (decrypted.group.avatar !== null) {
promises.push(this.handleAttachment(decrypted.group.avatar));
}
}
promises.push(textsecure.storage.groups.getNumbers(decrypted.group.id).then(function(existingGroup) {
if (existingGroup === undefined) {
if (decrypted.group.type != textsecure.protobuf.GroupContext.Type.UPDATE) {
throw new Error("Got message for unknown group");
}
return textsecure.storage.groups.createNewGroup(decrypted.group.members, decrypted.group.id);
} else {
var fromIndex = existingGroup.indexOf(source);
if (fromIndex < 0) {
//TODO: This could be indication of a race...
throw new Error("Sender was not a member of the group they were sending from");
}
switch(decrypted.group.type) {
case textsecure.protobuf.GroupContext.Type.UPDATE:
return textsecure.storage.groups.updateNumbers(
decrypted.group.id, decrypted.group.members
).then(function(added) {
decrypted.group.added = added;
if (decrypted.group.avatar === null &&
decrypted.group.added.length == 0 &&
decrypted.group.name === null) {
return;
}
decrypted.body = null;
decrypted.attachments = [];
});
break;
case textsecure.protobuf.GroupContext.Type.QUIT:
decrypted.body = null;
decrypted.attachments = [];
return textsecure.storage.groups.removeNumber(decrypted.group.id, source);
case textsecure.protobuf.GroupContext.Type.DELIVER:
decrypted.group.name = null;
decrypted.group.members = [];
decrypted.group.avatar = null;
break;
default:
throw new Error("Unknown group message type");
}
}
}));
}
for (var i in decrypted.attachments) {
promises.push(this.handleAttachment(decrypted.attachments[i]));
}
return Promise.all(promises).then(function() {
return decrypted;
});
},
/* Implements EventTarget */
dispatchEvent: function(ev) {
if (!(ev instanceof Event)) {
throw new Error('Expects an event');
}
if (this.listeners === null || typeof this.listeners !== 'object') {
this.listeners = {};
}
var listeners = this.listeners[ev.type];
if (typeof listeners === 'object') {
for (var i=0; i < listeners.length; ++i) {
if (typeof listeners[i] === 'function') {
listeners[i].call(null, ev);
}
}
}
},
addEventListener: function(eventName, callback) {
if (typeof eventName !== 'string') {
throw new Error('First argument expects a string');
}
if (typeof callback !== 'function') {
throw new Error('Second argument expects a function');
}
if (this.listeners === null || typeof this.listeners !== 'object') {
this.listeners = {};
}
var listeners = this.listeners[eventName];
if (typeof listeners !== 'object') {
listeners = [];
}
listeners.push(callback);
this.listeners[eventName] = listeners;
},
removeEventListener: function(eventName, callback) {
if (typeof eventName !== 'string') {
throw new Error('First argument expects a string');
}
if (typeof callback !== 'function') {
throw new Error('Second argument expects a function');
}
if (this.listeners === null || typeof this.listeners !== 'object') {
this.listeners = {};
}
var listeners = this.listeners[eventName];
for (var i=0; i < listeners.length; ++ i) {
if (listeners[i] === callback) {
listeners.splice(i, 1);
return;
}
}
this.listeners[eventName] = listeners;
}
};
window.textsecure = window.textsecure || {};
textsecure.MessageReceiver = function(url, username, password, signalingKey) {
var messageReceiver = new MessageReceiver(url, username, password, signalingKey);
this.addEventListener = messageReceiver.addEventListener.bind(messageReceiver);
this.removeEventListener = messageReceiver.removeEventListener.bind(messageReceiver);
this.getStatus = messageReceiver.getStatus.bind(messageReceiver);
this.close = messageReceiver.close.bind(messageReceiver);
messageReceiver.connect();
};
textsecure.MessageReceiver.prototype = {
constructor: textsecure.MessageReceiver
};
}());