From 971d572fbfea821fb825b963e0a64876405e45c7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 28 Jan 2019 16:03:27 -0700 Subject: [PATCH 1/3] Supporting infrastructure for educated decisions on when to upgrade rooms Part of https://github.com/vector-im/riot-web/issues/8251 --- src/client.js | 29 +++++++++++++++++++ src/models/room.js | 69 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 92 insertions(+), 6 deletions(-) diff --git a/src/client.js b/src/client.js index 43c1b8761..30ebcb09c 100644 --- a/src/client.js +++ b/src/client.js @@ -59,6 +59,7 @@ Promise.config({warnings: false}); const SCROLLBACK_DELAY_MS = 3000; const CRYPTO_ENABLED = isCryptoAvailable(); +const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value function keysFromRecoverySession(sessions, decryptionKey, roomId) { const keys = []; @@ -225,6 +226,8 @@ function MatrixClient(opts) { this._pushProcessor = new PushProcessor(this); this._serverSupportsLazyLoading = null; + + this._cachedCapabilities = null; // { capabilities: {}, lastUpdated: timestamp } } utils.inherits(MatrixClient, EventEmitter); utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype); @@ -392,6 +395,32 @@ MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) { this._notifTimelineSet = notifTimelineSet; }; +/** + * Gets the capabilities of the homeserver. Always returns an object of + * capability keys and their options, which may be empty. + * @return {module:client.Promise} Resolves to the capabilities of the homeserver + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.getCapabilities = function() { + if (this._cachedCapabilities) { + const now = new Date().getTime(); + if (now - this._cachedCapabilities.lastUpdated <= CAPABILITIES_CACHE_MS) { + return Promise.resolve(this._cachedCapabilities.capabilities); + } + } + return this._http.authedRequest( + undefined, "GET", "/capabilities", + ).then((r) => { + if (!r) r = {}; + const capabilities = r["capabilities"] || {}; + this._cachedCapabilities = { + capabilities: capabilities, + lastUpdated: new Date().getTime(), + }; + return capabilities; + }); +}; + // Crypto bits // =========== diff --git a/src/models/room.js b/src/models/room.js index c01027114..13e1fde22 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -31,7 +31,7 @@ const EventTimelineSet = require("./event-timeline-set"); import ReEmitter from '../ReEmitter'; -const LATEST_ROOM_VERSION = '1'; +const KNOWN_SAFE_ROOM_VERSION = '1'; const SAFE_ROOM_VERSIONS = ['1', '2']; function synthesizeReceipt(userId, event, receiptType) { @@ -212,19 +212,76 @@ Room.prototype.getVersion = function() { * Determines whether this room needs to be upgraded to a new version * @returns {string?} What version the room should be upgraded to, or null if * the room does not require upgrading at this time. + * @deprecated Use #getRecommendedVersion() instead */ Room.prototype.shouldUpgradeToVersion = function() { - // This almost certainly won't be the way this actually works - this - // is essentially a stub method. - // Something like https://github.com/matrix-org/matrix-doc/pull/1804 - // would solve this problem for us. + // TODO: Remove this function. + // This makes assumptions about which versions are safe, and can easily + // be wrong. Instead, people are encouraged to use getRecommendedVersion + // which determines a safer value. This function doesn't use that function + // because this is not async-capable, and to avoid breaking the contract + // we're deprecating this. + if (!SAFE_ROOM_VERSIONS.includes(this.getVersion())) { - return LATEST_ROOM_VERSION; + return KNOWN_SAFE_ROOM_VERSION; } return null; }; +/** + * Determines the recommended room version for the room. This returns an + * object with 3 properties: version as the new version the + * room should be upgraded to (may be the same as the current version); + * needsUpgrade to indicate if the room actually can be + * upgraded (ie: does the current version not match?); and urgent + * to indicate if the new version patches a vulnerability in a previous + * version. + * @returns {Promise<{version: string, needsUpgrade: bool, urgent: bool}>} + * Resolves to the version the room should be upgraded to. + */ +Room.prototype.getRecommendedVersion = async function() { + const capabilities = await this._client.getCapabilities(); + let versionCap = capabilities["m.room_versions"]; + if (!versionCap) { + versionCap = { + default: KNOWN_SAFE_ROOM_VERSION, + available: {}, + }; + for (const safeVer of SAFE_ROOM_VERSIONS) { + versionCap.available[safeVer] = "stable"; + } + } + + const currentVersion = this.getVersion(); + + const result = { + version: currentVersion, + needsUpgrade: false, + urgent: false, + }; + + // If the room is on the default version then nothing needs to change + if (currentVersion === versionCap.default) return Promise.resolve(result); + + const stableVersions = Object.keys(versionCap.available) + .filter((v) => versionCap.available[v] === 'stable'); + + // Check if the room is on an unstable version. We determine urgency based + // off the version being in the Matrix spec namespace or not (if the version + // is in the current namespace and unstable, the room is probably vulnerable). + if (!stableVersions.includes(currentVersion)) { + result.version = versionCap.default; + result.needsUpgrade = true; + result.urgent = !!this.getVersion().match(/^[0-9]+[0-9.]*$/g); + return Promise.resolve(result); + } + + // The room is on a stable, but non-default, version by this point. + // No upgrade needed. + return Promise.resolve(result); +}; + /** * Determines whether the given user is permitted to perform a room upgrade * @param {String} userId The ID of the user to test against From 2d4e9d0d3f0d9d96d336ce6d10e9c0f07f832749 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 28 Jan 2019 17:18:57 -0700 Subject: [PATCH 2/3] Add safety for when the endpoint doesn't exist --- src/client.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 30ebcb09c..9efdf6d53 100644 --- a/src/client.js +++ b/src/client.js @@ -408,9 +408,11 @@ MatrixClient.prototype.getCapabilities = function() { return Promise.resolve(this._cachedCapabilities.capabilities); } } + + // We swallow errors because we need a default object anyhow return this._http.authedRequest( undefined, "GET", "/capabilities", - ).then((r) => { + ).catch(() => null).then((r) => { if (!r) r = {}; const capabilities = r["capabilities"] || {}; this._cachedCapabilities = { From 4ea785b604f717164a9329ffd4a306938bbbcdd4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 29 Jan 2019 10:46:40 -0700 Subject: [PATCH 3/3] Add some prose for what safe versions are --- src/models/room.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/models/room.js b/src/models/room.js index 13e1fde22..a4d5c1874 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -31,6 +31,12 @@ const EventTimelineSet = require("./event-timeline-set"); import ReEmitter from '../ReEmitter'; +// These constants are used as sane defaults when the homeserver doesn't support +// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be +// the same as the common default room version whereas SAFE_ROOM_VERSIONS are the +// room versions which are considered okay for people to run without being asked +// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers +// return an m.room_versions capability. const KNOWN_SAFE_ROOM_VERSION = '1'; const SAFE_ROOM_VERSIONS = ['1', '2'];