Add labs flag to automatically rageshake on decryption errors (#7307)
Also sends a to-device message to the sender, prompting them to auto-rageshake too if they have this lab enabled as well. Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
22c2aa37d7
commit
3eb5130cda
7 changed files with 166 additions and 7 deletions
2
src/@types/global.d.ts
vendored
2
src/@types/global.d.ts
vendored
|
@ -52,6 +52,7 @@ import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
|
||||||
import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake";
|
import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake";
|
||||||
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
||||||
import { Skinner } from "../Skinner";
|
import { Skinner } from "../Skinner";
|
||||||
|
import AutoRageshakeStore from "../stores/AutoRageshakeStore";
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
|
||||||
|
@ -111,6 +112,7 @@ declare global {
|
||||||
electron?: Electron;
|
electron?: Electron;
|
||||||
mxSendSentryReport: (userText: string, issueUrl: string, error: Error) => Promise<void>;
|
mxSendSentryReport: (userText: string, issueUrl: string, error: Error) => Promise<void>;
|
||||||
mxLoginWithAccessToken: (hsUrl: string, accessToken: string) => Promise<void>;
|
mxLoginWithAccessToken: (hsUrl: string, accessToken: string) => Promise<void>;
|
||||||
|
mxAutoRageshakeStore?: AutoRageshakeStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DesktopCapturerSource {
|
interface DesktopCapturerSource {
|
||||||
|
|
|
@ -44,6 +44,7 @@ import * as Rooms from '../../Rooms';
|
||||||
import * as Lifecycle from '../../Lifecycle';
|
import * as Lifecycle from '../../Lifecycle';
|
||||||
// LifecycleStore is not used but does listen to and dispatch actions
|
// LifecycleStore is not used but does listen to and dispatch actions
|
||||||
import '../../stores/LifecycleStore';
|
import '../../stores/LifecycleStore';
|
||||||
|
import '../../stores/AutoRageshakeStore';
|
||||||
import PageType from '../../PageTypes';
|
import PageType from '../../PageTypes';
|
||||||
import createRoom, { IOpts } from "../../createRoom";
|
import createRoom, { IOpts } from "../../createRoom";
|
||||||
import { _t, _td, getCurrentLanguage } from '../../languageHandler';
|
import { _t, _td, getCurrentLanguage } from '../../languageHandler';
|
||||||
|
|
|
@ -129,6 +129,11 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
|
||||||
name="automaticErrorReporting"
|
name="automaticErrorReporting"
|
||||||
level={SettingLevel.DEVICE}
|
level={SettingLevel.DEVICE}
|
||||||
/>,
|
/>,
|
||||||
|
<SettingsFlag
|
||||||
|
key="automaticDecryptionErrorReporting"
|
||||||
|
name="automaticDecryptionErrorReporting"
|
||||||
|
level={SettingLevel.DEVICE}
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.state.showHiddenReadReceipts) {
|
if (this.state.showHiddenReadReceipts) {
|
||||||
|
|
|
@ -948,6 +948,7 @@
|
||||||
"Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.",
|
"Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.",
|
||||||
"Developer mode": "Developer mode",
|
"Developer mode": "Developer mode",
|
||||||
"Automatically send debug logs on any error": "Automatically send debug logs on any error",
|
"Automatically send debug logs on any error": "Automatically send debug logs on any error",
|
||||||
|
"Automatically send debug logs on decryption errors": "Automatically send debug logs on decryption errors",
|
||||||
"Collecting app version information": "Collecting app version information",
|
"Collecting app version information": "Collecting app version information",
|
||||||
"Collecting logs": "Collecting logs",
|
"Collecting logs": "Collecting logs",
|
||||||
"Uploading logs": "Uploading logs",
|
"Uploading logs": "Uploading logs",
|
||||||
|
|
|
@ -31,7 +31,8 @@ interface IOpts {
|
||||||
label?: string;
|
label?: string;
|
||||||
userText?: string;
|
userText?: string;
|
||||||
sendLogs?: boolean;
|
sendLogs?: boolean;
|
||||||
progressCallback?: (string) => void;
|
progressCallback?: (s: string) => void;
|
||||||
|
customFields?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function collectBugReport(opts: IOpts = {}, gzipLogs = true) {
|
async function collectBugReport(opts: IOpts = {}, gzipLogs = true) {
|
||||||
|
@ -72,6 +73,12 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) {
|
||||||
body.append('installed_pwa', installedPWA);
|
body.append('installed_pwa', installedPWA);
|
||||||
body.append('touch_input', touchInput);
|
body.append('touch_input', touchInput);
|
||||||
|
|
||||||
|
if (opts.customFields) {
|
||||||
|
for (const key in opts.customFields) {
|
||||||
|
body.append(key, opts.customFields[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (client) {
|
if (client) {
|
||||||
body.append('user_id', client.credentials.userId);
|
body.append('user_id', client.credentials.userId);
|
||||||
body.append('device_id', client.deviceId);
|
body.append('device_id', client.deviceId);
|
||||||
|
@ -191,9 +198,9 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) {
|
||||||
*
|
*
|
||||||
* @param {function(string)} opts.progressCallback Callback to call with progress updates
|
* @param {function(string)} opts.progressCallback Callback to call with progress updates
|
||||||
*
|
*
|
||||||
* @return {Promise} Resolved when the bug report is sent.
|
* @return {Promise<string>} URL returned by the rageshake server
|
||||||
*/
|
*/
|
||||||
export default async function sendBugReport(bugReportEndpoint: string, opts: IOpts = {}) {
|
export default async function sendBugReport(bugReportEndpoint: string, opts: IOpts = {}): Promise<string> {
|
||||||
if (!bugReportEndpoint) {
|
if (!bugReportEndpoint) {
|
||||||
throw new Error("No bug report endpoint has been set.");
|
throw new Error("No bug report endpoint has been set.");
|
||||||
}
|
}
|
||||||
|
@ -202,7 +209,7 @@ export default async function sendBugReport(bugReportEndpoint: string, opts: IOp
|
||||||
const body = await collectBugReport(opts);
|
const body = await collectBugReport(opts);
|
||||||
|
|
||||||
progressCallback(_t("Uploading logs"));
|
progressCallback(_t("Uploading logs"));
|
||||||
await submitReport(bugReportEndpoint, body, progressCallback);
|
return await submitReport(bugReportEndpoint, body, progressCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -291,10 +298,11 @@ export async function submitFeedback(
|
||||||
await submitReport(SdkConfig.get().bug_report_endpoint_url, body, () => {});
|
await submitReport(SdkConfig.get().bug_report_endpoint_url, body, () => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitReport(endpoint: string, body: FormData, progressCallback: (str: string) => void) {
|
function submitReport(endpoint: string, body: FormData, progressCallback: (str: string) => void): Promise<string> {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
const req = new XMLHttpRequest();
|
const req = new XMLHttpRequest();
|
||||||
req.open("POST", endpoint);
|
req.open("POST", endpoint);
|
||||||
|
req.responseType = "json";
|
||||||
req.timeout = 5 * 60 * 1000;
|
req.timeout = 5 * 60 * 1000;
|
||||||
req.onreadystatechange = function() {
|
req.onreadystatechange = function() {
|
||||||
if (req.readyState === XMLHttpRequest.LOADING) {
|
if (req.readyState === XMLHttpRequest.LOADING) {
|
||||||
|
@ -305,7 +313,7 @@ function submitReport(endpoint: string, body: FormData, progressCallback: (str:
|
||||||
reject(new Error(`HTTP ${req.status}`));
|
reject(new Error(`HTTP ${req.status}`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resolve();
|
resolve(req.response.report_url || "");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
req.send(body);
|
req.send(body);
|
||||||
|
|
|
@ -880,6 +880,12 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
default: false,
|
default: false,
|
||||||
controller: new ReloadOnChangeController(),
|
controller: new ReloadOnChangeController(),
|
||||||
},
|
},
|
||||||
|
"automaticDecryptionErrorReporting": {
|
||||||
|
displayName: _td("Automatically send debug logs on decryption errors"),
|
||||||
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||||
|
default: false,
|
||||||
|
controller: new ReloadOnChangeController(),
|
||||||
|
},
|
||||||
[UIFeature.RoomHistorySettings]: {
|
[UIFeature.RoomHistorySettings]: {
|
||||||
supportedLevels: LEVELS_UI_FEATURE,
|
supportedLevels: LEVELS_UI_FEATURE,
|
||||||
default: true,
|
default: true,
|
||||||
|
|
136
src/stores/AutoRageshakeStore.ts
Normal file
136
src/stores/AutoRageshakeStore.ts
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src";
|
||||||
|
|
||||||
|
import SdkConfig from '../SdkConfig';
|
||||||
|
import sendBugReport from '../rageshake/submit-rageshake';
|
||||||
|
import defaultDispatcher from '../dispatcher/dispatcher';
|
||||||
|
import { AsyncStoreWithClient } from './AsyncStoreWithClient';
|
||||||
|
import { ActionPayload } from '../dispatcher/payloads';
|
||||||
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
|
|
||||||
|
// Minimum interval of 5 minutes between reports, especially important when we're doing an initial sync with a lot of decryption errors
|
||||||
|
const RAGESHAKE_INTERVAL = 5*60*1000;
|
||||||
|
// Event type for to-device messages requesting sender auto-rageshakes
|
||||||
|
const AUTO_RS_REQUEST = "im.vector.auto_rs_request";
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
reportedSessionIds: Set<string>;
|
||||||
|
lastRageshakeTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watches for decryption errors to auto-report if the relevant lab is
|
||||||
|
* enabled, and keeps track of session IDs that have already been
|
||||||
|
* reported.
|
||||||
|
*/
|
||||||
|
export default class AutoRageshakeStore extends AsyncStoreWithClient<IState> {
|
||||||
|
private static internalInstance = new AutoRageshakeStore();
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
super(defaultDispatcher, {
|
||||||
|
reportedSessionIds: new Set<string>(),
|
||||||
|
lastRageshakeTime: 0,
|
||||||
|
});
|
||||||
|
this.onDecryptionAttempt = this.onDecryptionAttempt.bind(this);
|
||||||
|
this.onDeviceMessage = this.onDeviceMessage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static get instance(): AutoRageshakeStore {
|
||||||
|
return AutoRageshakeStore.internalInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onAction(payload: ActionPayload) {
|
||||||
|
// we don't actually do anything here
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onReady() {
|
||||||
|
if (!SettingsStore.getValue("automaticDecryptionErrorReporting")) return;
|
||||||
|
|
||||||
|
if (this.matrixClient) {
|
||||||
|
this.matrixClient.on('Event.decrypted', this.onDecryptionAttempt);
|
||||||
|
this.matrixClient.on('toDeviceEvent', this.onDeviceMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onNotReady() {
|
||||||
|
if (this.matrixClient) {
|
||||||
|
this.matrixClient.removeListener('toDeviceEvent', this.onDeviceMessage);
|
||||||
|
this.matrixClient.removeListener('Event.decrypted', this.onDecryptionAttempt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onDecryptionAttempt(ev: MatrixEvent): Promise<void> {
|
||||||
|
const wireContent = ev.getWireContent();
|
||||||
|
const sessionId = wireContent.session_id;
|
||||||
|
if (ev.isDecryptionFailure() && !this.state.reportedSessionIds.has(sessionId)) {
|
||||||
|
const newReportedSessionIds = new Set(this.state.reportedSessionIds);
|
||||||
|
await this.updateState({ reportedSessionIds: newReportedSessionIds.add(sessionId) });
|
||||||
|
|
||||||
|
const now = new Date().getTime();
|
||||||
|
if (now - this.state.lastRageshakeTime < RAGESHAKE_INTERVAL) { return; }
|
||||||
|
|
||||||
|
await this.updateState({ lastRageshakeTime: now });
|
||||||
|
|
||||||
|
const eventInfo = {
|
||||||
|
"event_id": ev.getId(),
|
||||||
|
"room_id": ev.getRoomId(),
|
||||||
|
"session_id": sessionId,
|
||||||
|
"device_id": wireContent.device_id,
|
||||||
|
"user_id": ev.getSender(),
|
||||||
|
"sender_key": wireContent.sender_key,
|
||||||
|
};
|
||||||
|
|
||||||
|
const rageshakeURL = await sendBugReport(SdkConfig.get().bug_report_endpoint_url, {
|
||||||
|
userText: "Auto-reporting decryption error (recipient)",
|
||||||
|
sendLogs: true,
|
||||||
|
label: "Z-UISI",
|
||||||
|
customFields: { "auto_uisi": JSON.stringify(eventInfo) },
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageContent = {
|
||||||
|
...eventInfo,
|
||||||
|
"recipient_rageshake": rageshakeURL,
|
||||||
|
};
|
||||||
|
this.matrixClient.sendToDevice(
|
||||||
|
AUTO_RS_REQUEST,
|
||||||
|
{ [messageContent.user_id]: { [messageContent.device_id]: messageContent } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onDeviceMessage(ev: MatrixEvent): Promise<void> {
|
||||||
|
if (ev.getType() !== AUTO_RS_REQUEST) return;
|
||||||
|
const messageContent = ev.getContent();
|
||||||
|
const recipientRageshake = messageContent["recipient_rageshake"] || "";
|
||||||
|
const now = new Date().getTime();
|
||||||
|
if (now - this.state.lastRageshakeTime > RAGESHAKE_INTERVAL) {
|
||||||
|
await this.updateState({ lastRageshakeTime: now });
|
||||||
|
await sendBugReport(SdkConfig.get().bug_report_endpoint_url, {
|
||||||
|
userText: `Auto-reporting decryption error (sender)\nRecipient rageshake: ${recipientRageshake}`,
|
||||||
|
sendLogs: true,
|
||||||
|
label: "Z-UISI",
|
||||||
|
customFields: {
|
||||||
|
"recipient_rageshake": recipientRageshake,
|
||||||
|
"auto_uisi": JSON.stringify(messageContent),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.mxAutoRageshakeStore = AutoRageshakeStore.instance;
|
Loading…
Reference in a new issue