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 ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
||||
import { Skinner } from "../Skinner";
|
||||
import AutoRageshakeStore from "../stores/AutoRageshakeStore";
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
|
@ -111,6 +112,7 @@ declare global {
|
|||
electron?: Electron;
|
||||
mxSendSentryReport: (userText: string, issueUrl: string, error: Error) => Promise<void>;
|
||||
mxLoginWithAccessToken: (hsUrl: string, accessToken: string) => Promise<void>;
|
||||
mxAutoRageshakeStore?: AutoRageshakeStore;
|
||||
}
|
||||
|
||||
interface DesktopCapturerSource {
|
||||
|
|
|
@ -44,6 +44,7 @@ import * as Rooms from '../../Rooms';
|
|||
import * as Lifecycle from '../../Lifecycle';
|
||||
// LifecycleStore is not used but does listen to and dispatch actions
|
||||
import '../../stores/LifecycleStore';
|
||||
import '../../stores/AutoRageshakeStore';
|
||||
import PageType from '../../PageTypes';
|
||||
import createRoom, { IOpts } from "../../createRoom";
|
||||
import { _t, _td, getCurrentLanguage } from '../../languageHandler';
|
||||
|
|
|
@ -129,6 +129,11 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
|
|||
name="automaticErrorReporting"
|
||||
level={SettingLevel.DEVICE}
|
||||
/>,
|
||||
<SettingsFlag
|
||||
key="automaticDecryptionErrorReporting"
|
||||
name="automaticDecryptionErrorReporting"
|
||||
level={SettingLevel.DEVICE}
|
||||
/>,
|
||||
);
|
||||
|
||||
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.",
|
||||
"Developer mode": "Developer mode",
|
||||
"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 logs": "Collecting logs",
|
||||
"Uploading logs": "Uploading logs",
|
||||
|
|
|
@ -31,7 +31,8 @@ interface IOpts {
|
|||
label?: string;
|
||||
userText?: string;
|
||||
sendLogs?: boolean;
|
||||
progressCallback?: (string) => void;
|
||||
progressCallback?: (s: string) => void;
|
||||
customFields?: Record<string, string>;
|
||||
}
|
||||
|
||||
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('touch_input', touchInput);
|
||||
|
||||
if (opts.customFields) {
|
||||
for (const key in opts.customFields) {
|
||||
body.append(key, opts.customFields[key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (client) {
|
||||
body.append('user_id', client.credentials.userId);
|
||||
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
|
||||
*
|
||||
* @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) {
|
||||
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);
|
||||
|
||||
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, () => {});
|
||||
}
|
||||
|
||||
function submitReport(endpoint: string, body: FormData, progressCallback: (str: string) => void) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
function submitReport(endpoint: string, body: FormData, progressCallback: (str: string) => void): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const req = new XMLHttpRequest();
|
||||
req.open("POST", endpoint);
|
||||
req.responseType = "json";
|
||||
req.timeout = 5 * 60 * 1000;
|
||||
req.onreadystatechange = function() {
|
||||
if (req.readyState === XMLHttpRequest.LOADING) {
|
||||
|
@ -305,7 +313,7 @@ function submitReport(endpoint: string, body: FormData, progressCallback: (str:
|
|||
reject(new Error(`HTTP ${req.status}`));
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
resolve(req.response.report_url || "");
|
||||
}
|
||||
};
|
||||
req.send(body);
|
||||
|
|
|
@ -880,6 +880,12 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
default: false,
|
||||
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]: {
|
||||
supportedLevels: LEVELS_UI_FEATURE,
|
||||
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