Alert internal users if first message on websocket is repeated
This commit is contained in:
parent
d0b8a2991f
commit
cd2bb537fa
8 changed files with 144 additions and 0 deletions
|
@ -2159,6 +2159,10 @@
|
|||
"messageformat": "Yesterday {time}",
|
||||
"description": "Timestamp format string for displaying \"Yesterday\" and the time"
|
||||
},
|
||||
"icu:messageLoop": {
|
||||
"messageformat": "Signal may be failing to process an incoming message.",
|
||||
"description": "Shown to non-production users if the same envelope timestamp is the first three times within an hour."
|
||||
},
|
||||
"icu:messageBodyTooLong": {
|
||||
"messageformat": "Message body is too long.",
|
||||
"description": "Shown if the user tries to send more than 64kb of text"
|
||||
|
|
|
@ -195,6 +195,7 @@ import { encryptConversationAttachments } from './util/encryptConversationAttach
|
|||
import { DataReader, DataWriter } from './sql/Client';
|
||||
import { restoreRemoteConfigFromStorage } from './RemoteConfig';
|
||||
import { getParametersForRedux, loadAll } from './services/allLoaders';
|
||||
import { checkFirstEnvelope } from './util/checkFirstEnvelope';
|
||||
|
||||
export function isOverHourIntoPast(timestamp: number): boolean {
|
||||
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
|
||||
|
@ -481,10 +482,13 @@ export async function startApp(): Promise<void> {
|
|||
first = false;
|
||||
|
||||
restoreRemoteConfigFromStorage();
|
||||
|
||||
window.Whisper.events.on('firstEnvelope', checkFirstEnvelope);
|
||||
server = window.WebAPI.connect({
|
||||
...window.textsecure.storage.user.getWebAPICredentials(),
|
||||
hasStoriesDisabled: window.storage.get('hasStoriesDisabled', false),
|
||||
});
|
||||
|
||||
window.textsecure.server = server;
|
||||
window.textsecure.messaging = new window.textsecure.MessageSender(server);
|
||||
|
||||
|
|
|
@ -122,6 +122,8 @@ function getToast(toastType: ToastType): AnyToast {
|
|||
return { toastType: ToastType.MaxAttachments };
|
||||
case ToastType.MessageBodyTooLong:
|
||||
return { toastType: ToastType.MessageBodyTooLong };
|
||||
case ToastType.MessageLoop:
|
||||
return { toastType: ToastType.MessageLoop };
|
||||
case ToastType.OriginalMessageNotFound:
|
||||
return { toastType: ToastType.OriginalMessageNotFound };
|
||||
case ToastType.PinnedConversationsFull:
|
||||
|
|
|
@ -385,6 +385,20 @@ export function renderToast({
|
|||
return <Toast onClose={hideToast}>{i18n('icu:messageBodyTooLong')}</Toast>;
|
||||
}
|
||||
|
||||
if (toastType === ToastType.MessageLoop) {
|
||||
return (
|
||||
<Toast
|
||||
onClose={hideToast}
|
||||
toastAction={{
|
||||
label: i18n('icu:Toast__ActionLabel--SubmitLog'),
|
||||
onClick: onShowDebugLog,
|
||||
}}
|
||||
>
|
||||
{i18n('icu:messageLoop')}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.OriginalMessageNotFound) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>{i18n('icu:originalMessageNotFound')}</Toast>
|
||||
|
|
|
@ -34,6 +34,7 @@ import type {
|
|||
import WebSocketResource, {
|
||||
connectAuthenticatedLibsignal,
|
||||
connectUnauthenticatedLibsignal,
|
||||
ServerRequestType,
|
||||
TransportOption,
|
||||
WebSocketResourceWithShadowing,
|
||||
} from './WebsocketResources';
|
||||
|
@ -106,6 +107,8 @@ export class SocketManager extends EventListener {
|
|||
|
||||
private reconnectController: AbortController | undefined;
|
||||
|
||||
private envelopeCount = 0;
|
||||
|
||||
constructor(
|
||||
private readonly libsignalNet: Net.Net,
|
||||
private readonly options: SocketManagerOptions
|
||||
|
@ -298,6 +301,7 @@ export class SocketManager extends EventListener {
|
|||
);
|
||||
|
||||
window.logAuthenticatedConnect?.();
|
||||
this.envelopeCount = 0;
|
||||
this.backOff.reset();
|
||||
|
||||
authenticated.addEventListener('close', ({ code, reason }): void => {
|
||||
|
@ -860,6 +864,12 @@ export class SocketManager extends EventListener {
|
|||
}
|
||||
|
||||
private queueOrHandleRequest(req: IncomingWebSocketRequest): void {
|
||||
if (req.requestType === ServerRequestType.ApiMessage) {
|
||||
this.envelopeCount += 1;
|
||||
if (this.envelopeCount === 1) {
|
||||
this.emit('firstEnvelope', req);
|
||||
}
|
||||
}
|
||||
if (this.requestHandlers.size === 0) {
|
||||
this.incomingRequestQueue.push(req);
|
||||
log.info(
|
||||
|
@ -924,6 +934,10 @@ export class SocketManager extends EventListener {
|
|||
public override on(type: 'statusChange', callback: () => void): this;
|
||||
public override on(type: 'online', callback: () => void): this;
|
||||
public override on(type: 'offline', callback: () => void): this;
|
||||
public override on(
|
||||
type: 'firstEnvelope',
|
||||
callback: (incoming: IncomingWebSocketRequest) => void
|
||||
): this;
|
||||
|
||||
public override on(
|
||||
type: string | symbol,
|
||||
|
@ -937,6 +951,10 @@ export class SocketManager extends EventListener {
|
|||
public override emit(type: 'statusChange'): boolean;
|
||||
public override emit(type: 'online'): boolean;
|
||||
public override emit(type: 'offline'): boolean;
|
||||
public override emit(
|
||||
type: 'firstEnvelope',
|
||||
incoming: IncomingWebSocketRequest
|
||||
): boolean;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public override emit(type: string | symbol, ...args: Array<any>): boolean {
|
||||
|
|
|
@ -1668,6 +1668,10 @@ export function initialize({
|
|||
window.Whisper.events.trigger('unlinkAndDisconnect');
|
||||
});
|
||||
|
||||
socketManager.on('firstEnvelope', incoming => {
|
||||
window.Whisper.events.trigger('firstEnvelope', incoming);
|
||||
});
|
||||
|
||||
if (useWebSocket) {
|
||||
void socketManager.authenticate({ username, password });
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ export enum ToastType {
|
|||
LoadingFullLogs = 'LoadingFullLogs',
|
||||
MaxAttachments = 'MaxAttachments',
|
||||
MessageBodyTooLong = 'MessageBodyTooLong',
|
||||
MessageLoop = 'MessageLoop',
|
||||
OriginalMessageNotFound = 'OriginalMessageNotFound',
|
||||
PinnedConversationsFull = 'PinnedConversationsFull',
|
||||
ReactionFailed = 'ReactionFailed',
|
||||
|
@ -126,6 +127,7 @@ export type AnyToast =
|
|||
| { toastType: ToastType.LoadingFullLogs }
|
||||
| { toastType: ToastType.MaxAttachments }
|
||||
| { toastType: ToastType.MessageBodyTooLong }
|
||||
| { toastType: ToastType.MessageLoop }
|
||||
| { toastType: ToastType.OriginalMessageNotFound }
|
||||
| { toastType: ToastType.PinnedConversationsFull }
|
||||
| { toastType: ToastType.ReactionFailed }
|
||||
|
|
96
ts/util/checkFirstEnvelope.ts
Normal file
96
ts/util/checkFirstEnvelope.ts
Normal file
|
@ -0,0 +1,96 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { isNumber } from 'lodash';
|
||||
|
||||
import * as log from '../logging/log';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
import { ToastType } from '../types/Toast';
|
||||
import { HOUR, SECOND } from './durations';
|
||||
import { isOlderThan } from './timestamp';
|
||||
import { isProduction } from './version';
|
||||
|
||||
import type { IncomingWebSocketRequest } from '../textsecure/WebsocketResources';
|
||||
|
||||
const FIRST_ENVELOPE_COUNT = 5;
|
||||
const FIRST_ENVELOPE_TIME = HOUR;
|
||||
type FirstEnvelopeStats = {
|
||||
kickoffTimestamp: number;
|
||||
envelopeTimestamp: number;
|
||||
count: number;
|
||||
};
|
||||
let firstEnvelopeStats: FirstEnvelopeStats | undefined;
|
||||
|
||||
export function checkFirstEnvelope(incoming: IncomingWebSocketRequest): void {
|
||||
const { body: plaintext } = incoming;
|
||||
if (!plaintext) {
|
||||
log.warn('checkFirstEnvelope: body was not present!');
|
||||
return;
|
||||
}
|
||||
|
||||
const decoded = Proto.Envelope.decode(plaintext);
|
||||
const newEnvelopeTimestamp = decoded.timestamp?.toNumber();
|
||||
if (!isNumber(newEnvelopeTimestamp)) {
|
||||
log.warn('checkFirstEnvelope: timestamp is not a number!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!firstEnvelopeStats ||
|
||||
firstEnvelopeStats.envelopeTimestamp !== newEnvelopeTimestamp ||
|
||||
isOlderThan(firstEnvelopeStats.kickoffTimestamp, FIRST_ENVELOPE_TIME)
|
||||
) {
|
||||
firstEnvelopeStats = {
|
||||
kickoffTimestamp: Date.now(),
|
||||
envelopeTimestamp: newEnvelopeTimestamp,
|
||||
count: 1,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const { count, kickoffTimestamp } = firstEnvelopeStats;
|
||||
const newCount = count + 1;
|
||||
|
||||
if (newCount < FIRST_ENVELOPE_COUNT) {
|
||||
firstEnvelopeStats = {
|
||||
...firstEnvelopeStats,
|
||||
count: newCount,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
log.warn(
|
||||
`checkFirstEnvelope: Timestamp ${newEnvelopeTimestamp} has been seen ${newCount} times since ${kickoffTimestamp}`
|
||||
);
|
||||
if (isProduction(window.getVersion())) {
|
||||
return;
|
||||
}
|
||||
|
||||
firstEnvelopeStats = undefined;
|
||||
|
||||
if (isReduxInitialized()) {
|
||||
showToast();
|
||||
} else {
|
||||
const interval = setInterval(() => {
|
||||
if (isReduxInitialized()) {
|
||||
clearInterval(interval);
|
||||
showToast();
|
||||
}
|
||||
}, 5 * SECOND);
|
||||
}
|
||||
}
|
||||
|
||||
function isReduxInitialized() {
|
||||
const result = Boolean(window.reduxActions);
|
||||
log.info(
|
||||
`checkFirstEnvelope: Is redux initialized? ${result ? 'Yes' : 'No'}`
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
function showToast() {
|
||||
log.info('checkFirstEnvelope: Showing toast asking user to submit logs');
|
||||
window.reduxActions.toast.showToast({
|
||||
toastType: ToastType.MessageLoop,
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue