Compare commits

..

44 Commits

Author SHA1 Message Date
David Baker 02f8e7da3d 0.6.2 2016-10-05 16:43:11 +01:00
David Baker a245b735b3 Prepare changelog for v0.6.2 2016-10-05 16:39:48 +01:00
Richard van der Hoff 0f71983cb9 Merge pull request #221 from matrix-org/dbkr/check_dependencies
Check dependencies aren't on develop in release.sh
2016-10-05 16:38:41 +01:00
David Baker 7db6b9e490 Check dependencies aren't on develop in release.sh 2016-10-05 16:34:36 +01:00
David Baker 0021b21170 Merge pull request #220 from matrix-org/rav/fix_turnserver_leak
Fix checkTurnServers leak on logout
2016-10-03 11:24:25 +01:00
David Baker 3080dc018a Merge pull request #219 from matrix-org/rav/refactor_httpapi
Fix leak of file upload objects
2016-10-03 11:18:02 +01:00
David Baker f5d0ec32e5 Merge pull request #218 from matrix-org/rav/deduplicate_storage_update
crypto: remove duplicate code
2016-10-03 11:01:33 +01:00
Richard van der Hoff 6b3a06a8ed Fix checkTurnServers leak on logout
Remember to cancel the checkTurnServers callback when we stop the client.
2016-10-02 21:17:31 +01:00
Richard van der Hoff 9e89e71e0e Fix leak of file upload objects
After an upload completed, we were failing to delete the details of the upload
from the list (because the comparison was bogus), meaning we leaked an object
each time.

While we're in the area:

  - make the request methods take an opts object (instead of a localTimeout
    param), and deprecate the WithPrefix versions.

  - make the non-xhr path for uploadContent use authedRequest instead of
    rolling its own.

  - make cancelUpload use the promise.abort() hack for simplicity
2016-09-30 15:29:45 +01:00
David Baker dcedc78fc2 Merge pull request #217 from matrix-org/dbkr/join_3p_location
Add API for 3rd party location lookup
2016-09-30 14:32:33 +01:00
Richard van der Hoff faff057592 crypto: remove duplicate code
Only call SessionStore.storeEndToEndDevicesForUser once per user, rather than
once per device.

