Handle server alerts received on libsignal auth socket
Co-authored-by: trevor-signal <trevor@signal.org>
This commit is contained in:
@@ -512,7 +512,6 @@ export async function startApp(): Promise<void> {
|
||||
restoreRemoteConfigFromStorage();
|
||||
|
||||
window.Whisper.events.on('firstEnvelope', checkFirstEnvelope);
|
||||
window.Whisper.events.on('serverAlerts', handleServerAlerts);
|
||||
|
||||
server = window.WebAPI.connect({
|
||||
...window.textsecure.storage.user.getWebAPICredentials(),
|
||||
@@ -1930,8 +1929,12 @@ export async function startApp(): Promise<void> {
|
||||
function afterEveryAuthConnect() {
|
||||
log.info('afterAuthSocketConnect/afterEveryAuthConnect');
|
||||
|
||||
strictAssert(server, 'afterEveryAuthConnect: server');
|
||||
handleServerAlerts(server.getServerAlerts());
|
||||
|
||||
strictAssert(challengeHandler, 'afterEveryAuthConnect: challengeHandler');
|
||||
drop(challengeHandler.onOnline());
|
||||
|
||||
reconnectBackOff.reset();
|
||||
drop(window.Signal.Services.initializeGroupCredentialFetcher());
|
||||
drop(AttachmentDownloadManager.start());
|
||||
|
@@ -13,18 +13,15 @@ export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
const SUPPORT_PAGE =
|
||||
'https://support.signal.org/hc/articles/8997185514138-Re-connect-your-primary-device-to-continue-using-Signal-Desktop';
|
||||
|
||||
export function CriticalIdlePrimaryDeviceDialog({
|
||||
containerWidthBreakpoint,
|
||||
i18n,
|
||||
}: PropsType): JSX.Element {
|
||||
const learnMoreLink = (parts: Array<string | JSX.Element>) => (
|
||||
<a
|
||||
key="signal-support"
|
||||
// TODO: DESKTOP-8377
|
||||
href="https://support.signal.org/"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<a href={SUPPORT_PAGE} rel="noreferrer" target="_blank">
|
||||
{parts}
|
||||
</a>
|
||||
);
|
||||
|
@@ -13,6 +13,16 @@ export type ServerStateType = ReadonlyDeep<{
|
||||
alerts: Array<ServerAlert>;
|
||||
}>;
|
||||
|
||||
export function parseServerAlertFromHeader(
|
||||
headerValue: string
|
||||
): ServerAlert | undefined {
|
||||
if (headerValue.toLowerCase() === 'critical-idle-primary-device') {
|
||||
return ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
const UPDATE_SERVER_ALERTS = 'server/UPDATE_SERVER_ALERTS';
|
||||
|
@@ -15,6 +15,7 @@ import type { Locator, Page } from 'playwright';
|
||||
import { expect } from 'playwright/test';
|
||||
import type { SignalService } from '../protobuf';
|
||||
import { strictAssert } from '../util/assert';
|
||||
import type { App, Bootstrap } from './bootstrap';
|
||||
|
||||
const debug = createDebug('mock:test:helpers');
|
||||
|
||||
@@ -434,3 +435,29 @@ export async function createCallLink(
|
||||
const testId = await callLinkItem.getAttribute('data-testid');
|
||||
return testId || undefined;
|
||||
}
|
||||
|
||||
export async function setupAppToUseLibsignalWebsockets(
|
||||
bootstrap: Bootstrap
|
||||
): Promise<App> {
|
||||
bootstrap.server.setRemoteConfig(
|
||||
'desktop.experimentalTransportEnabled.alpha',
|
||||
{ enabled: true }
|
||||
);
|
||||
|
||||
bootstrap.server.setRemoteConfig('desktop.experimentalTransport.enableAuth', {
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
// Link & close so that app can get remote config first over non-libsignal websocket,
|
||||
// and then on next app start it will connect via libsignal
|
||||
await bootstrap.linkAndClose();
|
||||
return bootstrap.startApp();
|
||||
}
|
||||
|
||||
export async function assertAppWasUsingLibsignalWebsockets(
|
||||
app: App
|
||||
): Promise<void> {
|
||||
const { authenticated, unauthenticated } = await app.getSocketStatus();
|
||||
assert.strictEqual(authenticated.lastConnectionTransport, 'libsignal');
|
||||
assert.strictEqual(unauthenticated.lastConnectionTransport, 'libsignal');
|
||||
}
|
||||
|
@@ -6,7 +6,12 @@ import { type PrimaryDevice, StorageState } from '@signalapp/mock-server';
|
||||
|
||||
import type { App } from '../playwright';
|
||||
import { Bootstrap } from '../bootstrap';
|
||||
import { typeIntoInput, waitForEnabledComposer } from '../helpers';
|
||||
import {
|
||||
assertAppWasUsingLibsignalWebsockets,
|
||||
setupAppToUseLibsignalWebsockets,
|
||||
typeIntoInput,
|
||||
waitForEnabledComposer,
|
||||
} from '../helpers';
|
||||
import { MINUTE } from '../../util/durations';
|
||||
|
||||
export const debug = createDebug('mock:test:libsignal');
|
||||
@@ -32,23 +37,13 @@ describe('Libsignal-net', function (this: Mocha.Suite) {
|
||||
|
||||
await bootstrap.phone.setStorageState(state);
|
||||
|
||||
bootstrap.server.setRemoteConfig(
|
||||
'desktop.experimentalTransportEnabled.alpha',
|
||||
{ enabled: true }
|
||||
);
|
||||
|
||||
bootstrap.server.setRemoteConfig(
|
||||
'desktop.experimentalTransport.enableAuth',
|
||||
{ enabled: true }
|
||||
);
|
||||
|
||||
// Link & close so that app can get remote config first over non-libsignal websocket,
|
||||
// and then on next app start it will connect via libsignal
|
||||
await bootstrap.linkAndClose();
|
||||
app = await bootstrap.startApp();
|
||||
app = await setupAppToUseLibsignalWebsockets(bootstrap);
|
||||
});
|
||||
|
||||
afterEach(async function (this: Mocha.Context) {
|
||||
debug('confirming that app was actually using libsignal');
|
||||
await assertAppWasUsingLibsignalWebsockets(app);
|
||||
|
||||
if (!bootstrap) {
|
||||
return;
|
||||
}
|
||||
@@ -86,10 +81,5 @@ describe('Libsignal-net', function (this: Mocha.Suite) {
|
||||
|
||||
debug('confirming app successfully sent message');
|
||||
await app.waitForMessageSend();
|
||||
|
||||
debug('confirming that app was actually using libsignal');
|
||||
const { authenticated, unauthenticated } = await app.getSocketStatus();
|
||||
assert.strictEqual(authenticated.lastConnectionTransport, 'libsignal');
|
||||
assert.strictEqual(unauthenticated.lastConnectionTransport, 'libsignal');
|
||||
});
|
||||
});
|
||||
|
@@ -4,7 +4,11 @@ import createDebug from 'debug';
|
||||
|
||||
import type { App } from '../playwright';
|
||||
import { Bootstrap } from '../bootstrap';
|
||||
import { getLeftPane } from '../helpers';
|
||||
import {
|
||||
assertAppWasUsingLibsignalWebsockets,
|
||||
getLeftPane,
|
||||
setupAppToUseLibsignalWebsockets,
|
||||
} from '../helpers';
|
||||
import { MINUTE } from '../../util/durations';
|
||||
|
||||
export const debug = createDebug('mock:test:serverAlerts');
|
||||
@@ -29,7 +33,7 @@ describe('serverAlerts', function (this: Mocha.Suite) {
|
||||
await bootstrap.teardown();
|
||||
});
|
||||
|
||||
it('shows idle primary device alert', async () => {
|
||||
it('shows critical idle primary device alert using classic desktop socket', async () => {
|
||||
bootstrap.server.setWebsocketUpgradeResponseHeaders({
|
||||
'X-Signal-Alert': 'critical-idle-primary-device',
|
||||
});
|
||||
@@ -37,4 +41,18 @@ describe('serverAlerts', function (this: Mocha.Suite) {
|
||||
const window = await app.getWindow();
|
||||
await getLeftPane(window).getByText('Open Signal on your phone').waitFor();
|
||||
});
|
||||
|
||||
it('shows critical idle primary device alert using libsignal socket', async () => {
|
||||
bootstrap.server.setWebsocketUpgradeResponseHeaders({
|
||||
'X-Signal-Alert': 'critical-idle-primary-device',
|
||||
});
|
||||
|
||||
app = await setupAppToUseLibsignalWebsockets(bootstrap);
|
||||
|
||||
const window = await app.getWindow();
|
||||
await getLeftPane(window).getByText('Open Signal on your phone').waitFor();
|
||||
|
||||
debug('confirming that app was actually using libsignal');
|
||||
await assertAppWasUsingLibsignalWebsockets(app);
|
||||
});
|
||||
});
|
||||
|
@@ -51,7 +51,8 @@ import { isNightly, isBeta, isStaging } from '../util/version';
|
||||
import { getBasicAuth } from '../util/getBasicAuth';
|
||||
import { isTestOrMockEnvironment } from '../environment';
|
||||
import type { ConfigKeyType } from '../RemoteConfig';
|
||||
import { ServerAlert } from '../state/ducks/server';
|
||||
import type { ServerAlert } from '../state/ducks/server';
|
||||
import { parseServerAlertFromHeader } from '../state/ducks/server';
|
||||
|
||||
const FIVE_MINUTES = 5 * durations.MINUTE;
|
||||
|
||||
@@ -205,6 +206,9 @@ export class SocketManager extends EventListener {
|
||||
handler: (req: IncomingWebSocketRequest): void => {
|
||||
this.#queueOrHandleRequest(req);
|
||||
},
|
||||
onReceivedAlerts: (alerts: Array<ServerAlert>) => {
|
||||
this.emit('serverAlerts', alerts);
|
||||
},
|
||||
receiveStories: !this.#hasStoriesDisabled,
|
||||
keepalive: { path: '/v1/keepalive' },
|
||||
})
|
||||
@@ -972,12 +976,9 @@ export class SocketManager extends EventListener {
|
||||
}
|
||||
}
|
||||
|
||||
const serverAlerts: Array<ServerAlert> = [];
|
||||
alerts.forEach(alert => {
|
||||
if (alert.toLowerCase() === 'critical-idle-primary-device') {
|
||||
serverAlerts.push(ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE);
|
||||
}
|
||||
});
|
||||
const serverAlerts: Array<ServerAlert> = alerts
|
||||
.map(parseServerAlertFromHeader)
|
||||
.filter(v => v !== undefined);
|
||||
|
||||
this.emit('serverAlerts', serverAlerts);
|
||||
}
|
||||
|
@@ -82,6 +82,7 @@ import { getMockServerPort } from '../util/getMockServerPort';
|
||||
import { pemToDer } from '../util/pemToDer';
|
||||
import { ToastType } from '../types/Toast';
|
||||
import { isProduction } from '../util/version';
|
||||
import type { ServerAlert } from '../state/ducks/server';
|
||||
|
||||
// Note: this will break some code that expects to be able to use err.response when a
|
||||
// web request fails, because it will force it to text. But it is very useful for
|
||||
@@ -1606,6 +1607,7 @@ export type WebAPIType = {
|
||||
getConfig: () => Promise<RemoteConfigResponseType>;
|
||||
authenticate: (credentials: WebAPICredentials) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
getServerAlerts: () => Array<ServerAlert>;
|
||||
getSocketStatus: () => SocketStatuses;
|
||||
registerRequestHandler: (handler: IRequestHandler) => void;
|
||||
unregisterRequestHandler: (handler: IRequestHandler) => void;
|
||||
@@ -1761,6 +1763,10 @@ export function initialize({
|
||||
}
|
||||
}
|
||||
|
||||
// We store server alerts (returned on the WS upgrade response headers) so that the app
|
||||
// can query them later, which is necessary if they arrive before app state is ready
|
||||
let serverAlerts: Array<ServerAlert> = [];
|
||||
|
||||
// Thanks to function-hoisting, we can put this return statement before all of the
|
||||
// below function definitions.
|
||||
return {
|
||||
@@ -1813,7 +1819,8 @@ export function initialize({
|
||||
});
|
||||
|
||||
socketManager.on('serverAlerts', alerts => {
|
||||
window.Whisper.events.trigger('serverAlerts', alerts);
|
||||
log.info(`onServerAlerts: number of alerts received: ${alerts.length}`);
|
||||
serverAlerts = alerts;
|
||||
});
|
||||
|
||||
if (useWebSocket) {
|
||||
@@ -1939,6 +1946,7 @@ export function initialize({
|
||||
getReleaseNoteImageAttachment,
|
||||
getTransferArchive,
|
||||
getSenderCertificate,
|
||||
getServerAlerts,
|
||||
getSocketStatus,
|
||||
getSticker,
|
||||
getStickerPackManifest,
|
||||
@@ -2143,6 +2151,10 @@ export function initialize({
|
||||
return socketManager.getStatus();
|
||||
}
|
||||
|
||||
function getServerAlerts(): Array<ServerAlert> {
|
||||
return serverAlerts;
|
||||
}
|
||||
|
||||
function checkSockets(): void {
|
||||
// Intentionally not awaiting
|
||||
void socketManager.check();
|
||||
|
@@ -58,6 +58,8 @@ import { AbortableProcess } from '../util/AbortableProcess';
|
||||
import type { WebAPICredentials } from './Types';
|
||||
import { NORMAL_DISCONNECT_CODE } from './SocketManager';
|
||||
import { parseUnknown } from '../util/schemas';
|
||||
import type { ServerAlert } from '../state/ducks/server';
|
||||
import { parseServerAlertFromHeader } from '../state/ducks/server';
|
||||
|
||||
const THIRTY_SECONDS = 30 * durations.SECOND;
|
||||
|
||||
@@ -345,11 +347,13 @@ export function connectAuthenticatedLibsignal({
|
||||
handler,
|
||||
receiveStories,
|
||||
keepalive,
|
||||
onReceivedAlerts,
|
||||
}: {
|
||||
libsignalNet: Net.Net;
|
||||
name: string;
|
||||
credentials: WebAPICredentials;
|
||||
handler: (request: IncomingWebSocketRequest) => void;
|
||||
onReceivedAlerts: (alerts: Array<ServerAlert>) => void;
|
||||
receiveStories: boolean;
|
||||
keepalive: KeepAliveOptionsType;
|
||||
}): AbortableProcess<LibsignalWebSocketResource> {
|
||||
@@ -391,6 +395,11 @@ export function connectAuthenticatedLibsignal({
|
||||
this.resource.onConnectionInterrupted(cause);
|
||||
this.resource = undefined;
|
||||
},
|
||||
onReceivedAlerts(alerts: Array<string>): void {
|
||||
onReceivedAlerts(
|
||||
alerts.map(parseServerAlertFromHeader).filter(v => v !== undefined)
|
||||
);
|
||||
},
|
||||
};
|
||||
return connectLibsignal(
|
||||
(abortSignal: AbortSignal) =>
|
||||
|
Reference in New Issue
Block a user