"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"); var _utils = require("../utils"); 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 = new _utils.MapWithDefault(() => new Map()); for (const recip of recipients) { const userDeviceMap = contentMap.getOrCreate(recip.userId); userDeviceMap.set(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