matrix-js-sdk
Version:
Matrix Client-Server SDK for Javascript
395 lines (373 loc) • 17.8 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.RoomKeyRequestState = exports.OutgoingRoomKeyRequestManager = void 0;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _uuid = require("uuid");
var _logger = require("../logger");
var _event = require("../@types/event");
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
/**
* Internal module. Management of outgoing room key requests.
*
* See https://docs.google.com/document/d/1m4gQkcnJkxNuBmb5NoFCIadIY-DyqqNAS3lloE73BlQ
* for draft documentation on what we're supposed to be implementing here.
*/
// delay between deciding we want some keys, and sending out the request, to
// allow for (a) it turning up anyway, (b) grouping requests together
const SEND_KEY_REQUESTS_DELAY_MS = 500;
/**
* possible states for a room key request
*
* The state machine looks like:
* ```
*
* | (cancellation sent)
* | .-------------------------------------------------.
* | | |
* V V (cancellation requested) |
* UNSENT -----------------------------+ |
* | | |
* | | |
* | (send successful) | CANCELLATION_PENDING_AND_WILL_RESEND
* V | Λ
* SENT | |
* |-------------------------------- | --------------'
* | | (cancellation requested with intent
* | | to resend the original request)
* | |
* | (cancellation requested) |
* V |
* CANCELLATION_PENDING |
* | |
* | (cancellation sent) |
* V |
* (deleted) <---------------------------+
* ```
*/
let RoomKeyRequestState;
exports.RoomKeyRequestState = RoomKeyRequestState;
(function (RoomKeyRequestState) {
RoomKeyRequestState[RoomKeyRequestState["Unsent"] = 0] = "Unsent";
RoomKeyRequestState[RoomKeyRequestState["Sent"] = 1] = "Sent";
RoomKeyRequestState[RoomKeyRequestState["CancellationPending"] = 2] = "CancellationPending";
RoomKeyRequestState[RoomKeyRequestState["CancellationPendingAndWillResend"] = 3] = "CancellationPendingAndWillResend";
})(RoomKeyRequestState || (exports.RoomKeyRequestState = RoomKeyRequestState = {}));
class OutgoingRoomKeyRequestManager {
// handle for the delayed call to sendOutgoingRoomKeyRequests. Non-null
// if the callback has been set, or if it is still running.
// sanity check to ensure that we don't end up with two concurrent runs
// of sendOutgoingRoomKeyRequests
constructor(baseApis, deviceId, cryptoStore) {
this.baseApis = baseApis;
this.deviceId = deviceId;
this.cryptoStore = cryptoStore;
(0, _defineProperty2.default)(this, "sendOutgoingRoomKeyRequestsTimer", void 0);
(0, _defineProperty2.default)(this, "sendOutgoingRoomKeyRequestsRunning", false);
(0, _defineProperty2.default)(this, "clientRunning", true);
}
/**
* Called when the client is stopped. Stops any running background processes.
*/
stop() {
_logger.logger.log("stopping OutgoingRoomKeyRequestManager");
// stop the timer on the next run
this.clientRunning = false;
}
/**
* Send any requests that have been queued
*/
sendQueuedRequests() {
this.startTimer();
}
/**
* Queue up a room key request, if we haven't already queued or sent one.
*
* The `requestBody` is compared (with a deep-equality check) against
* previous queued or sent requests and if it matches, no change is made.
* Otherwise, a request is added to the pending list, and a job is started
* in the background to send it.
*
* @param resend - whether to resend the key request if there is
* already one
*
* @returns resolves when the request has been added to the
* pending list (or we have established that a similar request already
* exists)
*/
async queueRoomKeyRequest(requestBody, recipients, resend = false) {
const req = await this.cryptoStore.getOutgoingRoomKeyRequest(requestBody);
if (!req) {
await this.cryptoStore.getOrAddOutgoingRoomKeyRequest({
requestBody: requestBody,
recipients: recipients,
requestId: this.baseApis.makeTxnId(),
state: RoomKeyRequestState.Unsent
});
} else {
switch (req.state) {
case RoomKeyRequestState.CancellationPendingAndWillResend:
case RoomKeyRequestState.Unsent:
// nothing to do here, since we're going to send a request anyways
return;
case RoomKeyRequestState.CancellationPending:
{
// existing request is about to be cancelled. If we want to
// resend, then change the state so that it resends after
// cancelling. Otherwise, just cancel the cancellation.
const state = resend ? RoomKeyRequestState.CancellationPendingAndWillResend : RoomKeyRequestState.Sent;
await this.cryptoStore.updateOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.CancellationPending, {
state,
cancellationTxnId: this.baseApis.makeTxnId()
});
break;
}
case RoomKeyRequestState.Sent:
{
// a request has already been sent. If we don't want to
// resend, then do nothing. If we do want to, then cancel the
// existing request and send a new one.
if (resend) {
const state = RoomKeyRequestState.CancellationPendingAndWillResend;
const updatedReq = await this.cryptoStore.updateOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.Sent, {
state,
cancellationTxnId: this.baseApis.makeTxnId(),
// need to use a new transaction ID so that
// the request gets sent
requestTxnId: this.baseApis.makeTxnId()
});
if (!updatedReq) {
// updateOutgoingRoomKeyRequest couldn't find the request
// in state ROOM_KEY_REQUEST_STATES.SENT, so we must have
// raced with another tab to mark the request cancelled.
// Try again, to make sure the request is resent.
return this.queueRoomKeyRequest(requestBody, recipients, resend);
}
// We don't want to wait for the timer, so we send it
// immediately. (We might actually end up racing with the timer,
// but that's ok: even if we make the request twice, we'll do it
// with the same transaction_id, so only one message will get
// sent).
//
// (We also don't want to wait for the response from the server
// here, as it will slow down processing of received keys if we
// do.)
try {
await this.sendOutgoingRoomKeyRequestCancellation(updatedReq, true);
} catch (e) {
_logger.logger.error("Error sending room key request cancellation;" + " will retry later.", e);
}
// The request has transitioned from
// CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We
// still need to resend the request which is now UNSENT, so
// start the timer if it isn't already started.
}
break;
}
default:
throw new Error("unhandled state: " + req.state);
}
}
}
/**
* Cancel room key requests, if any match the given requestBody
*
*
* @returns resolves when the request has been updated in our
* pending list.
*/
cancelRoomKeyRequest(requestBody) {
return this.cryptoStore.getOutgoingRoomKeyRequest(requestBody).then(req => {
if (!req) {
// no request was made for this key
return;
}
switch (req.state) {
case RoomKeyRequestState.CancellationPending:
case RoomKeyRequestState.CancellationPendingAndWillResend:
// nothing to do here
return;
case RoomKeyRequestState.Unsent:
// just delete it
// FIXME: ghahah we may have attempted to send it, and
// not yet got a successful response. So the server
// may have seen it, so we still need to send a cancellation
// in that case :/
_logger.logger.log("deleting unnecessary room key request for " + stringifyRequestBody(requestBody));
return this.cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.Unsent);
case RoomKeyRequestState.Sent:
{
// send a cancellation.
return this.cryptoStore.updateOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.Sent, {
state: RoomKeyRequestState.CancellationPending,
cancellationTxnId: this.baseApis.makeTxnId()
}).then(updatedReq => {
if (!updatedReq) {
// updateOutgoingRoomKeyRequest couldn't find the
// request in state ROOM_KEY_REQUEST_STATES.SENT,
// so we must have raced with another tab to mark
// the request cancelled. There is no point in
// sending another cancellation since the other tab
// will do it.
_logger.logger.log("Tried to cancel room key request for " + stringifyRequestBody(requestBody) + " but it was already cancelled in another tab");
return;
}
// We don't want to wait for the timer, so we send it
// immediately. (We might actually end up racing with the timer,
// but that's ok: even if we make the request twice, we'll do it
// with the same transaction_id, so only one message will get
// sent).
//
// (We also don't want to wait for the response from the server
// here, as it will slow down processing of received keys if we
// do.)
this.sendOutgoingRoomKeyRequestCancellation(updatedReq).catch(e => {
_logger.logger.error("Error sending room key request cancellation;" + " will retry later.", e);
this.startTimer();
});
});
}
default:
throw new Error("unhandled state: " + req.state);
}
});
}
/**
* Look for room key requests by target device and state
*
* @param userId - Target user ID
* @param deviceId - Target device ID
*
* @returns resolves to a list of all the {@link OutgoingRoomKeyRequest}
*/
getOutgoingSentRoomKeyRequest(userId, deviceId) {
return this.cryptoStore.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, [RoomKeyRequestState.Sent]);
}
/**
* Find anything in `sent` state, and kick it around the loop again.
* This is intended for situations where something substantial has changed, and we
* don't really expect the other end to even care about the cancellation.
* For example, after initialization or self-verification.
* @returns An array of `queueRoomKeyRequest` outputs.
*/
async cancelAndResendAllOutgoingRequests() {
const outgoings = await this.cryptoStore.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent);
return Promise.all(outgoings.map(({
requestBody,
recipients
}) => this.queueRoomKeyRequest(requestBody, recipients, true)));
}
// start the background timer to send queued requests, if the timer isn't
// already running
startTimer() {
if (this.sendOutgoingRoomKeyRequestsTimer) {
return;
}
const startSendingOutgoingRoomKeyRequests = () => {
if (this.sendOutgoingRoomKeyRequestsRunning) {
throw new Error("RoomKeyRequestSend already in progress!");
}
this.sendOutgoingRoomKeyRequestsRunning = true;
this.sendOutgoingRoomKeyRequests().finally(() => {
this.sendOutgoingRoomKeyRequestsRunning = false;
}).catch(e => {
// this should only happen if there is an indexeddb error,
// in which case we're a bit stuffed anyway.
_logger.logger.warn(`error in OutgoingRoomKeyRequestManager: ${e}`);
});
};
this.sendOutgoingRoomKeyRequestsTimer = setTimeout(startSendingOutgoingRoomKeyRequests, SEND_KEY_REQUESTS_DELAY_MS);
}
// look for and send any queued requests. Runs itself recursively until
// there are no more requests, or there is an error (in which case, the
// timer will be restarted before the promise resolves).
async sendOutgoingRoomKeyRequests() {
if (!this.clientRunning) {
this.sendOutgoingRoomKeyRequestsTimer = undefined;
return;
}
const req = await this.cryptoStore.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.CancellationPending, RoomKeyRequestState.CancellationPendingAndWillResend, RoomKeyRequestState.Unsent]);
if (!req) {
this.sendOutgoingRoomKeyRequestsTimer = undefined;
return;
}
try {
switch (req.state) {
case RoomKeyRequestState.Unsent:
await this.sendOutgoingRoomKeyRequest(req);
break;
case RoomKeyRequestState.CancellationPending:
await this.sendOutgoingRoomKeyRequestCancellation(req);
break;
case RoomKeyRequestState.CancellationPendingAndWillResend:
await this.sendOutgoingRoomKeyRequestCancellation(req, true);
break;
}
// go around the loop again
return this.sendOutgoingRoomKeyRequests();
} catch (e) {
_logger.logger.error("Error sending room key request; will retry later.", e);
this.sendOutgoingRoomKeyRequestsTimer = undefined;
}
}
// given a RoomKeyRequest, send it and update the request record
sendOutgoingRoomKeyRequest(req) {
_logger.logger.log(`Requesting keys for ${stringifyRequestBody(req.requestBody)}` + ` from ${stringifyRecipientList(req.recipients)}` + `(id ${req.requestId})`);
const requestMessage = {
action: "request",
requesting_device_id: this.deviceId,
request_id: req.requestId,
body: req.requestBody
};
return this.sendMessageToDevices(requestMessage, req.recipients, req.requestTxnId || req.requestId).then(() => {
return this.cryptoStore.updateOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.Unsent, {
state: RoomKeyRequestState.Sent
});
});
}
// Given a RoomKeyRequest, cancel it and delete the request record unless
// andResend is set, in which case transition to UNSENT.
sendOutgoingRoomKeyRequestCancellation(req, andResend = false) {
_logger.logger.log(`Sending cancellation for key request for ` + `${stringifyRequestBody(req.requestBody)} to ` + `${stringifyRecipientList(req.recipients)} ` + `(cancellation id ${req.cancellationTxnId})`);
const requestMessage = {
action: "request_cancellation",
requesting_device_id: this.deviceId,
request_id: req.requestId
};
return this.sendMessageToDevices(requestMessage, req.recipients, req.cancellationTxnId).then(() => {
if (andResend) {
// We want to resend, so transition to UNSENT
return this.cryptoStore.updateOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.CancellationPendingAndWillResend, {
state: RoomKeyRequestState.Unsent
});
}
return this.cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.CancellationPending);
});
}
// send a RoomKeyRequest to a list of recipients
sendMessageToDevices(message, recipients, txnId) {
const contentMap = {};
for (const recip of recipients) {
if (!contentMap[recip.userId]) {
contentMap[recip.userId] = {};
}
contentMap[recip.userId][recip.deviceId] = _objectSpread(_objectSpread({}, message), {}, {
[_event.ToDeviceMessageId]: (0, _uuid.v4)()
});
}
return this.baseApis.sendToDevice(_event.EventType.RoomKeyRequest, contentMap, txnId);
}
}
exports.OutgoingRoomKeyRequestManager = OutgoingRoomKeyRequestManager;
function stringifyRequestBody(requestBody) {
// we assume that the request is for megolm keys, which are identified by
// room id and session id
return requestBody.room_id + " / " + requestBody.session_id;
}
function stringifyRecipientList(recipients) {
return `[${recipients.map(r => `${r.userId}:${r.deviceId}`).join(",")}]`;
}
//# sourceMappingURL=OutgoingRoomKeyRequestManager.js.map