(Probably also fixes a bug where, when a user removes all devices, the store
isn't updated)
2016-09-30 09:17:54 +01:00
David Baker 56be271b0a I actually docced them as well 2016-09-29 17:47:51 +01:00
David Baker fa557eb0cc Add API for 3rd party location lookup 2016-09-29 15:50:00 +01:00
Erik Johnston b784d1a5e7 Merge pull request #216 from matrix-org/erikj/sync_fail_first
Handle the first /sync failure differently.
2016-09-23 11:01:10 +01:00
Erik Johnston f6614ac0e4 Fix tests 2016-09-23 10:08:40 +01:00
Erik Johnston e4aea701ab Comment 2016-09-23 09:57:06 +01:00
Erik Johnston 352f79e9fd Handle the first /sync failure differently.
A /sync request may spuriously fail on occasion, without the
"connection" actually being lost. To avoid spurious "Connection Lost"
warning messages we ignore the first /sync and immediately retry, and
only if that fails do we enter an ERROR state.
2016-09-22 16:24:40 +01:00
David Baker 3a17ef983e 0.6.1 2016-09-21 17:23:15 +01:00
David Baker 91e571fb68 Prepare changelog for v0.6.1 2016-09-21 17:20:10 +01:00
Richard van der Hoff 1a3ee28d01 Log when we get an oh_hai message 2016-09-21 17:07:40 +01:00
Richard van der Hoff 669aecf4e6 E2E: Fix NPE in getEventSenderDeviceInfo 2016-09-21 15:05:27 +01:00
David Baker ea23db6450 Merge branch 'master' into develop 2016-09-21 11:38:59 +01:00
David Baker 69e4bdd421 0.6.0 2016-09-21 11:32:26 +01:00
David Baker 86a4fd687c Prepare changelog for v0.6.0 2016-09-21 11:29:28 +01:00
David Baker 14bc4af90c Merge pull request #215 from matrix-org/rav/key_proofs
Fix the ed25519 key checking
2016-09-21 10:44:46 +01:00
Richard van der Hoff 0cd2b2c0e2 Merge pull request #214 from matrix-org/rav/event_sender_device_info
Add MatrixClient.getEventSenderDeviceInfo()
2016-09-21 10:21:57 +01:00
Richard van der Hoff 832559926f Fix the ed25519 key checking
Finish plumbing in the Ed25519 key checks. Make sure we store the claimed key
correctly in the megolm sessions, and keep them as a separate field in
MatrixEvent rather than stuffing them into _clearEvent
2016-09-20 20:42:08 +01:00
Richard van der Hoff 59411353b1 Add 'keys' to *all* olm messages
(including ones which just carry megolm keys)
2016-09-20 20:39:40 +01:00
Richard van der Hoff 83bd420cd5 Return null from decryptEvent if session is unknown
This just makes the shape of the API a bit saner.
2016-09-20 20:39:40 +01:00
Richard van der Hoff 78a0aa5d47 Add MatrixClient.getEventSenderDeviceInfo()
- a function to get information about the device which sent an event
2016-09-20 20:39:16 +01:00
Richard van der Hoff 6e31319294 Handle lack of one-time keys better
If a device had run out of one-time keys, we would send it an empty to_device
event, which it would then fail to decrypt with a "Not included in
recipients", which is all a bit pointless.
2016-09-18 22:58:35 +01:00
Richard van der Hoff cd0b19f93f Crypto: improve console logs
Attempt to make the console logs more helpful by reducing noise and adding
helpful debug info.
2016-09-18 21:55:38 +01:00
Richard van der Hoff 4f22610499 Megolm: clarify jsdoc
Clarify somewhat misleading jsdoc text
2016-09-18 13:52:48 +01:00
Matthew Hodgson 9e57a9352a Merge pull request #212 from matrix-org/rav/get_devicelist_on_join
Pull user device list on join
2016-09-17 19:21:17 +01:00
Richard van der Hoff 4e0d7b56d8 Merge pull request #213 from matrix-org/rav/fix_oh_hai_ping
Fix sending of oh_hais on bad sessions
2016-09-17 19:08:09 +01:00
Richard van der Hoff f2e10e030d Unknown sessions: send oh_hai to all devices if device_id is unknown 2016-09-17 19:07:03 +01:00
Richard van der Hoff 266b7afc72 Fix sending of oh_hais on bad sessions
Fix a bunch of bugs in the code which tried to send an oh_hai message when we
got a message with an unknown megolm session.
2016-09-17 18:30:12 +01:00
Richard van der Hoff a15dffbb3a Pull user device list on join
When a new user joins a room, make sure we download their device list if we
don't already have it.

This should fix at least one cause of
https://github.com/vector-im/vector-web/issues/2249.
2016-09-17 17:44:15 +01:00
Matthew Hodgson a30c816cb6 typo 2016-09-17 15:44:55 +01:00
Matthew Hodgson e65fe483e1 Merge pull request #211 from matrix-org/dbkr/public_rooms_paginate
Support /publicRooms pagination
2016-09-17 01:24:40 +01:00
David Baker 0d51fad805 Make js-sdk compatible with older synapses
Use GET API if no params given. Revert changelog entry since it now doesn't break older synapses.
2016-09-16 23:23:25 +01:00
David Baker 17ed38ad05 Merge remote-tracking branch 'origin/develop' into dbkr/public_rooms_paginate 2016-09-16 20:14:29 +01:00
David Baker 8259f08882 Add changelog entry
to note we've broken publicRooms on older synapses
2016-09-16 20:11:00 +01:00
David Baker 55d6cf7ab0 Update /publicRooms to use the new pagination API 2016-09-16 20:08:21 +01:00
21 changed files with 122111 additions and 305 deletions
+93
View File
@@ -1,3 +1,96 @@
Changes in [0.6.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.2) (2016-10-05)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.1...v0.6.2)
* Check dependencies aren't on develop in release.sh
[\#221](https://github.com/matrix-org/matrix-js-sdk/pull/221)
* Fix checkTurnServers leak on logout
[\#220](https://github.com/matrix-org/matrix-js-sdk/pull/220)
* Fix leak of file upload objects
[\#219](https://github.com/matrix-org/matrix-js-sdk/pull/219)
* crypto: remove duplicate code
[\#218](https://github.com/matrix-org/matrix-js-sdk/pull/218)
* Add API for 3rd party location lookup
[\#217](https://github.com/matrix-org/matrix-js-sdk/pull/217)
* Handle the first /sync failure differently.
[\#216](https://github.com/matrix-org/matrix-js-sdk/pull/216)
Changes in [0.6.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.1) (2016-09-21)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.0...v0.6.1)
* Fix the ed25519 key checking
[\#215](https://github.com/matrix-org/matrix-js-sdk/pull/215)
* Add MatrixClient.getEventSenderDeviceInfo()
[\#214](https://github.com/matrix-org/matrix-js-sdk/pull/214)
Changes in [0.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.0) (2016-09-21)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.6...v0.6.0)
* Pull user device list on join
[\#212](https://github.com/matrix-org/matrix-js-sdk/pull/212)
* Fix sending of oh_hais on bad sessions
[\#213](https://github.com/matrix-org/matrix-js-sdk/pull/213)
* Support /publicRooms pagination
[\#211](https://github.com/matrix-org/matrix-js-sdk/pull/211)
* Update the olm library version to 1.3.0
[\#205](https://github.com/matrix-org/matrix-js-sdk/pull/205)
* Comment what the logic in uploadKeys does
[\#209](https://github.com/matrix-org/matrix-js-sdk/pull/209)
* Include keysProved and keysClaimed in the local echo for events we send.
[\#210](https://github.com/matrix-org/matrix-js-sdk/pull/210)
* Check if we need to upload new one-time keys every 10 minutes
[\#208](https://github.com/matrix-org/matrix-js-sdk/pull/208)
* Reset oneTimeKey to null on each loop iteration.
[\#207](https://github.com/matrix-org/matrix-js-sdk/pull/207)
* Add getKeysProved and getKeysClaimed methods to MatrixEvent.
[\#206](https://github.com/matrix-org/matrix-js-sdk/pull/206)
* Send a 'm.new_device' when we get a message for an unknown group session
[\#204](https://github.com/matrix-org/matrix-js-sdk/pull/204)
* Introduce EventTimelineSet, filtered timelines and global notif timeline.
[\#196](https://github.com/matrix-org/matrix-js-sdk/pull/196)
* Wrap the crypto event handlers in try/catch blocks
[\#203](https://github.com/matrix-org/matrix-js-sdk/pull/203)
* Show warnings on to-device decryption fail
[\#202](https://github.com/matrix-org/matrix-js-sdk/pull/202)
* s/Displayname/DisplayName/
[\#201](https://github.com/matrix-org/matrix-js-sdk/pull/201)
* OH HAI
[\#200](https://github.com/matrix-org/matrix-js-sdk/pull/200)
* Share the current ratchet with new members
[\#199](https://github.com/matrix-org/matrix-js-sdk/pull/199)
* Move crypto bits into a subdirectory
[\#198](https://github.com/matrix-org/matrix-js-sdk/pull/198)
* Refactor event handling in Crypto
[\#197](https://github.com/matrix-org/matrix-js-sdk/pull/197)
* Don't create Olm sessions proactively
[\#195](https://github.com/matrix-org/matrix-js-sdk/pull/195)
* Use to-device events for key sharing
[\#194](https://github.com/matrix-org/matrix-js-sdk/pull/194)
* README: callbacks deprecated
[\#193](https://github.com/matrix-org/matrix-js-sdk/pull/193)
* Fix sender verification for megolm messages
[\#192](https://github.com/matrix-org/matrix-js-sdk/pull/192)
* Use `ciphertext` instead of `body` in megolm events
[\#191](https://github.com/matrix-org/matrix-js-sdk/pull/191)
* Add debug methods to get the state of OlmSessions
[\#189](https://github.com/matrix-org/matrix-js-sdk/pull/189)
* MatrixClient.getStoredDevicesForUser
[\#190](https://github.com/matrix-org/matrix-js-sdk/pull/190)
* Olm-related cleanups
[\#188](https://github.com/matrix-org/matrix-js-sdk/pull/188)
* Update to fixed olmlib
[\#187](https://github.com/matrix-org/matrix-js-sdk/pull/187)
* always play audio out of the remoteAudioElement if it exists.
[\#186](https://github.com/matrix-org/matrix-js-sdk/pull/186)
* Fix exceptions where HTMLMediaElement loads and plays race
[\#185](https://github.com/matrix-org/matrix-js-sdk/pull/185)
* Reset megolm session when people join/leave the room
[\#183](https://github.com/matrix-org/matrix-js-sdk/pull/183)
* Fix exceptions when dealing with redactions
[\#184](https://github.com/matrix-org/matrix-js-sdk/pull/184)
Changes in [0.5.6](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.6) (2016-08-28)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.5...v0.5.6)
+40341
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+40409
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+40513
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+62 -2
View File
@@ -415,12 +415,39 @@ MatrixBaseApis.prototype.roomInitialSync = function(roomId, limit, callback) {
// =========================
/**
* @param {string} options.server The remote server to query for the room list.
* Optional. If unspecified, get the local home
* server's public room list.
* @param {number} options.limit Maximum number of entries to return
* @param {string} options.since Token to paginate from
* @param {object} options.filter Filter parameters
* @param {string} options.filter.generic_search_term String to search for
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.publicRooms = function(callback) {
return this._http.authedRequest(callback, "GET", "/publicRooms");
MatrixBaseApis.prototype.publicRooms = function(options, callback) {
if (typeof(options) == 'function') {
callback = options;
options = {};
}
if (options === undefined) {
options = {};
}
var query_params = {};
if (options.server) {
query_params.server = options.server;
delete options.server;
}
if (Object.keys(options).length === 0 && Object.keys(query_params).length === 0) {
return this._http.authedRequest(callback, "GET", "/publicRooms");
} else {
return this._http.authedRequest(
callback, "POST", "/publicRooms", query_params, options
);
}
};
/**
@@ -1012,6 +1039,39 @@ MatrixBaseApis.prototype.sendToDevice = function(
);
};
// Third party Lookup API
// ======================
/**
* Get the third party protocols that can be reached using
* this HS
* @return {module:client.Promise} Resolves to the result object
*/
MatrixBaseApis.prototype.getThirdpartyProtocols = function() {
return this._http.authedRequestWithPrefix(
undefined, "GET", "/thirdparty/protocols", undefined, undefined,
httpApi.PREFIX_UNSTABLE
);
};
/**
* Get information on how a specific place on a third party protocol
* may be reached.
* @param {string} protocol The protocol given in getThirdpartyProtocols()
* @param {object} params Protocol-specific parameters, as given in th
* response to getThirdpartyProtocols()
* @return {module:client.Promise} Resolves to the result object
*/
MatrixBaseApis.prototype.getThirdpartyLocation = function(protocol, params) {
var path = utils.encodeUri("/thirdparty/location/$protocol", {
$protocol: protocol
});
return this._http.authedRequestWithPrefix(
undefined, "GET", path, params, undefined,
httpApi.PREFIX_UNSTABLE
);
};
/**
* MatrixBaseApis object
+38 -26
View File
@@ -401,6 +401,21 @@ function _setDeviceVerification(client, userId, deviceId, verified, blocked) {
client.emit("deviceVerificationChanged", userId, deviceId);
}
/**
* Get e2e information on the device that sent an event
*
* @param {MatrixEvent} event event to be checked
*
* @return {module:crypto/deviceinfo?}
*/
MatrixClient.prototype.getEventSenderDeviceInfo = function(event) {
if (!this._crypto) {
return null;
}
return this._crypto.getEventSenderDeviceInfo(event);
};
/**
* Check if the sender of an event is verified
*
@@ -410,21 +425,11 @@ function _setDeviceVerification(client, userId, deviceId, verified, blocked) {
* {@link module:client~MatrixClient#setDeviceVerified|setDeviceVerified}.
*/
MatrixClient.prototype.isEventSenderVerified = function(event) {
if (!this._crypto) {
var device = this.getEventSenderDeviceInfo(event);
if (!device) {
return false;
}
var sender_key = event.getSenderKey();
if (!sender_key) {
return false;
}
var algorithm = event.getWireContent().algorithm;
return this._crypto.isSenderKeyVerified(
event.getSender(), algorithm, sender_key
);
return device.isVerified();
};
/**
@@ -460,32 +465,38 @@ MatrixClient.prototype.isRoomEncrypted = function(roomId) {
* @param {MatrixClient} client
* @param {object} raw event
*
* @return {object} decrypted payload (with properties 'type', 'content')
* @return {MatrixEvent}
*/
function _decryptMessage(client, event) {
function _decryptEvent(client, event) {
if (!client._crypto) {
return _badEncryptedMessage(event, "**Encryption not enabled**");
}
var decryptionResult;
try {
return client._crypto.decryptEvent(event);
decryptionResult = client._crypto.decryptEvent(event);
} catch (e) {
if (!(e instanceof Crypto.DecryptionError)) {
throw e;
}
return _badEncryptedMessage(event, "**" + e.message + "**");
}
return new MatrixEvent(
event, decryptionResult.payload,
decryptionResult.keysProved,
decryptionResult.keysClaimed
);
}
function _badEncryptedMessage(event, reason) {
return {
return new MatrixEvent(event, {
type: "m.room.message",
content: {
msgtype: "m.bad.encrypted",
body: reason,
content: event.content,
},
};
});
}
// Room ops
@@ -2532,6 +2543,7 @@ MatrixClient.prototype.stopClient = function() {
if (this._crypto) {
global.clearInterval(this._uploadIntervalID);
}
global.clearTimeout(this._checkTurnServersTimeoutID);
};
function setupCallEventHandler(client) {
@@ -2751,13 +2763,15 @@ function checkTurnServers(client) {
};
client._turnServers = [servers];
// re-fetch when we're about to reach the TTL
setTimeout(function() { checkTurnServers(client); },
(res.ttl || (60 * 60)) * 1000 * 0.9
);
client._checkTurnServersTimeoutID =
setTimeout(function() { checkTurnServers(client); },
(res.ttl || (60 * 60)) * 1000 * 0.9
);
}
}, function(err) {
console.error("Failed to get TURN URIs");
setTimeout(function() { checkTurnServers(client); }, 60000);
client._checkTurnServersTimeoutID =
setTimeout(function() { checkTurnServers(client); }, 60000);
});
}
@@ -2777,12 +2791,10 @@ function _resolve(callback, defer, res) {
function _PojoToMatrixEventMapper(client) {
function mapper(plainOldJsObject) {
var clearData;
if (plainOldJsObject.type === "m.room.encrypted") {
clearData = _decryptMessage(client, plainOldJsObject);
return _decryptEvent(client, plainOldJsObject);
}
var matrixEvent = new MatrixEvent(plainOldJsObject, clearData);
return matrixEvent;
return new MatrixEvent(plainOldJsObject);
}
return mapper;
}
+66 -44
View File
@@ -260,7 +260,8 @@ OlmDevice.prototype.createOutboundSession = function(
* @param {number} message_type message_type field from the received message (must be 0)
* @param {string} ciphertext base64-encoded body from the received message
*
* @return {string} decrypted payload
* @return {{payload: string, session_id: string}} decrypted payload, and
* session id of new session
*
* @raises {Error} if the received message was not valid (for instance, it
* didn't use a valid one-time key).
@@ -284,7 +285,10 @@ OlmDevice.prototype.createInboundSession = function(
self._saveSession(theirDeviceIdentityKey, session);
return payloadString;
return {
payload: payloadString,
session_id: session.session_id(),
};
} finally {
session.free();
}
@@ -386,12 +390,7 @@ OlmDevice.prototype.encryptMessage = function(
* @param {number} message_type message_type field from the received message
* @param {string} ciphertext base64-encoded body from the received message
*
* @return {object} Result, with keys <ul>
* <li><tt>matchesInbound</tt>: (boolean): true if the message was a pre-key
* message which matched an existing inbound session.</li>
* <li><tt>payload</tt>: (string): decrypted payload; null if the received
* message was not valid (for instance, it did not match this session).</li>
* </ul>
* @return {string} decrypted payload.
*/
OlmDevice.prototype.decryptMessage = function(
theirDeviceIdentityKey, sessionId, message_type, ciphertext
@@ -399,28 +398,38 @@ OlmDevice.prototype.decryptMessage = function(
var self = this;
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
var matchesInbound = (message_type === 0 &&
session.matches_inbound(ciphertext));
var payloadString = null;
try {
payloadString = session.decrypt(message_type, ciphertext);
} catch (e) {
console.log(
"Failed to decrypt with an existing session: " + e.message
);
// return null as the payload
}
var payloadString = session.decrypt(message_type, ciphertext);
self._saveSession(theirDeviceIdentityKey, session);
return {
matchesInbound: matchesInbound,
payload: payloadString,
};
return payloadString;
});
};
/**
* Determine if an incoming messages is a prekey message matching an existing session
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @param {string} sessionId the id of the active session
* @param {number} message_type message_type field from the received message
* @param {string} ciphertext base64-encoded body from the received message
*
* @return {boolean} true if the received message is a prekey message which matches
* the given session.
*/
OlmDevice.prototype.matchesSession = function(
theirDeviceIdentityKey, sessionId, message_type, ciphertext
) {
if (message_type !== 0) {
return false;
}
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
return session.matches_inbound(ciphertext);
});
};
// Outbound group session
// ======================
@@ -548,11 +557,15 @@ OlmDevice.prototype._saveInboundGroupSession = function(
* @param {string} roomId
* @param {string} senderKey
* @param {string} sessionId
* @param {function} func
* @return {object} Object with two keys "result": result of func, "exists"
* whether the session exists. if the session doesn't exist then the function
* isn't called and the "result" is undefined.
* @param {function(Olm.InboundGroupSession, Object<string, string>): T} func
* function to call. Second argument is the map of keys claimed by the session.
*
* @return {null} the sessionId is unknown
*
* @return {T} result of func
*
* @private
* @template {T}
*/
OlmDevice.prototype._getInboundGroupSession = function(
roomId, senderKey, sessionId, func
@@ -562,7 +575,7 @@ OlmDevice.prototype._getInboundGroupSession = function(
);
if (r === null) {
return {sessionExists: false};
return null;
}
r = JSON.parse(r);
@@ -579,12 +592,7 @@ OlmDevice.prototype._getInboundGroupSession = function(
var session = new Olm.InboundGroupSession();
try {
session.unpickle(this._pickleKey, r.session);
return {
sessionExists: true,
result: func(session),
keysProved: {curve25519: senderKey},
keysClaimed: r.keysClaimed || {},
};
return func(session, r.keysClaimed || {});
} finally {
session.free();
}
@@ -596,11 +604,11 @@ OlmDevice.prototype._getInboundGroupSession = function(
* @param {string} roomId room in which this session will be used
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @param {string} sessionKey base64-encoded secret key at index chainIndex
* @param {number} chainIndex index at which sessionKey applies
* @param {string} sessionKey base64-encoded secret key
* @param {Object<string, string>} keysClaimed Other keys the sender claims.
*/
OlmDevice.prototype.addInboundGroupSession = function(
roomId, senderKey, sessionId, sessionKey, chainIndex
roomId, senderKey, sessionId, sessionKey, keysClaimed
) {
var self = this;
var session = new Olm.InboundGroupSession();
@@ -611,7 +619,9 @@ OlmDevice.prototype.addInboundGroupSession = function(
"Mismatched group session ID from senderKey: " + senderKey
);
}
self._saveInboundGroupSession(roomId, senderKey, sessionId, session);
self._saveInboundGroupSession(
roomId, senderKey, sessionId, session, keysClaimed
);
} finally {
session.free();
}
@@ -625,19 +635,31 @@ OlmDevice.prototype.addInboundGroupSession = function(
* @param {string} sessionId session identifier
* @param {string} body base64-encoded body of the encrypted message
*
* @return {object} {result: "plaintext"|undefined, sessionExists: Boolean}
* @return {null} the sessionId is unknown
*
* @return {{result: string, keysProved: Object<string, string>, keysClaimed:
* Object<string, string>}} result
*/
OlmDevice.prototype.decryptGroupMessage = function(
roomId, senderKey, sessionId, body
) {
var self = this;
function decrypt(session) {
function decrypt(session, keysClaimed) {
var res = session.decrypt(body);
// the sender must have had the senderKey to persuade us to save the
// session.
var keysProved = {curve25519: senderKey};
self._saveInboundGroupSession(
roomId, senderKey, sessionId, session
roomId, senderKey, sessionId, session, keysClaimed
);
return res;
return {
result: res,
keysClaimed: keysClaimed,
keysProved: keysProved,
};
}
return this._getInboundGroupSession(
+2 -1
View File
@@ -117,7 +117,8 @@ module.exports.DecryptionAlgorithm = DecryptionAlgorithm;
*
* @param {object} event raw event
*
* @return {object} decrypted payload (with properties 'type', 'content')
* @return {null} if the event referred to an unknown megolm session
* @return {module:crypto.DecryptionResult} decryption result
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
+55 -17
View File
@@ -124,7 +124,7 @@ MegolmEncryption.prototype._prepareNewSession = function(room) {
this._olmDevice.addInboundGroupSession(
this._roomId, this._olmDevice.deviceCurve25519Key, session_id,
key.key
key.key, {ed25519: this._olmDevice.deviceEd25519Key}
);
// we're going to share the key with all current members of the room,
@@ -169,7 +169,11 @@ MegolmEncryption.prototype._prepareNewSession = function(room) {
* @private
*
* @param {string} session_id
*
* @param {Object<string, Object<string, boolean>|boolean>} shareMap
* Map from userid to either: true (meaning this is a new user in the room,
* so all of his devices need the keys); or a map from deviceid to true
* (meaning this user has one or more new devices, which need the keys).
*
* @return {module:client.Promise} Promise which resolves once the key sharing
* message has been sent.
@@ -189,6 +193,9 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap)
}
};
// we downloaded the user's device list when they joined the room, or when
// the new device announced itself, so there is no need to do so now.
return self._crypto.ensureOlmSessionsForUsers(
utils.keys(shareMap)
).then(function(devicemap) {
@@ -201,10 +208,10 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap)
}
var devicesToShareWith = shareMap[userId];
var deviceInfos = devicemap[userId];
var sessionResults = devicemap[userId];
for (var deviceId in deviceInfos) {
if (!deviceInfos.hasOwnProperty(deviceId)) {
for (var deviceId in sessionResults) {
if (!sessionResults.hasOwnProperty(deviceId)) {
continue;
}
@@ -215,11 +222,27 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap)
continue;
}
var sessionResult = sessionResults[deviceId];
if (!sessionResult.sessionId) {
// no session with this device, probably because there
// were no one-time keys.
//
// we could send them a to_device message anyway, as a
// signal that they have missed out on the key sharing
// message because of the lack of keys, but there's not
// much point in that really; it will mostly serve to clog
// up to_device inboxes.
//
// ensureOlmSessionsForUsers has already done the logging,
// so just skip it.
continue;
}
console.log(
"sharing keys with device " + userId + ":" + deviceId
);
var deviceInfo = deviceInfos[deviceId].device;
var deviceInfo = sessionResult.device;
if (!contentMap[userId]) {
contentMap[userId] = {};
@@ -292,8 +315,7 @@ MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembers
var newMembership = member.membership;
if (newMembership === 'join') {
// new member in the room.
this._devicesPendingKeyShare[member.userId] = true;
this._onNewRoomMember(member.userId);
return;
}
@@ -318,6 +340,23 @@ MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembers
}
};
/**
* handle a new user joining a room
*
* @param {string} userId new member
*/
MegolmEncryption.prototype._onNewRoomMember = function(userId) {
// make sure we have a list of this user's devices. We are happy to use a
// cached version here: we assume that if we already have a list of the
// user's devices, then we already share an e2e room with them, which means
// that they will have announced any new devices via an m.new_device.
this._crypto.downloadKeys([userId], false).done();
// also flag this user up for needing a keyshare.
this._devicesPendingKeyShare[userId] = true;
};
/**
* @inheritdoc
*
@@ -359,8 +398,8 @@ utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
*
* @param {object} event raw event
*
* @return {object} object with 'result' key with decrypted payload (with
* properties 'type', 'content') and a 'sessionExists' key.
* @return {null} The event referred to an unknown megolm session
* @return {module:crypto.DecryptionResult} decryption result
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
@@ -368,9 +407,6 @@ utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
MegolmDecryption.prototype.decryptEvent = function(event) {
var content = event.content;
console.log("decrypting " + event.event_id + " with sid " +
content.session_id);
if (!content.sender_key || !content.session_id ||
!content.ciphertext
) {
@@ -381,12 +417,14 @@ MegolmDecryption.prototype.decryptEvent = function(event) {
var res = this._olmDevice.decryptGroupMessage(
event.room_id, content.sender_key, content.session_id, content.ciphertext
);
if (res.sessionExists) {
res.result = JSON.parse(res.result);
return res;
} else {
return {sessionExists: false};
if (res === null) {
return null;
}
return {
payload: JSON.parse(res.result),
keysClaimed: res.keysClaimed,
keysProved: res.keysProved,
};
} catch (e) {
throw new base.DecryptionError(e);
}
+102 -57
View File
@@ -119,17 +119,6 @@ OlmEncryption.prototype.encryptMessage = function(room, eventType, content) {
room_id: room.roomId,
type: eventType,
content: content,
// Include the ED25519 key so that the recipient knows what
// device this message came from.
// We don't need to include the curve25519 key since the
// recipient will already know this from the olm headers.
// When combined with the device keys retrieved from the
// homeserver signed by the ed25519 key this proves that
// the curve25519 key and the ed25519 key are owned by
// the same device.
keys: {
"ed25519": self._olmDevice.deviceEd25519Key
},
}
);
});
@@ -153,9 +142,7 @@ utils.inherits(OlmDecryption, base.DecryptionAlgorithm);
*
* @param {object} event raw event
*
* @return {object} result object with result property with the decrypted
* payload (with properties 'type', 'content'), and a "sessionExists" key
* always set to true.
* @return {module:crypto.DecryptionResult} decryption result
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
@@ -169,58 +156,116 @@ OlmDecryption.prototype.decryptEvent = function(event) {
throw new base.DecryptionError("Missing ciphertext");
}
if (!(this._olmDevice.deviceCurve25519Key in content.ciphertext)) {
if (!(this._olmDevice.deviceCurve25519Key in ciphertext)) {
throw new base.DecryptionError("Not included in recipients");
}
var message = ciphertext[this._olmDevice.deviceCurve25519Key];
var payloadString;
var message = content.ciphertext[this._olmDevice.deviceCurve25519Key];
var sessionIds = this._olmDevice.getSessionIdsForDevice(deviceKey);
var payloadString = null;
var foundSession = false;
for (var i = 0; i < sessionIds.length; i++) {
var sessionId = sessionIds[i];
var res = this._olmDevice.decryptMessage(
deviceKey, sessionId, message.type, message.body
try {
payloadString = this._decryptMessage(deviceKey, message);
} catch (e) {
console.warn(
"Failed to decrypt Olm event (id=" +
event.event_id + ") from " + deviceKey +
": " + e.message
);
payloadString = res.payload;
if (payloadString) {
console.log("decrypted with sessionId " + sessionId);
break;
}
if (res.matchesInbound) {
// this was a prekey message which matched this session; don't
// create a new session.
foundSession = true;
break;
}
throw new base.DecryptionError("Bad Encrypted Message");
}
if (message.type === 0 && !foundSession && payloadString === null) {
try {
payloadString = this._olmDevice.createInboundSession(
deviceKey, message.type, message.body
);
console.log("created new inbound sesion");
} catch (e) {
// Failed to decrypt with a new session.
console.warn("Failed to decrypt message with new session", e);
}
}
// TODO: Check the sender user id matches the sender key.
// TODO: check the room_id and fingerprint
if (payloadString !== null) {
var payload = JSON.parse(payloadString);
return {
result: payload,
sessionExists: true,
keysProved: {curve25519: deviceKey},
keysClaimed: payload.keys || {}
};
} else {
throw new base.DecryptionError("Bad Encrypted Message");
}
var payload = JSON.parse(payloadString);
return {
payload: payload,
sessionExists: true,
keysProved: {curve25519: deviceKey},
keysClaimed: payload.keys || {}
};
};
/**
* Attempt to decrypt an Olm message
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key of the sender
* @param {object} message message object, with 'type' and 'body' fields
*
* @return {string} payload, if decrypted successfully.
*/
OlmDecryption.prototype._decryptMessage = function(theirDeviceIdentityKey, message) {
var sessionIds = this._olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey);
// try each session in turn.
var decryptionErrors = {};
for (var i = 0; i < sessionIds.length; i++) {
var sessionId = sessionIds[i];
try {
var payload = this._olmDevice.decryptMessage(
theirDeviceIdentityKey, sessionId, message.type, message.body
);
console.log(
"Decrypted Olm message from " + theirDeviceIdentityKey +
" with session " + sessionId
);
return payload;
} catch (e) {
var foundSession = this._olmDevice.matchesSession(
theirDeviceIdentityKey, sessionId, message.type, message.body
);
if (foundSession) {
// decryption failed, but it was a prekey message matching this
// session, so it should have worked.
throw new Error(
"Error decrypting prekey message with existing session id " +
sessionId + ": " + e.message
);
}
// otherwise it's probably a message for another session; carry on, but
// keep a record of the error
decryptionErrors[sessionId] = e.message;
}
}
if (message.type !== 0) {
// not a prekey message, so it should have matched an existing session, but it
// didn't work.
if (sessionIds.length === 0) {
throw new Error("No existing sessions");
}
throw new Error(
"Error decrypting non-prekey message with existing sessions: " +
JSON.stringify(decryptionErrors)
);
}
// prekey message which doesn't match any existing sessions: make a new
// session.
var res;
try {
res = this._olmDevice.createInboundSession(
theirDeviceIdentityKey, message.type, message.body
);
} catch (e) {
decryptionErrors["(new)"] = e.message;
throw new Error(
"Error decrypting prekey message: " +
JSON.stringify(decryptionErrors)
);
}
console.log(
"created new inbound Olm session ID " +
res.session_id + " with " + theirDeviceIdentityKey
);
return res.payload;
};
base.registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
+125 -64
View File
@@ -86,6 +86,7 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId) {
_registerEventHandlers(this, eventEmitter);
// map from userId -> deviceId -> roomId -> timestamp
this._lastNewDeviceMessageTsByUserDeviceRoom = {};
}
@@ -317,10 +318,10 @@ Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
}
storage[deviceId] = userStore[deviceId].toStorage();
self._sessionStore.storeEndToEndDevicesForUser(
userId, storage
);
}
self._sessionStore.storeEndToEndDevicesForUser(
userId, storage
);
}
return stored;
});
@@ -610,33 +611,56 @@ Crypto.prototype.getOlmSessionsForUser = function(userId) {
/**
* Identify a device by curve25519 identity key and determine its verification state
* Get the device which sent an event
*
* @param {string} userId owner of the device
* @param {string} algorithm encryption algorithm
* @param {string} sender_key curve25519 key to match
* @param {module:models/event.MatrixEvent} event event to be checked
*
* @return {boolean} true if the device is verified
* @return {module:crypto/deviceinfo?}
*/
Crypto.prototype.isSenderKeyVerified = function(userId, algorithm, sender_key) {
Crypto.prototype.getEventSenderDeviceInfo = function(event) {
var sender_key = event.getSenderKey();
var algorithm = event.getWireContent().algorithm;
// sender_key is the curve25519 public key of the device, that the event
// purports to have been sent from. It's assumed that, by the time we get here,
// we have already checked that the event was, in fact, sent by that device.
//
// In the case of both olm and megolm, that is achieved primarily by the
// fact that sessions are indexed by the curve25519 key of the device that
// created the session, and we assume that only that device has the keys
// necessary to create valid messages in that session.
//
// So, all we need to do here is look up the device by sender and
// curve25519 key and determine the state of the verification flag.
var device = this.getDeviceByIdentityKey(userId, algorithm, sender_key);
if (!device) {
return false;
if (!sender_key || !algorithm) {
return null;
}
return device.verified == DeviceVerification.VERIFIED;
// sender_key is the Curve25519 identity key of the device which the event
// was sent from. In the case of Megolm, it's actually the Curve25519
// identity key of the device which set up the Megolm session.
var device = this.getDeviceByIdentityKey(
event.getSender(), algorithm, sender_key
);
if (device === null) {
// we haven't downloaded the details of this device yet.
return null;
}
// so far so good, but now we need to check that the sender of this event
// hadn't advertised someone else's Curve25519 key as their own. We do that
// by checking the Ed25519 claimed by the event (or, in the case of megolm,
// the event which set up the megolm session), to check that it matches the
// fingerprint of the purported sending device.
//
// (see https://github.com/vector-im/vector-web/issues/2215)
var claimedKey = event.getKeysClaimed().ed25519;
if (!claimedKey) {
console.warn("Event " + event.getId() + " claims no ed25519 key: " +
"cannot verify sending device");
return null;
}
if (claimedKey !== device.getFingerprint()) {
console.warn(
"Event " + event.getId() + " claims ed25519 key " + claimedKey +
"but sender device has key " + device.getFingerprint());
return null;
}
return device;
};
@@ -847,12 +871,26 @@ Crypto.prototype.encryptEventIfNeeded = function(event, room) {
});
};
/**
* @typedef {Object} module:crypto.DecryptionResult
*
* @property {Object} payload decrypted payload (with properties 'type',
* 'content').
*
* @property {Object<string, string>} keysClaimed keys that the sender of the
* event claims ownership of: map from key type to base64-encoded key
*
* @property {Object<string, string>} keysProved keys that the sender of the
* event is known to have ownership of: map from key type to base64-encoded
* key
*/
/**
* Decrypt a received event
*
* @param {object} event raw event
*
* @return {object} decrypted payload (with properties 'type', 'content')
* @return {module:crypto.DecryptionResult} decryption result
*
* @raises {algorithms.DecryptionError} if there is a problem decrypting the event
*/
@@ -866,20 +904,28 @@ Crypto.prototype.decryptEvent = function(event) {
olmDevice: this._olmDevice,
});
var r = alg.decryptEvent(event);
var payload = r.result;
if (r.sessionExists) {
payload.keysClaimed = r.keysClaimed;
payload.keysProved = r.keysProved;
return payload;
if (r !== null) {
return r;
} else {
// We've got a message for a session we don't have.
// Maybe the sender forgot to tell us about the session.
// Remind the sender that we exist so that they might
// tell us about the sender.
if (event.getRoomId !== undefined && event.getSender !== undefined) {
this._sendPingToDevice(
event.getSender(), event.content.device, event.getRoomId
);
// We've got a message for a session we don't have. Maybe the sender
// forgot to tell us about the session. Remind the sender that we
// exist so that they might tell us about the session on their next
// send.
//
// (Alternatively, it might be that we are just looking at
// scrollback... at least we rate-limit the m.new_device events :/)
//
// XXX: this is a band-aid which masks symptoms of other bugs. It would
// be nice to get rid of it.
if (event.room_id !== undefined && event.sender !== undefined) {
var device_id = event.content.device_id;
if (device_id === undefined) {
// if the sending device didn't tell us its device_id, fall
// back to all devices.
device_id = null;
}
this._sendPingToDevice(event.sender, device_id, event.room_id);
}
throw new algorithms.DecryptionError("Unknown inbound session id");
@@ -892,39 +938,50 @@ Crypto.prototype.decryptEvent = function(event) {
*
* This is rate limited to send a message at most once an hour per desination.
*
* @param {string} userId The ID of the user to ping.
* @param {string} deviceId The ID of the device to ping.
* @param {string} roomId The ID of the room we want to remind them about.
* @param {string} userId The ID of the user to ping.
* @param {string?} deviceId The ID of the device to ping. If null, all
* devices.
* @param {string} roomId The ID of the room we want to remind them about.
*/
Crypto.prototype._sendPingToDevice = function(userId, deviceId, roomId) {
if (deviceId === undefined) {
if (deviceId === null) {
deviceId = "*";
}
var lastMessageTsMap = this._lastNewDeviceMessageTsByUserDeviceRoom;
var lastTsByDevice = lastMessageTsMap[userId] || {};
var lastTsByRoom = lastTsByDevice[deviceId] || {};
var lastTsByDevice = lastMessageTsMap[userId];
if (!lastTsByDevice) {
lastTsByDevice = lastMessageTsMap[userId] = {};
}
var lastTsByRoom = lastTsByDevice[deviceId];
if (!lastTsByRoom) {
lastTsByRoom = lastTsByDevice[deviceId] = {};
}
var lastTs = lastTsByRoom[roomId];
var timeNowMs = Date.now();
var oneHourMs = 1000 * 60 * 60;
if (lastTs === undefined || lastTs + oneHourMs < timeNowMs) {
var content = {
userId: {
deviceId: {
device_id: this._deviceId,
rooms: [roomId],
}
}
};
lastTsByRoom[roomId] = timeNowMs;
this._baseApis.sendToDevice(
"m.new_device", // OH HAI!
content
).done(function() {});
if (lastTs !== undefined && lastTs + oneHourMs > timeNowMs) {
// rate-limiting
return;
}
var content = {};
content[userId] = {};
content[userId][deviceId] = {
device_id: this._deviceId,
rooms: [roomId],
};
this._baseApis.sendToDevice(
"m.new_device", // OH HAI!
content
).done();
lastTsByRoom[roomId] = timeNowMs;
};
/**
@@ -1042,9 +1099,10 @@ Crypto.prototype._onRoomKeyEvent = function(event) {
*/
Crypto.prototype._onRoomMembership = function(event, member, oldMembership) {
// this event handler is registered on the *client* (as opposed to the
// room member itself), which means it is only called on changes to the
// *live* membership state (ie, it is not called when we back-paginate).
// this event handler is registered on the *client* (as opposed to the room
// member itself), which means it is only called on changes to the *live*
// membership state (ie, it is not called when we back-paginate, nor when
// we load the state in the initialsync).
//
// Further, it is automatically registered and called when new members
// arrive in the room.
@@ -1078,6 +1136,9 @@ Crypto.prototype._onNewDeviceEvent = function(event) {
return;
}
console.log("m.new_device event from " + userId + ":" + deviceId +
" for rooms " + rooms);
var self = this;
this.downloadKeys(
[userId], true
+12
View File
@@ -51,6 +51,18 @@ module.exports.encryptMessageForDevices = function(
var payloadJson = {
fingerprint: participantHash,
sender_device: ourDeviceId,
// Include the Ed25519 key so that the recipient knows what
// device this message came from.
// We don't need to include the curve25519 key since the
// recipient will already know this from the olm headers.
// When combined with the device keys retrieved from the
// homeserver signed by the ed25519 key this proves that
// the curve25519 key and the ed25519 key are owned by
// the same device.
keys: {
"ed25519": olmDevice.deviceEd25519Key,
},
};
utils.extend(payloadJson, payloadFields);
+122 -69
View File
@@ -113,8 +113,6 @@ module.exports.MatrixHttpApi.prototype = {
"Expected callback to be a function but got " + typeof callback
);
}
var defer = q.defer();
var url = this.opts.baseUrl + "/_matrix/media/v1/upload";
// browser-request doesn't support File objects because it deep-copies
// the options using JSON.parse(JSON.stringify(options)). Instead of
// loading the whole file into memory as a string and letting
@@ -124,8 +122,9 @@ module.exports.MatrixHttpApi.prototype = {
// of important here)
var upload = { loaded: 0, total: 0 };
var promise;
if (global.XMLHttpRequest) {
var defer = q.defer();
var xhr = new global.XMLHttpRequest();
upload.xhr = xhr;
var cb = requestCallback(defer, callback, this.opts.onlyData);
@@ -168,6 +167,7 @@ module.exports.MatrixHttpApi.prototype = {
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
defer.notify(ev);
});
var url = this.opts.baseUrl + "/_matrix/media/v1/upload";
url += "?access_token=" + encodeURIComponent(this.opts.accessToken);
url += "&filename=" + encodeURIComponent(file.name);
@@ -180,47 +180,48 @@ module.exports.MatrixHttpApi.prototype = {
xhr.setRequestHeader("Content-Type", 'application/octet-stream');
}
xhr.send(file);
promise = defer.promise;
// dirty hack (as per _request) to allow the upload to be cancelled.
promise.abort = xhr.abort.bind(xhr);
} else {
var queryParams = {
filename: file.name,
access_token: this.opts.accessToken
};
upload.request = this.opts.request({
uri: url,
qs: queryParams,
method: "POST",
headers: {"Content-Type": file.type},
body: file.stream
}, requestCallback(defer, callback, this.opts.onlyData));
promise = this.authedRequest(
callback, "POST", "/upload", queryParams, file.stream, {
prefix: "/_matrix/media/v1",
localTimeoutMs: 30000,
headers: {"Content-Type": file.type},
}
);
}
this.uploads.push(upload);
var self = this;
upload.promise = defer.promise.finally(function() {
var uploadsKeys = Object.keys(self.uploads);
for (var i = 0; i < uploadsKeys.length; ++i) {
if (self.uploads[uploadsKeys[i]].promise === defer.promise) {
self.uploads.splice(uploadsKeys[i], 1);
// remove the upload from the list on completion
var promise0 = promise.finally(function() {
for (var i = 0; i < self.uploads.length; ++i) {
if (self.uploads[i] === upload) {
self.uploads.splice(i, 1);
return;
}
}
});
return upload.promise;
// copy our dirty abort() method to the new promise
promise0.abort = promise.abort;
upload.promise = promise0;
this.uploads.push(upload);
return promise0;
},
cancelUpload: function(promise) {
var uploadsKeys = Object.keys(this.uploads);
for (var i = 0; i < uploadsKeys.length; ++i) {
var upload = this.uploads[uploadsKeys[i]];
if (upload.promise === promise) {
if (upload.xhr !== undefined) {
upload.xhr.abort();
return true;
} else if (upload.request !== undefined) {
upload.request.abort();
return true;
}
}
if (promise.abort) {
promise.abort();
return true;
}
return false;
},
@@ -271,11 +272,22 @@ module.exports.MatrixHttpApi.prototype = {
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
*
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} data The HTTP JSON body.
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
*
* @param {Object=} opts additional options
*
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {sting=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
@@ -283,18 +295,25 @@ module.exports.MatrixHttpApi.prototype = {
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
authedRequest: function(callback, method, path, queryParams, data, localTimeoutMs) {
if (!queryParams) { queryParams = {}; }
queryParams.access_token = this.opts.accessToken;
var self = this;
authedRequest: function(callback, method, path, queryParams, data, opts) {
if (!queryParams) {
queryParams = {};
}
if (!queryParams.access_token) {
queryParams.access_token = this.opts.accessToken;
}
var request_promise = this.request(
callback, method, path, queryParams, data, localTimeoutMs
callback, method, path, queryParams, data, opts
);
var self = this;
request_promise.catch(function(err) {
if (err.errcode == 'M_UNKNOWN_TOKEN') {
self.event_emitter.emit("Session.logged_out");
}
});
// return the original promise, otherwise tests break due to it having to
// go around the event loop one more time to process the result of the request
return request_promise;
@@ -307,11 +326,22 @@ module.exports.MatrixHttpApi.prototype = {
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
*
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} data The HTTP JSON body.
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
*
* @param {Object=} opts additional options
*
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {sting=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
@@ -319,9 +349,13 @@ module.exports.MatrixHttpApi.prototype = {
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
request: function(callback, method, path, queryParams, data, localTimeoutMs) {
return this.requestWithPrefix(
callback, method, path, queryParams, data, this.opts.prefix, localTimeoutMs
request: function(callback, method, path, queryParams, data, opts) {
opts = opts || {};
var prefix = opts.prefix || this.opts.prefix;
var fullUri = this.opts.baseUrl + prefix + path;
return this.requestOtherUrl(
callback, method, fullUri, queryParams, data, opts
);
},
@@ -347,16 +381,16 @@ module.exports.MatrixHttpApi.prototype = {
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*
* @deprecated prefer authedRequest with opts.prefix
*/
authedRequestWithPrefix: function(callback, method, path, queryParams, data,
prefix, localTimeoutMs) {
var fullUri = this.opts.baseUrl + prefix + path;
if (!queryParams) {
queryParams = {};
}
queryParams.access_token = this.opts.accessToken;
return this._request(
callback, method, fullUri, queryParams, data, localTimeoutMs
return this.authedRequest(
callback, method, path, queryParams, data, {
localTimeoutMs: localTimeoutMs,
prefix: prefix,
}
);
},
@@ -382,15 +416,16 @@ module.exports.MatrixHttpApi.prototype = {
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*
* @deprecated prefer request with opts.prefix
*/
requestWithPrefix: function(callback, method, path, queryParams, data, prefix,
localTimeoutMs) {
var fullUri = this.opts.baseUrl + prefix + path;
if (!queryParams) {
queryParams = {};
}
return this._request(
callback, method, fullUri, queryParams, data, localTimeoutMs
return this.request(
callback, method, path, queryParams, data, {
localTimeoutMs: localTimeoutMs,
prefix: prefix,
}
);
},
@@ -400,11 +435,22 @@ module.exports.MatrixHttpApi.prototype = {
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} uri The HTTP URI
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
*
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} data The HTTP JSON body.
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
*
* @param {Object=} opts additional options
*
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {sting=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
@@ -413,12 +459,18 @@ module.exports.MatrixHttpApi.prototype = {
* occurred. This includes network problems and Matrix-specific error JSON.
*/
requestOtherUrl: function(callback, method, uri, queryParams, data,
localTimeoutMs) {
if (!queryParams) {
queryParams = {};
opts) {
if (opts === undefined || opts === null) {
opts = {};
} else if (isFinite(opts)) {
// opts used to be localTimeoutMs
opts = {
localTimeoutMs: opts
};
}
return this._request(
callback, method, uri, queryParams, data, localTimeoutMs
callback, method, uri, queryParams, data, opts
);
},
@@ -441,16 +493,15 @@ module.exports.MatrixHttpApi.prototype = {
return this.opts.baseUrl + prefix + path + queryString;
},
_request: function(callback, method, uri, queryParams, data, localTimeoutMs) {
_request: function(callback, method, uri, queryParams, data, opts) {
if (callback !== undefined && !utils.isFunction(callback)) {
throw Error(
"Expected callback to be a function but got " + typeof callback
);
}
opts = opts || {};
var self = this;
if (!queryParams) {
queryParams = {};
}
if (this.opts.extraParams) {
for (var key in this.opts.extraParams) {
if (!this.opts.extraParams.hasOwnProperty(key)) { continue; }
@@ -462,6 +513,7 @@ module.exports.MatrixHttpApi.prototype = {
var timeoutId;
var timedOut = false;
var req;
var localTimeoutMs = opts.localTimeoutMs;
if (localTimeoutMs) {
timeoutId = callbacks.setTimeout(function() {
timedOut = true;
@@ -488,6 +540,7 @@ module.exports.MatrixHttpApi.prototype = {
body: data,
json: true,
timeout: localTimeoutMs,
headers: opts.headers || {},
_matrix_opts: this.opts
},
function(err, response, body) {
+23 -16
View File
@@ -54,6 +54,12 @@ module.exports.EventStatus = {
* @param {Object=} clearEvent For encrypted events, the plaintext payload for
* the event (typically containing <tt>type</tt> and <tt>content</tt> fields).
*
* @param {Object=} keysProved Keys owned by the sender of this event.
* See {@link module:models/event.MatrixEvent#getKeysProved}.
*
* @param {Object=} keysClaimed Keys the sender of this event claims.
* See {@link module:models/event.MatrixEvent#getKeysClaimed}.
*
* @prop {Object} event The raw (possibly encrypted) event. <b>Do not access
* this property</b> directly unless you absolutely have to. Prefer the getter
* methods defined on this class. Using the getter methods shields your app
@@ -68,7 +74,9 @@ module.exports.EventStatus = {
* that getDirectionalContent() will return event.content and not event.prev_content.
* Default: true. <strong>This property is experimental and may change.</strong>
*/
module.exports.MatrixEvent = function MatrixEvent(event, clearEvent) {
module.exports.MatrixEvent = function MatrixEvent(
event, clearEvent, keysProved, keysClaimed
) {
this.event = event || {};
this.sender = null;
this.target = null;
@@ -77,6 +85,9 @@ module.exports.MatrixEvent = function MatrixEvent(event, clearEvent) {
this._clearEvent = clearEvent || {};
this._pushActions = null;
this._keysProved = keysProved || {};
this._keysClaimed = keysClaimed || {};
};
module.exports.MatrixEvent.prototype = {
@@ -221,11 +232,11 @@ module.exports.MatrixEvent.prototype = {
this._clearEvent = {
type: this.event.type,
content: this.event.content,
keysProved: keys,
keysClaimed: keys,
};
this.event.type = crypto_type;
this.event.content = crypto_content;
this._keysProved = keys;
this._keysClaimed = keys;
},
/**
@@ -246,32 +257,28 @@ module.exports.MatrixEvent.prototype = {
/**
* The keys that must have been owned by the sender of this encrypted event.
* <p>
* These don't necessarily have to come from this event itself, but may be
* implied by the cryptographic session.
* For example megolm messages don't prove keys directly, but instead
* inherit a proof from the olm message that established the session.
* @return {object}
*
* @return {Object<string, string>}
*/
getKeysProved: function() {
// The keysProved property usually isn't actually part of the decrypted
// plaintext. Instead it is added after decryption by the crypto
// algorithm in lib/crypto/algorithms.
return this._clearEvent.keysProved || {};
return this._keysProved;
},
/**
* The additional keys the sender of this encrypted event claims to possess
* The additional keys the sender of this encrypted event claims to possess.
* <p>
* These don't necessarily have to come from this event itself, but may be
* implied by the cryptographic session.
* For example megolm messages don't claim keys directly, but instead
* inherit a claim from the olm message that established the session.
* @return {object}
*
* @return {Object<string, string>}
*/
getKeysClaimed: function() {
// The keysClaimed property usually isn't actually part of the
// decrypted plaintext. Instead it is added after decryption by the
// crypto algorithm in lib/crypto/algorithms.
return this._clearEvent.keysClaimed || {};
return this._keysClaimed;
},
getUnsigned: function() {
+21 -6
View File
@@ -545,13 +545,28 @@ SyncApi.prototype._sync = function(syncOptions) {
console.error("/sync error %s", err);
console.error(err);
debuglog("Starting keep-alive");
self._syncConnectionLost = true;
self._startKeepAlives().done(function() {
if (!self._syncConnectionLost) {
// This is the first failure, which may be spurious. To avoid unnecessary
// connection error warnings we simply retry the /sync immediately. Only
// if *that* one fails too do we say the connection has been lost.
// Examples of when this may happen are:
// - Restarting backend servers. (In an HA world backends may be
// restarted all the time, and its easiest just to make the
// client retry).
// - Intermediate proxies restarting.
// - Device network changes.
// Should we emit a state like "MAYBE_CONNETION_LOST"?
self._syncConnectionLost = true;
self._sync(syncOptions);
});
self._currentSyncRequest = null;
self._updateSyncState("ERROR", { error: err });
} else {
debuglog("Starting keep-alive");
self._syncConnectionLost = true;
self._startKeepAlives().done(function() {
self._sync(syncOptions);
});
self._currentSyncRequest = null;
self._updateSyncState("ERROR", { error: err });
}
});
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "0.5.6",
"version": "0.6.2",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
+11 -2
View File
@@ -3,8 +3,10 @@
# Script to perform a release of matrix-js-sdk. Performs the steps documented
# in RELEASING.md
#
# Requires github-changelog-generator; to install, do
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
# Requires:
# github-changelog-generator; to install, do
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
# jq; install from your distibution's package manager (https://stedolan.github.io/jq/)
set -e
@@ -20,6 +22,13 @@ $USAGE
EOF
}
ret=0
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
if [ "$ret" -eq 0 ]; then
echo "package.json contains develop dependencies. Refusing to release."
exit
fi
skip_changelog=
skip_jsdoc=
changelog_file="CHANGELOG.md"
+12
View File
@@ -332,9 +332,13 @@ describe("MatrixClient", function() {
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
// We fail twice since the SDK ignores the first error.
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }
});
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE2" }
});
httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA
});
@@ -355,9 +359,13 @@ describe("MatrixClient", function() {
it("should transition SYNCING -> ERROR after a failed /sync", function(done) {
var expectedStates = [];
// We fail twice since the SDK ignores the first error.
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
});
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO2" }
});
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
@@ -396,6 +404,10 @@ describe("MatrixClient", function() {
it("should transition ERROR -> ERROR if multiple /sync fails", function(done) {
var expectedStates = [];
// We fail twice since the SDK ignores the first error.
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
});
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
});