signal-desktop/ts/textsecure/WebSocket.ts

146 lines
3.8 KiB
TypeScript
Raw Normal View History

2021-11-09 00:32:31 +01:00
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { client as WebSocketClient } from 'websocket';
import type { connection as WebSocket } from 'websocket';
import type { IncomingMessage } from 'http';
2021-11-09 00:32:31 +01:00
import { AbortableProcess } from '../util/AbortableProcess';
import { strictAssert } from '../util/assert';
import { explodePromise } from '../util/explodePromise';
import { getUserAgent } from '../util/getUserAgent';
import * as durations from '../util/durations';
2024-03-20 11:05:10 -07:00
import type { ProxyAgent } from '../util/createProxyAgent';
import { createHTTPSAgent } from '../util/createHTTPSAgent';
2025-06-16 11:59:31 -07:00
import { createLogger } from '../logging/log';
2021-11-09 00:32:31 +01:00
import * as Timers from '../Timers';
import { ConnectTimeoutError, HTTPError } from './Errors';
import { handleStatusCode, translateError } from './Utils';
2025-06-16 11:59:31 -07:00
const log = createLogger('WebSocket');
2021-11-09 00:32:31 +01:00
const TEN_SECONDS = 10 * durations.SECOND;
const WEBSOCKET_CONNECT_TIMEOUT = TEN_SECONDS;
2023-06-21 21:05:44 +02:00
const KEEPALIVE_INTERVAL_MS = TEN_SECONDS;
2021-11-09 00:32:31 +01:00
export type IResource = {
close(code: number, reason: string): void;
};
export type ConnectOptionsType<Resource extends IResource> = Readonly<{
name: string;
2021-11-09 00:32:31 +01:00
url: string;
2023-02-27 14:34:43 -08:00
certificateAuthority?: string;
2021-11-09 00:32:31 +01:00
version: string;
2024-03-20 11:05:10 -07:00
proxyAgent?: ProxyAgent;
2021-11-09 00:32:31 +01:00
timeout?: number;
2022-03-09 11:28:40 -08:00
extraHeaders?: Record<string, string>;
onUpgradeResponse?: (response: IncomingMessage) => void;
2021-11-09 00:32:31 +01:00
createResource(socket: WebSocket): Resource;
}>;
export function connect<Resource extends IResource>({
name,
2021-11-09 00:32:31 +01:00
url,
certificateAuthority,
version,
proxyAgent,
2022-03-09 11:28:40 -08:00
extraHeaders = {},
timeout = WEBSOCKET_CONNECT_TIMEOUT,
onUpgradeResponse,
2021-11-09 00:32:31 +01:00
createResource,
}: ConnectOptionsType<Resource>): AbortableProcess<Resource> {
const fixedScheme = url
.replace('https://', 'wss://')
.replace('http://', 'ws://');
const headers = {
2022-03-09 11:28:40 -08:00
...extraHeaders,
2021-11-09 00:32:31 +01:00
'User-Agent': getUserAgent(version),
};
const client = new WebSocketClient({
tlsOptions: {
ca: certificateAuthority,
agent: proxyAgent ?? createHTTPSAgent(),
2021-11-09 00:32:31 +01:00
},
maxReceivedFrameSize: 0x210000,
});
client.connect(fixedScheme, undefined, undefined, headers);
const { stack } = new Error();
const { promise, resolve, reject } = explodePromise<Resource>();
const timer = Timers.setTimeout(() => {
reject(new ConnectTimeoutError('Connection timed out'));
client.abort();
}, timeout);
let resource: Resource | undefined;
client.on('connect', socket => {
Timers.clearTimeout(timer);
2023-06-21 21:05:44 +02:00
socket.socket.setKeepAlive(true, KEEPALIVE_INTERVAL_MS);
2021-11-09 00:32:31 +01:00
resource = createResource(socket);
resolve(resource);
});
client.on('upgradeResponse', response => {
onUpgradeResponse?.(response);
});
2021-11-09 00:32:31 +01:00
client.on('httpResponse', async response => {
Timers.clearTimeout(timer);
const statusCode = response.statusCode || -1;
await handleStatusCode(statusCode);
const error = new HTTPError('connectResource: invalid websocket response', {
code: statusCode || -1,
headers: {},
stack,
});
const translatedError = translateError(error);
strictAssert(
translatedError,
'`httpResponse` event cannot be emitted with 200 status code'
);
reject(translatedError);
});
client.on('connectFailed', originalErr => {
2021-11-09 00:32:31 +01:00
Timers.clearTimeout(timer);
const err = new HTTPError('connectResource: connectFailed', {
code: -1,
headers: {},
stack,
cause: originalErr,
});
reject(err);
2021-11-09 00:32:31 +01:00
});
return new AbortableProcess<Resource>(
`WebSocket.connect(${name})`,
2021-11-09 00:32:31 +01:00
{
abort() {
if (resource) {
2025-06-16 11:59:31 -07:00
log.warn(`closing socket ${name}`);
2021-11-09 00:32:31 +01:00
resource.close(3000, 'aborted');
} else {
2025-06-16 11:59:31 -07:00
log.warn(`aborting connection ${name}`);
2021-11-09 00:32:31 +01:00
Timers.clearTimeout(timer);
client.abort();
}
},
},
promise
);
}