Add mock test for libsignal websockets
This commit is contained in:
@@ -219,7 +219,7 @@
|
|||||||
"@indutny/parallel-prettier": "3.0.0",
|
"@indutny/parallel-prettier": "3.0.0",
|
||||||
"@indutny/rezip-electron": "2.0.1",
|
"@indutny/rezip-electron": "2.0.1",
|
||||||
"@napi-rs/canvas": "0.1.61",
|
"@napi-rs/canvas": "0.1.61",
|
||||||
"@signalapp/mock-server": "10.5.0",
|
"@signalapp/mock-server": "11.0.0",
|
||||||
"@storybook/addon-a11y": "8.4.4",
|
"@storybook/addon-a11y": "8.4.4",
|
||||||
"@storybook/addon-actions": "8.4.4",
|
"@storybook/addon-actions": "8.4.4",
|
||||||
"@storybook/addon-controls": "8.4.4",
|
"@storybook/addon-controls": "8.4.4",
|
||||||
|
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@@ -432,8 +432,8 @@ importers:
|
|||||||
specifier: 0.1.61
|
specifier: 0.1.61
|
||||||
version: 0.1.61
|
version: 0.1.61
|
||||||
'@signalapp/mock-server':
|
'@signalapp/mock-server':
|
||||||
specifier: 10.5.0
|
specifier: 11.0.0
|
||||||
version: 10.5.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)
|
version: 11.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)
|
||||||
'@storybook/addon-a11y':
|
'@storybook/addon-a11y':
|
||||||
specifier: 8.4.4
|
specifier: 8.4.4
|
||||||
version: 8.4.4(storybook@8.4.4(bufferutil@4.0.9)(prettier@3.3.3)(utf-8-validate@5.0.10))
|
version: 8.4.4(storybook@8.4.4(bufferutil@4.0.9)(prettier@3.3.3)(utf-8-validate@5.0.10))
|
||||||
@@ -2530,8 +2530,8 @@ packages:
|
|||||||
'@signalapp/libsignal-client@0.66.2':
|
'@signalapp/libsignal-client@0.66.2':
|
||||||
resolution: {integrity: sha512-zwgU0LXIJjJNVBxJWnjbJN1+gIUJ+eY+BnqIP1d0751Sq1zy5VMHs+/5nmEyVGNn6ptD0qseYXkxtr/XLs07Lw==}
|
resolution: {integrity: sha512-zwgU0LXIJjJNVBxJWnjbJN1+gIUJ+eY+BnqIP1d0751Sq1zy5VMHs+/5nmEyVGNn6ptD0qseYXkxtr/XLs07Lw==}
|
||||||
|
|
||||||
'@signalapp/mock-server@10.5.0':
|
'@signalapp/mock-server@11.0.0':
|
||||||
resolution: {integrity: sha512-w7KXcWYRPXhAxYWzeBNesu+A9DO6FYJUhg52md3M9yQkPR2Yr/YBvc9zBhFo5fafZmkaRoxDoz9y29Ivd5ZykQ==}
|
resolution: {integrity: sha512-JHEqdjXvWcXyLJ90OtaTIQVtizH/w+rmnPplNmHuUiDZIQIEvE3lRyZFyIvgVTNg29lQWs/Gw/o+hICerdOChQ==}
|
||||||
|
|
||||||
'@signalapp/parchment-cjs@3.0.1':
|
'@signalapp/parchment-cjs@3.0.1':
|
||||||
resolution: {integrity: sha512-hSBMQ1M7wE4GcC8ZeNtvpJF+DAJg3eIRRf1SiHS3I3Algav/sgJJNm6HIYm6muHuK7IJmuEjkL3ILSXgmu0RfQ==}
|
resolution: {integrity: sha512-hSBMQ1M7wE4GcC8ZeNtvpJF+DAJg3eIRRf1SiHS3I3Algav/sgJJNm6HIYm6muHuK7IJmuEjkL3ILSXgmu0RfQ==}
|
||||||
@@ -6802,9 +6802,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==}
|
resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==}
|
||||||
engines: {node: '>= 0.6.0'}
|
engines: {node: '>= 0.6.0'}
|
||||||
|
|
||||||
long@4.0.0:
|
|
||||||
resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==}
|
|
||||||
|
|
||||||
long@5.2.3:
|
long@5.2.3:
|
||||||
resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==}
|
resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==}
|
||||||
|
|
||||||
@@ -12262,7 +12259,7 @@ snapshots:
|
|||||||
type-fest: 4.26.1
|
type-fest: 4.26.1
|
||||||
uuid: 8.3.2
|
uuid: 8.3.2
|
||||||
|
|
||||||
'@signalapp/mock-server@10.5.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)':
|
'@signalapp/mock-server@11.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@indutny/parallel-prettier': 3.0.0(prettier@3.3.3)
|
'@indutny/parallel-prettier': 3.0.0(prettier@3.3.3)
|
||||||
'@signalapp/libsignal-client': 0.60.2
|
'@signalapp/libsignal-client': 0.60.2
|
||||||
@@ -12270,7 +12267,7 @@ snapshots:
|
|||||||
'@tus/server': 1.10.2
|
'@tus/server': 1.10.2
|
||||||
debug: 4.3.7(supports-color@8.1.1)
|
debug: 4.3.7(supports-color@8.1.1)
|
||||||
is-plain-obj: 3.0.0
|
is-plain-obj: 3.0.0
|
||||||
long: 4.0.0
|
long: 5.2.3
|
||||||
micro: 9.4.1
|
micro: 9.4.1
|
||||||
microrouter: 3.1.3
|
microrouter: 3.1.3
|
||||||
prettier: 3.3.3
|
prettier: 3.3.3
|
||||||
@@ -17566,8 +17563,6 @@ snapshots:
|
|||||||
|
|
||||||
loglevel@1.9.2: {}
|
loglevel@1.9.2: {}
|
||||||
|
|
||||||
long@4.0.0: {}
|
|
||||||
|
|
||||||
long@5.2.3: {}
|
long@5.2.3: {}
|
||||||
|
|
||||||
longest-streak@2.0.4: {}
|
longest-streak@2.0.4: {}
|
||||||
|
7
ts/CI.ts
7
ts/CI.ts
@@ -17,6 +17,7 @@ import { SECOND } from './util/durations';
|
|||||||
import { isSignalRoute } from './util/signalRoutes';
|
import { isSignalRoute } from './util/signalRoutes';
|
||||||
import { strictAssert } from './util/assert';
|
import { strictAssert } from './util/assert';
|
||||||
import { MessageModel } from './models/messages';
|
import { MessageModel } from './models/messages';
|
||||||
|
import type { SocketStatuses } from './textsecure/SocketManager';
|
||||||
|
|
||||||
type ResolveType = (data: unknown) => void;
|
type ResolveType = (data: unknown) => void;
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ export type CIType = {
|
|||||||
sentAt: number
|
sentAt: number
|
||||||
): Promise<ReadonlyArray<MessageAttributesType>>;
|
): Promise<ReadonlyArray<MessageAttributesType>>;
|
||||||
getPendingEventCount: (event: string) => number;
|
getPendingEventCount: (event: string) => number;
|
||||||
|
getSocketStatus: () => SocketStatuses;
|
||||||
handleEvent: (event: string, data: unknown) => unknown;
|
handleEvent: (event: string, data: unknown) => unknown;
|
||||||
setProvisioningURL: (url: string) => unknown;
|
setProvisioningURL: (url: string) => unknown;
|
||||||
solveChallenge: (response: ChallengeResponseType) => unknown;
|
solveChallenge: (response: ChallengeResponseType) => unknown;
|
||||||
@@ -202,6 +204,10 @@ export function getCI({
|
|||||||
handleEvent('print', format(...args));
|
handleEvent('print', format(...args));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSocketStatus() {
|
||||||
|
return window.getSocketStatus();
|
||||||
|
}
|
||||||
|
|
||||||
async function resetReleaseNotesFetcher() {
|
async function resetReleaseNotesFetcher() {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
window.textsecure.storage.put(
|
window.textsecure.storage.put(
|
||||||
@@ -218,6 +224,7 @@ export function getCI({
|
|||||||
getConversationId,
|
getConversationId,
|
||||||
createNotificationToken,
|
createNotificationToken,
|
||||||
getMessagesBySentAt,
|
getMessagesBySentAt,
|
||||||
|
getSocketStatus,
|
||||||
handleEvent,
|
handleEvent,
|
||||||
setProvisioningURL,
|
setProvisioningURL,
|
||||||
solveChallenge,
|
solveChallenge,
|
||||||
|
@@ -398,7 +398,10 @@ export async function startApp(): Promise<void> {
|
|||||||
|
|
||||||
window.getSocketStatus = () => {
|
window.getSocketStatus = () => {
|
||||||
if (server === undefined) {
|
if (server === undefined) {
|
||||||
return SocketStatus.CLOSED;
|
return {
|
||||||
|
authenticated: { status: SocketStatus.CLOSED },
|
||||||
|
unauthenticated: { status: SocketStatus.CLOSED },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return server.getSocketStatus();
|
return server.getSocketStatus();
|
||||||
};
|
};
|
||||||
@@ -1141,7 +1144,8 @@ export async function startApp(): Promise<void> {
|
|||||||
setupAppState();
|
setupAppState();
|
||||||
drop(start());
|
drop(start());
|
||||||
window.Signal.Services.initializeNetworkObserver(
|
window.Signal.Services.initializeNetworkObserver(
|
||||||
window.reduxActions.network
|
window.reduxActions.network,
|
||||||
|
() => window.getSocketStatus().authenticated.status
|
||||||
);
|
);
|
||||||
window.Signal.Services.initializeUpdateListener(
|
window.Signal.Services.initializeUpdateListener(
|
||||||
window.reduxActions.updates
|
window.reduxActions.updates
|
||||||
@@ -1947,7 +1951,7 @@ export async function startApp(): Promise<void> {
|
|||||||
window.addEventListener('offline', onNavigatorOffline);
|
window.addEventListener('offline', onNavigatorOffline);
|
||||||
|
|
||||||
window.Whisper.events.on('socketStatusChange', () => {
|
window.Whisper.events.on('socketStatusChange', () => {
|
||||||
if (window.getSocketStatus() === SocketStatus.OPEN) {
|
if (window.getSocketStatus().authenticated.status === SocketStatus.OPEN) {
|
||||||
pauseQueuesAndNotificationsOnSocketConnect();
|
pauseQueuesAndNotificationsOnSocketConnect();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1976,7 +1980,7 @@ export async function startApp(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isSocketOnline() {
|
function isSocketOnline() {
|
||||||
const socketStatus = window.getSocketStatus();
|
const socketStatus = window.getSocketStatus().authenticated.status;
|
||||||
return (
|
return (
|
||||||
socketStatus === SocketStatus.CONNECTING ||
|
socketStatus === SocketStatus.CONNECTING ||
|
||||||
socketStatus === SocketStatus.OPEN
|
socketStatus === SocketStatus.OPEN
|
||||||
|
@@ -61,7 +61,7 @@ export function Inbox({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
const status = window.getSocketStatus();
|
const { status } = window.getSocketStatus().authenticated;
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'CONNECTING':
|
case 'CONNECTING':
|
||||||
break;
|
break;
|
||||||
|
@@ -5,7 +5,6 @@ import type {
|
|||||||
SetNetworkStatusPayloadType,
|
SetNetworkStatusPayloadType,
|
||||||
NetworkActionType,
|
NetworkActionType,
|
||||||
} from '../state/ducks/network';
|
} from '../state/ducks/network';
|
||||||
import { getSocketStatus } from '../shims/socketStatus';
|
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import { SECOND } from '../util/durations';
|
import { SECOND } from '../util/durations';
|
||||||
import { electronLookup } from '../util/dns';
|
import { electronLookup } from '../util/dns';
|
||||||
@@ -31,14 +30,15 @@ type NetworkActions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function initializeNetworkObserver(
|
export function initializeNetworkObserver(
|
||||||
networkActions: NetworkActions
|
networkActions: NetworkActions,
|
||||||
|
getAuthSocketStatus: () => SocketStatus
|
||||||
): void {
|
): void {
|
||||||
log.info('Initializing network observer');
|
log.info('Initializing network observer');
|
||||||
|
|
||||||
let onlineStatus = OnlineStatus.Online;
|
let onlineStatus = OnlineStatus.Online;
|
||||||
|
|
||||||
const refresh = () => {
|
const refresh = () => {
|
||||||
const socketStatus = getSocketStatus();
|
const socketStatus = getAuthSocketStatus();
|
||||||
|
|
||||||
networkActions.setNetworkStatus({
|
networkActions.setNetworkStatus({
|
||||||
isOnline: onlineStatus !== OnlineStatus.Offline,
|
isOnline: onlineStatus !== OnlineStatus.Offline,
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
// Copyright 2020 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import type { SocketStatus } from '../types/SocketStatus';
|
|
||||||
|
|
||||||
export function getSocketStatus(): SocketStatus {
|
|
||||||
const { getSocketStatus: getMessageReceiverStatus } = window;
|
|
||||||
|
|
||||||
return getMessageReceiverStatus();
|
|
||||||
}
|
|
@@ -142,6 +142,15 @@ function sanitizePathComponent(component: string): string {
|
|||||||
return normalizePath(component.replace(/[^a-z]+/gi, '-'));
|
return normalizePath(component.replace(/[^a-z]+/gi, '-'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_REMOTE_CONFIG = [
|
||||||
|
['desktop.backup.credentialFetch', { enabled: true }],
|
||||||
|
['desktop.internalUser', { enabled: true }],
|
||||||
|
['desktop.releaseNotes', { enabled: true }],
|
||||||
|
['desktop.senderKey.retry', { enabled: true }],
|
||||||
|
['global.groupsv2.groupSizeHardLimit', { enabled: true, value: '64' }],
|
||||||
|
['global.groupsv2.maxGroupSize', { enabled: true, value: '32' }],
|
||||||
|
] as const;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Bootstrap is a class that prepares mock server and desktop for running
|
// Bootstrap is a class that prepares mock server and desktop for running
|
||||||
// tests/benchmarks.
|
// tests/benchmarks.
|
||||||
@@ -266,6 +275,10 @@ export class Bootstrap {
|
|||||||
path.join(os.tmpdir(), 'mock-signal-')
|
path.join(os.tmpdir(), 'mock-signal-')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DEFAULT_REMOTE_CONFIG.forEach(([key, value]) =>
|
||||||
|
this.server.setRemoteConfig(key, value)
|
||||||
|
);
|
||||||
|
|
||||||
debug('setting storage path=%j', this.#storagePath);
|
debug('setting storage path=%j', this.#storagePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -725,7 +738,7 @@ export class Bootstrap {
|
|||||||
storagePath: this.#storagePath,
|
storagePath: this.#storagePath,
|
||||||
storageProfile: 'mock',
|
storageProfile: 'mock',
|
||||||
serverUrl: url,
|
serverUrl: url,
|
||||||
storageUrl: url,
|
storageUrl: `${url}/storageService`,
|
||||||
resourcesUrl: `${url}/updates2`,
|
resourcesUrl: `${url}/updates2`,
|
||||||
sfuUrl: url,
|
sfuUrl: url,
|
||||||
cdn: {
|
cdn: {
|
||||||
|
95
ts/test-mock/network/libsignal_test.ts
Normal file
95
ts/test-mock/network/libsignal_test.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
// Copyright 2025 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
import createDebug from 'debug';
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { type PrimaryDevice, StorageState } from '@signalapp/mock-server';
|
||||||
|
|
||||||
|
import type { App } from '../playwright';
|
||||||
|
import { Bootstrap } from '../bootstrap';
|
||||||
|
import { typeIntoInput, waitForEnabledComposer } from '../helpers';
|
||||||
|
import { MINUTE } from '../../util/durations';
|
||||||
|
|
||||||
|
export const debug = createDebug('mock:test:libsignal');
|
||||||
|
|
||||||
|
describe('Libsignal-net', function (this: Mocha.Suite) {
|
||||||
|
this.timeout(MINUTE);
|
||||||
|
let bootstrap: Bootstrap;
|
||||||
|
let app: App;
|
||||||
|
let contact: PrimaryDevice;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
bootstrap = new Bootstrap();
|
||||||
|
await bootstrap.init();
|
||||||
|
[contact] = bootstrap.contacts;
|
||||||
|
|
||||||
|
let state = StorageState.getEmpty();
|
||||||
|
|
||||||
|
state = state.addContact(contact, {
|
||||||
|
identityKey: contact.publicKey.serialize(),
|
||||||
|
profileKey: contact.profileKey.serialize(),
|
||||||
|
whitelisted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async function (this: Mocha.Context) {
|
||||||
|
if (!bootstrap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||||
|
await app.close();
|
||||||
|
await bootstrap.teardown();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can send and receive messages', async () => {
|
||||||
|
const window = await app.getWindow();
|
||||||
|
const { desktop } = bootstrap;
|
||||||
|
|
||||||
|
debug('receiving incoming message');
|
||||||
|
await contact.sendText(bootstrap.desktop, 'incoming message');
|
||||||
|
|
||||||
|
debug('ensuring app received message, opening conversation');
|
||||||
|
{
|
||||||
|
const leftPane = window.locator('#LeftPane');
|
||||||
|
const item = leftPane
|
||||||
|
.getByTestId(contact.toContact().aci)
|
||||||
|
.getByText('incoming message');
|
||||||
|
await item.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('sending outgoing message');
|
||||||
|
const input = await waitForEnabledComposer(window);
|
||||||
|
await typeIntoInput(input, 'outgoing message');
|
||||||
|
await input.press('Enter');
|
||||||
|
|
||||||
|
debug('waiting for message on server side');
|
||||||
|
const { body, source } = await contact.waitForMessage();
|
||||||
|
assert.strictEqual(body, 'outgoing message');
|
||||||
|
assert.strictEqual(source, desktop);
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
@@ -14,6 +14,7 @@ import type { ReceiptType } from '../types/Receipt';
|
|||||||
import { SECOND } from '../util/durations';
|
import { SECOND } from '../util/durations';
|
||||||
import { drop } from '../util/drop';
|
import { drop } from '../util/drop';
|
||||||
import type { MessageAttributesType } from '../model-types';
|
import type { MessageAttributesType } from '../model-types';
|
||||||
|
import type { SocketStatuses } from '../textsecure/SocketManager';
|
||||||
|
|
||||||
export type AppLoadedInfoType = Readonly<{
|
export type AppLoadedInfoType = Readonly<{
|
||||||
loadTime: number;
|
loadTime: number;
|
||||||
@@ -187,6 +188,11 @@ export class App extends EventEmitter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getSocketStatus(): Promise<SocketStatuses> {
|
||||||
|
const window = await this.getWindow();
|
||||||
|
return window.evaluate('window.SignalCI.getSocketStatus()');
|
||||||
|
}
|
||||||
|
|
||||||
public async getMessagesBySentAt(
|
public async getMessagesBySentAt(
|
||||||
timestamp: number
|
timestamp: number
|
||||||
): Promise<Array<MessageAttributesType>> {
|
): Promise<Array<MessageAttributesType>> {
|
||||||
|
@@ -27,6 +27,10 @@ describe('release notes', function (this: Mocha.Suite) {
|
|||||||
bootstrap = new Bootstrap();
|
bootstrap = new Bootstrap();
|
||||||
await bootstrap.init();
|
await bootstrap.init();
|
||||||
|
|
||||||
|
bootstrap.server.setRemoteConfig('desktop.releaseNotes', {
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
app = await bootstrap.link();
|
app = await bootstrap.link();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -26,7 +26,7 @@ import { sleep } from '../util/sleep';
|
|||||||
import { drop } from '../util/drop';
|
import { drop } from '../util/drop';
|
||||||
import type { ProxyAgent } from '../util/createProxyAgent';
|
import type { ProxyAgent } from '../util/createProxyAgent';
|
||||||
import { createProxyAgent } from '../util/createProxyAgent';
|
import { createProxyAgent } from '../util/createProxyAgent';
|
||||||
import { SocketStatus } from '../types/SocketStatus';
|
import { type SocketInfo, SocketStatus } from '../types/SocketStatus';
|
||||||
import * as Errors from '../types/errors';
|
import * as Errors from '../types/errors';
|
||||||
import * as Bytes from '../Bytes';
|
import * as Bytes from '../Bytes';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
@@ -39,6 +39,7 @@ import type {
|
|||||||
import WebSocketResource, {
|
import WebSocketResource, {
|
||||||
connectAuthenticatedLibsignal,
|
connectAuthenticatedLibsignal,
|
||||||
connectUnauthenticatedLibsignal,
|
connectUnauthenticatedLibsignal,
|
||||||
|
LibsignalWebSocketResource,
|
||||||
ServerRequestType,
|
ServerRequestType,
|
||||||
TransportOption,
|
TransportOption,
|
||||||
WebSocketResourceWithShadowing,
|
WebSocketResourceWithShadowing,
|
||||||
@@ -48,6 +49,7 @@ import type { IRequestHandler, WebAPICredentials } from './Types.d';
|
|||||||
import { connect as connectWebSocket } from './WebSocket';
|
import { connect as connectWebSocket } from './WebSocket';
|
||||||
import { isNightly, isBeta, isStaging } from '../util/version';
|
import { isNightly, isBeta, isStaging } from '../util/version';
|
||||||
import { getBasicAuth } from '../util/getBasicAuth';
|
import { getBasicAuth } from '../util/getBasicAuth';
|
||||||
|
import { isTestOrMockEnvironment } from '../environment';
|
||||||
|
|
||||||
const FIVE_MINUTES = 5 * durations.MINUTE;
|
const FIVE_MINUTES = 5 * durations.MINUTE;
|
||||||
|
|
||||||
@@ -68,6 +70,20 @@ export type SocketManagerOptions = Readonly<{
|
|||||||
hasStoriesDisabled: boolean;
|
hasStoriesDisabled: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type SocketStatusUpdate =
|
||||||
|
| {
|
||||||
|
status: SocketStatus.OPEN;
|
||||||
|
transportOption: TransportOption.Libsignal | TransportOption.Original;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
status: Exclude<SocketStatus, SocketStatus.OPEN>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SocketStatuses = Record<
|
||||||
|
'authenticated' | 'unauthenticated',
|
||||||
|
SocketInfo
|
||||||
|
>;
|
||||||
|
|
||||||
// This class manages two websocket resources:
|
// This class manages two websocket resources:
|
||||||
//
|
//
|
||||||
// - Authenticated IWebSocketResource which uses supplied WebAPICredentials and
|
// - Authenticated IWebSocketResource which uses supplied WebAPICredentials and
|
||||||
@@ -92,7 +108,12 @@ export class SocketManager extends EventListener {
|
|||||||
#unauthenticatedExpirationTimer?: NodeJS.Timeout;
|
#unauthenticatedExpirationTimer?: NodeJS.Timeout;
|
||||||
#credentials?: WebAPICredentials;
|
#credentials?: WebAPICredentials;
|
||||||
#lazyProxyAgent?: Promise<ProxyAgent>;
|
#lazyProxyAgent?: Promise<ProxyAgent>;
|
||||||
#status = SocketStatus.CLOSED;
|
#authenticatedStatus: SocketInfo = {
|
||||||
|
status: SocketStatus.CLOSED,
|
||||||
|
};
|
||||||
|
#unathenticatedStatus: SocketInfo = {
|
||||||
|
status: SocketStatus.CLOSED,
|
||||||
|
};
|
||||||
#requestHandlers = new Set<IRequestHandler>();
|
#requestHandlers = new Set<IRequestHandler>();
|
||||||
#incomingRequestQueue = new Array<IncomingWebSocketRequest>();
|
#incomingRequestQueue = new Array<IncomingWebSocketRequest>();
|
||||||
#isNavigatorOffline = false;
|
#isNavigatorOffline = false;
|
||||||
@@ -111,8 +132,11 @@ export class SocketManager extends EventListener {
|
|||||||
this.#hasStoriesDisabled = options.hasStoriesDisabled;
|
this.#hasStoriesDisabled = options.hasStoriesDisabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getStatus(): SocketStatus {
|
public getStatus(): SocketStatuses {
|
||||||
return this.#status;
|
return {
|
||||||
|
authenticated: this.#authenticatedStatus,
|
||||||
|
unauthenticated: this.#unathenticatedStatus,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#markOffline() {
|
#markOffline() {
|
||||||
@@ -163,7 +187,7 @@ export class SocketManager extends EventListener {
|
|||||||
`(hasStoriesDisabled=${this.#hasStoriesDisabled})`
|
`(hasStoriesDisabled=${this.#hasStoriesDisabled})`
|
||||||
);
|
);
|
||||||
|
|
||||||
this.#setStatus(SocketStatus.CONNECTING);
|
this.#setAuthenticatedStatus({ status: SocketStatus.CONNECTING });
|
||||||
|
|
||||||
const proxyAgent = await this.#getProxyAgent();
|
const proxyAgent = await this.#getProxyAgent();
|
||||||
const useLibsignalTransport =
|
const useLibsignalTransport =
|
||||||
@@ -252,7 +276,14 @@ export class SocketManager extends EventListener {
|
|||||||
let authenticated: IWebSocketResource;
|
let authenticated: IWebSocketResource;
|
||||||
try {
|
try {
|
||||||
authenticated = await process.getResult();
|
authenticated = await process.getResult();
|
||||||
this.#setStatus(SocketStatus.OPEN);
|
|
||||||
|
this.#setAuthenticatedStatus({
|
||||||
|
status: SocketStatus.OPEN,
|
||||||
|
transportOption:
|
||||||
|
authenticated instanceof LibsignalWebSocketResource
|
||||||
|
? TransportOption.Libsignal
|
||||||
|
: TransportOption.Original,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.warn(
|
log.warn(
|
||||||
'SocketManager: authenticated socket connection failed with ' +
|
'SocketManager: authenticated socket connection failed with ' +
|
||||||
@@ -290,6 +321,11 @@ export class SocketManager extends EventListener {
|
|||||||
) {
|
) {
|
||||||
this.emit('authError');
|
this.emit('authError');
|
||||||
return;
|
return;
|
||||||
|
} else if (
|
||||||
|
error instanceof LibSignalErrorBase &&
|
||||||
|
error.code === ErrorCode.IoError
|
||||||
|
) {
|
||||||
|
this.#markOffline();
|
||||||
} else if (
|
} else if (
|
||||||
error instanceof LibSignalErrorBase &&
|
error instanceof LibSignalErrorBase &&
|
||||||
error.code === ErrorCode.AppExpired
|
error.code === ErrorCode.AppExpired
|
||||||
@@ -566,21 +602,44 @@ export class SocketManager extends EventListener {
|
|||||||
// Private
|
// Private
|
||||||
//
|
//
|
||||||
|
|
||||||
#setStatus(status: SocketStatus): void {
|
#setAuthenticatedStatus(newStatus: SocketStatusUpdate): void {
|
||||||
if (this.#status === status) {
|
if (this.#authenticatedStatus.status === newStatus.status) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#status = status;
|
this.#authenticatedStatus.status = newStatus.status;
|
||||||
this.emit('statusChange');
|
this.emit('statusChange');
|
||||||
|
|
||||||
if (this.#status === SocketStatus.OPEN && !this.#privIsOnline) {
|
if (newStatus.status === SocketStatus.OPEN) {
|
||||||
this.#privIsOnline = true;
|
this.#authenticatedStatus.lastConnectionTimestamp = Date.now();
|
||||||
this.emit('online');
|
this.#authenticatedStatus.lastConnectionTransport =
|
||||||
|
newStatus.transportOption;
|
||||||
|
|
||||||
|
if (!this.#privIsOnline) {
|
||||||
|
this.#privIsOnline = true;
|
||||||
|
this.emit('online');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#setUnauthenticatedStatus(newStatus: SocketStatusUpdate): void {
|
||||||
|
this.#unathenticatedStatus.status = newStatus.status;
|
||||||
|
|
||||||
|
if (newStatus.status === SocketStatus.OPEN) {
|
||||||
|
this.#unathenticatedStatus.lastConnectionTimestamp = Date.now();
|
||||||
|
this.#unathenticatedStatus.lastConnectionTransport =
|
||||||
|
newStatus.transportOption;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#transportOption(): TransportOption {
|
#transportOption(): TransportOption {
|
||||||
|
if (isTestOrMockEnvironment()) {
|
||||||
|
const configValue = window.Signal.RemoteConfig.isEnabled(
|
||||||
|
'desktop.experimentalTransportEnabled.alpha'
|
||||||
|
);
|
||||||
|
return configValue ? TransportOption.Libsignal : TransportOption.Original;
|
||||||
|
}
|
||||||
|
|
||||||
// in staging, switch to using libsignal transport
|
// in staging, switch to using libsignal transport
|
||||||
if (isStaging(this.options.version)) {
|
if (isStaging(this.options.version)) {
|
||||||
return TransportOption.Libsignal;
|
return TransportOption.Libsignal;
|
||||||
@@ -641,6 +700,10 @@ export class SocketManager extends EventListener {
|
|||||||
`SocketManager: connecting unauthenticated socket, transport option [${transportOption}]`
|
`SocketManager: connecting unauthenticated socket, transport option [${transportOption}]`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.#setUnauthenticatedStatus({
|
||||||
|
status: SocketStatus.CONNECTING,
|
||||||
|
});
|
||||||
|
|
||||||
let process: AbortableProcess<IWebSocketResource>;
|
let process: AbortableProcess<IWebSocketResource>;
|
||||||
|
|
||||||
if (transportOption === TransportOption.Libsignal) {
|
if (transportOption === TransportOption.Libsignal) {
|
||||||
@@ -667,6 +730,13 @@ export class SocketManager extends EventListener {
|
|||||||
let unauthenticated: IWebSocketResource;
|
let unauthenticated: IWebSocketResource;
|
||||||
try {
|
try {
|
||||||
unauthenticated = await this.#unauthenticated.getResult();
|
unauthenticated = await this.#unauthenticated.getResult();
|
||||||
|
this.#setUnauthenticatedStatus({
|
||||||
|
status: SocketStatus.OPEN,
|
||||||
|
transportOption:
|
||||||
|
unauthenticated instanceof LibsignalWebSocketResource
|
||||||
|
? TransportOption.Libsignal
|
||||||
|
: TransportOption.Original,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.info(
|
log.info(
|
||||||
'SocketManager: failed to connect unauthenticated socket ' +
|
'SocketManager: failed to connect unauthenticated socket ' +
|
||||||
@@ -824,7 +894,7 @@ export class SocketManager extends EventListener {
|
|||||||
|
|
||||||
this.#incomingRequestQueue = [];
|
this.#incomingRequestQueue = [];
|
||||||
this.#authenticated = undefined;
|
this.#authenticated = undefined;
|
||||||
this.#setStatus(SocketStatus.CLOSED);
|
this.#setAuthenticatedStatus({ status: SocketStatus.CLOSED });
|
||||||
}
|
}
|
||||||
|
|
||||||
#dropUnauthenticated(process: AbortableProcess<IWebSocketResource>): void {
|
#dropUnauthenticated(process: AbortableProcess<IWebSocketResource>): void {
|
||||||
@@ -833,6 +903,7 @@ export class SocketManager extends EventListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.#unauthenticated = undefined;
|
this.#unauthenticated = undefined;
|
||||||
|
this.#setUnauthenticatedStatus({ status: SocketStatus.CLOSED });
|
||||||
if (!this.#unauthenticatedExpirationTimer) {
|
if (!this.#unauthenticatedExpirationTimer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -30,7 +30,6 @@ import { createHTTPSAgent } from '../util/createHTTPSAgent';
|
|||||||
import { createProxyAgent } from '../util/createProxyAgent';
|
import { createProxyAgent } from '../util/createProxyAgent';
|
||||||
import type { ProxyAgent } from '../util/createProxyAgent';
|
import type { ProxyAgent } from '../util/createProxyAgent';
|
||||||
import type { FetchFunctionType } from '../util/uploads/tusProtocol';
|
import type { FetchFunctionType } from '../util/uploads/tusProtocol';
|
||||||
import type { SocketStatus } from '../types/SocketStatus';
|
|
||||||
import { VerificationTransport } from '../types/VerificationTransport';
|
import { VerificationTransport } from '../types/VerificationTransport';
|
||||||
import { toLogFormat } from '../types/errors';
|
import { toLogFormat } from '../types/errors';
|
||||||
import { isPackIdValid, redactPackId } from '../types/Stickers';
|
import { isPackIdValid, redactPackId } from '../types/Stickers';
|
||||||
@@ -52,7 +51,7 @@ import { randomInt } from '../Crypto';
|
|||||||
import * as linkPreviewFetch from '../linkPreviews/linkPreviewFetch';
|
import * as linkPreviewFetch from '../linkPreviews/linkPreviewFetch';
|
||||||
import { isBadgeImageFileUrlValid } from '../badges/isBadgeImageFileUrlValid';
|
import { isBadgeImageFileUrlValid } from '../badges/isBadgeImageFileUrlValid';
|
||||||
|
|
||||||
import { SocketManager } from './SocketManager';
|
import { SocketManager, type SocketStatuses } from './SocketManager';
|
||||||
import type { CDSAuthType, CDSResponseType } from './cds/Types.d';
|
import type { CDSAuthType, CDSResponseType } from './cds/Types.d';
|
||||||
import { CDSI } from './cds/CDSI';
|
import { CDSI } from './cds/CDSI';
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
@@ -1578,7 +1577,7 @@ export type WebAPIType = {
|
|||||||
getConfig: () => Promise<RemoteConfigResponseType>;
|
getConfig: () => Promise<RemoteConfigResponseType>;
|
||||||
authenticate: (credentials: WebAPICredentials) => Promise<void>;
|
authenticate: (credentials: WebAPICredentials) => Promise<void>;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
getSocketStatus: () => SocketStatus;
|
getSocketStatus: () => SocketStatuses;
|
||||||
registerRequestHandler: (handler: IRequestHandler) => void;
|
registerRequestHandler: (handler: IRequestHandler) => void;
|
||||||
unregisterRequestHandler: (handler: IRequestHandler) => void;
|
unregisterRequestHandler: (handler: IRequestHandler) => void;
|
||||||
onHasStoriesDisabledChange: (newValue: boolean) => void;
|
onHasStoriesDisabledChange: (newValue: boolean) => void;
|
||||||
@@ -2106,7 +2105,7 @@ export function initialize({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSocketStatus(): SocketStatus {
|
function getSocketStatus(): SocketStatuses {
|
||||||
return socketManager.getStatus();
|
return socketManager.getStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { TransportOption } from '../textsecure/WebsocketResources';
|
||||||
|
|
||||||
// Maps to values found here: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
|
// Maps to values found here: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
|
||||||
// which are returned by libtextsecure's MessageReceiver
|
// which are returned by libtextsecure's MessageReceiver
|
||||||
export enum SocketStatus {
|
export enum SocketStatus {
|
||||||
@@ -9,3 +11,11 @@ export enum SocketStatus {
|
|||||||
CLOSING = 'CLOSING',
|
CLOSING = 'CLOSING',
|
||||||
CLOSED = 'CLOSED',
|
CLOSED = 'CLOSED',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SocketInfo = {
|
||||||
|
status: SocketStatus;
|
||||||
|
lastConnectionTimestamp?: number;
|
||||||
|
lastConnectionTransport?:
|
||||||
|
| TransportOption.Libsignal
|
||||||
|
| TransportOption.Original;
|
||||||
|
};
|
||||||
|
8
ts/window.d.ts
vendored
8
ts/window.d.ts
vendored
@@ -51,6 +51,7 @@ import type { RetryPlaceholders } from './util/retryPlaceholders';
|
|||||||
import type { PropsPreloadType as PreferencesPropsType } from './components/Preferences';
|
import type { PropsPreloadType as PreferencesPropsType } from './components/Preferences';
|
||||||
import type { WindowsNotificationData } from './services/notifications';
|
import type { WindowsNotificationData } from './services/notifications';
|
||||||
import type { QueryStatsOptions } from './sql/main';
|
import type { QueryStatsOptions } from './sql/main';
|
||||||
|
import type { SocketStatuses } from './textsecure/SocketManager';
|
||||||
|
|
||||||
export { Long } from 'long';
|
export { Long } from 'long';
|
||||||
|
|
||||||
@@ -146,7 +147,10 @@ export type SignalCoreType = {
|
|||||||
calling: CallingClass;
|
calling: CallingClass;
|
||||||
backups: BackupsService;
|
backups: BackupsService;
|
||||||
initializeGroupCredentialFetcher: () => Promise<void>;
|
initializeGroupCredentialFetcher: () => Promise<void>;
|
||||||
initializeNetworkObserver: (network: ReduxActions['network']) => void;
|
initializeNetworkObserver: (
|
||||||
|
network: ReduxActions['network'],
|
||||||
|
getAuthSocketStatus: () => SocketStatus
|
||||||
|
) => void;
|
||||||
initializeUpdateListener: (updates: ReduxActions['updates']) => void;
|
initializeUpdateListener: (updates: ReduxActions['updates']) => void;
|
||||||
retryPlaceholders?: RetryPlaceholders;
|
retryPlaceholders?: RetryPlaceholders;
|
||||||
lightSessionResetQueue?: PQueue;
|
lightSessionResetQueue?: PQueue;
|
||||||
@@ -201,7 +205,7 @@ declare global {
|
|||||||
getBackupServerPublicParams: () => string;
|
getBackupServerPublicParams: () => string;
|
||||||
getSfuUrl: () => string;
|
getSfuUrl: () => string;
|
||||||
getIceServerOverride: () => string;
|
getIceServerOverride: () => string;
|
||||||
getSocketStatus: () => SocketStatus;
|
getSocketStatus: () => SocketStatuses;
|
||||||
getTitle: () => string;
|
getTitle: () => string;
|
||||||
waitForEmptyEventQueue: () => Promise<void>;
|
waitForEmptyEventQueue: () => Promise<void>;
|
||||||
getVersion: () => string;
|
getVersion: () => string;
|
||||||
|
@@ -78,6 +78,7 @@ if (
|
|||||||
getSfuUrl: () => window.Signal.Services.calling._sfuUrl,
|
getSfuUrl: () => window.Signal.Services.calling._sfuUrl,
|
||||||
getIceServerOverride: () =>
|
getIceServerOverride: () =>
|
||||||
window.Signal.Services.calling._iceServerOverride,
|
window.Signal.Services.calling._iceServerOverride,
|
||||||
|
getSocketStatus: () => window.textsecure.server?.getSocketStatus(),
|
||||||
getStorageItem: (name: keyof StorageAccessType) => window.storage.get(name),
|
getStorageItem: (name: keyof StorageAccessType) => window.storage.get(name),
|
||||||
putStorageItem: <K extends keyof StorageAccessType>(
|
putStorageItem: <K extends keyof StorageAccessType>(
|
||||||
name: K,
|
name: K,
|
||||||
|
Reference in New Issue
Block a user