signal-desktop/ts/textsecure/WebSocket.ts

136 lines
3.5 KiB
TypeScript
Raw Normal View History

2021-11-08 23:32:31 +00:00
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type ProxyAgent from 'proxy-agent';
import { client as WebSocketClient } from 'websocket';
import type { connection as WebSocket } from 'websocket';
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';
import { createHTTPSAgent } from '../util/createHTTPSAgent';
2021-11-08 23:32:31 +00:00
import * as log from '../logging/log';
import * as Timers from '../Timers';
import { ConnectTimeoutError, HTTPError } from './Errors';
import { handleStatusCode, translateError } from './Utils';
const TEN_SECONDS = 10 * durations.SECOND;
2023-06-21 19:05:44 +00:00
const KEEPALIVE_INTERVAL_MS = TEN_SECONDS;
2021-11-08 23:32:31 +00:00
export type IResource = {
close(code: number, reason: string): void;
};
export type ConnectOptionsType<Resource extends IResource> = Readonly<{
name: string;
2021-11-08 23:32:31 +00:00
url: string;
2023-02-27 22:34:43 +00:00
certificateAuthority?: string;
2021-11-08 23:32:31 +00:00
version: string;
proxyAgent?: ReturnType<typeof ProxyAgent>;
timeout?: number;
2022-03-09 19:28:40 +00:00
extraHeaders?: Record<string, string>;
2021-11-08 23:32:31 +00:00
createResource(socket: WebSocket): Resource;
}>;
export function connect<Resource extends IResource>({
name,
2021-11-08 23:32:31 +00:00
url,
certificateAuthority,
version,
proxyAgent,
2022-03-09 19:28:40 +00:00
extraHeaders = {},
2021-11-08 23:32:31 +00:00
timeout = TEN_SECONDS,
createResource,
}: ConnectOptionsType<Resource>): AbortableProcess<Resource> {
const fixedScheme = url
.replace('https://', 'wss://')
.replace('http://', 'ws://');
const headers = {
2022-03-09 19:28:40 +00:00
...extraHeaders,
2021-11-08 23:32:31 +00:00
'User-Agent': getUserAgent(version),
};
const client = new WebSocketClient({
tlsOptions: {
ca: certificateAuthority,
agent: proxyAgent ?? createHTTPSAgent(),
2021-11-08 23:32:31 +00: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 19:05:44 +00:00
socket.socket.setKeepAlive(true, KEEPALIVE_INTERVAL_MS);
2021-11-08 23:32:31 +00:00
resource = createResource(socket);
resolve(resource);
});
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-08 23:32:31 +00:00
Timers.clearTimeout(timer);
const err = new HTTPError('connectResource: connectFailed', {
code: -1,
headers: {},
stack,
cause: originalErr,
});
reject(err);
2021-11-08 23:32:31 +00:00
});
return new AbortableProcess<Resource>(
`WebSocket.connect(${name})`,
2021-11-08 23:32:31 +00:00
{
abort() {
if (resource) {
log.warn(`WebSocket: closing socket ${name}`);
2021-11-08 23:32:31 +00:00
resource.close(3000, 'aborted');
} else {
log.warn(`WebSocket: aborting connection ${name}`);
2021-11-08 23:32:31 +00:00
Timers.clearTimeout(timer);
client.abort();
}
},
},
promise
);
}