Alert internal users if first message on websocket is repeated

This commit is contained in:
Scott Nonnenberg 2024-09-21 04:27:39 +10:00 committed by GitHub
parent d0b8a2991f
commit cd2bb537fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 144 additions and 0 deletions

View file

@ -2159,6 +2159,10 @@
"messageformat": "Yesterday {time}", "messageformat": "Yesterday {time}",
"description": "Timestamp format string for displaying \"Yesterday\" and the 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": { "icu:messageBodyTooLong": {
"messageformat": "Message body is too long.", "messageformat": "Message body is too long.",
"description": "Shown if the user tries to send more than 64kb of text" "description": "Shown if the user tries to send more than 64kb of text"

View file

@ -195,6 +195,7 @@ import { encryptConversationAttachments } from './util/encryptConversationAttach
import { DataReader, DataWriter } from './sql/Client'; import { DataReader, DataWriter } from './sql/Client';
import { restoreRemoteConfigFromStorage } from './RemoteConfig'; import { restoreRemoteConfigFromStorage } from './RemoteConfig';
import { getParametersForRedux, loadAll } from './services/allLoaders'; import { getParametersForRedux, loadAll } from './services/allLoaders';
import { checkFirstEnvelope } from './util/checkFirstEnvelope';
export function isOverHourIntoPast(timestamp: number): boolean { export function isOverHourIntoPast(timestamp: number): boolean {
return isNumber(timestamp) && isOlderThan(timestamp, HOUR); return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
@ -481,10 +482,13 @@ export async function startApp(): Promise<void> {
first = false; first = false;
restoreRemoteConfigFromStorage(); restoreRemoteConfigFromStorage();
window.Whisper.events.on('firstEnvelope', checkFirstEnvelope);
server = window.WebAPI.connect({ server = window.WebAPI.connect({
...window.textsecure.storage.user.getWebAPICredentials(), ...window.textsecure.storage.user.getWebAPICredentials(),
hasStoriesDisabled: window.storage.get('hasStoriesDisabled', false), hasStoriesDisabled: window.storage.get('hasStoriesDisabled', false),
}); });
window.textsecure.server = server; window.textsecure.server = server;
window.textsecure.messaging = new window.textsecure.MessageSender(server); window.textsecure.messaging = new window.textsecure.MessageSender(server);

View file

@ -122,6 +122,8 @@ function getToast(toastType: ToastType): AnyToast {
return { toastType: ToastType.MaxAttachments }; return { toastType: ToastType.MaxAttachments };
case ToastType.MessageBodyTooLong: case ToastType.MessageBodyTooLong:
return { toastType: ToastType.MessageBodyTooLong }; return { toastType: ToastType.MessageBodyTooLong };
case ToastType.MessageLoop:
return { toastType: ToastType.MessageLoop };
case ToastType.OriginalMessageNotFound: case ToastType.OriginalMessageNotFound:
return { toastType: ToastType.OriginalMessageNotFound }; return { toastType: ToastType.OriginalMessageNotFound };
case ToastType.PinnedConversationsFull: case ToastType.PinnedConversationsFull:

View file

@ -385,6 +385,20 @@ export function renderToast({
return <Toast onClose={hideToast}>{i18n('icu:messageBodyTooLong')}</Toast>; 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) { if (toastType === ToastType.OriginalMessageNotFound) {
return ( return (
<Toast onClose={hideToast}>{i18n('icu:originalMessageNotFound')}</Toast> <Toast onClose={hideToast}>{i18n('icu:originalMessageNotFound')}</Toast>

View file

@ -34,6 +34,7 @@ import type {
import WebSocketResource, { import WebSocketResource, {
connectAuthenticatedLibsignal, connectAuthenticatedLibsignal,
connectUnauthenticatedLibsignal, connectUnauthenticatedLibsignal,
ServerRequestType,
TransportOption, TransportOption,
WebSocketResourceWithShadowing, WebSocketResourceWithShadowing,
} from './WebsocketResources'; } from './WebsocketResources';
@ -106,6 +107,8 @@ export class SocketManager extends EventListener {
private reconnectController: AbortController | undefined; private reconnectController: AbortController | undefined;
private envelopeCount = 0;
constructor( constructor(
private readonly libsignalNet: Net.Net, private readonly libsignalNet: Net.Net,
private readonly options: SocketManagerOptions private readonly options: SocketManagerOptions
@ -298,6 +301,7 @@ export class SocketManager extends EventListener {
); );
window.logAuthenticatedConnect?.(); window.logAuthenticatedConnect?.();
this.envelopeCount = 0;
this.backOff.reset(); this.backOff.reset();
authenticated.addEventListener('close', ({ code, reason }): void => { authenticated.addEventListener('close', ({ code, reason }): void => {
@ -860,6 +864,12 @@ export class SocketManager extends EventListener {
} }
private queueOrHandleRequest(req: IncomingWebSocketRequest): void { 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) { if (this.requestHandlers.size === 0) {
this.incomingRequestQueue.push(req); this.incomingRequestQueue.push(req);
log.info( log.info(
@ -924,6 +934,10 @@ export class SocketManager extends EventListener {
public override on(type: 'statusChange', callback: () => void): this; public override on(type: 'statusChange', callback: () => void): this;
public override on(type: 'online', callback: () => void): this; public override on(type: 'online', callback: () => void): this;
public override on(type: 'offline', callback: () => void): this; public override on(type: 'offline', callback: () => void): this;
public override on(
type: 'firstEnvelope',
callback: (incoming: IncomingWebSocketRequest) => void
): this;
public override on( public override on(
type: string | symbol, type: string | symbol,
@ -937,6 +951,10 @@ export class SocketManager extends EventListener {
public override emit(type: 'statusChange'): boolean; public override emit(type: 'statusChange'): boolean;
public override emit(type: 'online'): boolean; public override emit(type: 'online'): boolean;
public override emit(type: 'offline'): boolean; public override emit(type: 'offline'): boolean;
public override emit(
type: 'firstEnvelope',
incoming: IncomingWebSocketRequest
): boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
public override emit(type: string | symbol, ...args: Array<any>): boolean { public override emit(type: string | symbol, ...args: Array<any>): boolean {

View file

@ -1668,6 +1668,10 @@ export function initialize({
window.Whisper.events.trigger('unlinkAndDisconnect'); window.Whisper.events.trigger('unlinkAndDisconnect');
}); });
socketManager.on('firstEnvelope', incoming => {
window.Whisper.events.trigger('firstEnvelope', incoming);
});
if (useWebSocket) { if (useWebSocket) {
void socketManager.authenticate({ username, password }); void socketManager.authenticate({ username, password });
} }

View file

@ -43,6 +43,7 @@ export enum ToastType {
LoadingFullLogs = 'LoadingFullLogs', LoadingFullLogs = 'LoadingFullLogs',
MaxAttachments = 'MaxAttachments', MaxAttachments = 'MaxAttachments',
MessageBodyTooLong = 'MessageBodyTooLong', MessageBodyTooLong = 'MessageBodyTooLong',
MessageLoop = 'MessageLoop',
OriginalMessageNotFound = 'OriginalMessageNotFound', OriginalMessageNotFound = 'OriginalMessageNotFound',
PinnedConversationsFull = 'PinnedConversationsFull', PinnedConversationsFull = 'PinnedConversationsFull',
ReactionFailed = 'ReactionFailed', ReactionFailed = 'ReactionFailed',
@ -126,6 +127,7 @@ export type AnyToast =
| { toastType: ToastType.LoadingFullLogs } | { toastType: ToastType.LoadingFullLogs }
| { toastType: ToastType.MaxAttachments } | { toastType: ToastType.MaxAttachments }
| { toastType: ToastType.MessageBodyTooLong } | { toastType: ToastType.MessageBodyTooLong }
| { toastType: ToastType.MessageLoop }
| { toastType: ToastType.OriginalMessageNotFound } | { toastType: ToastType.OriginalMessageNotFound }
| { toastType: ToastType.PinnedConversationsFull } | { toastType: ToastType.PinnedConversationsFull }
| { toastType: ToastType.ReactionFailed } | { toastType: ToastType.ReactionFailed }

View 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,
});
}