WebSocket API for CDS

This commit is contained in:
Fedor Indutny 2021-11-09 00:32:31 +01:00 committed by GitHub
parent 4cb1ea9e5d
commit 409bf1fc82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 436 additions and 109 deletions

View File

@ -296,6 +296,9 @@ function prepareUrl(
directoryUrl: config.get<string>('directoryUrl'),
directoryEnclaveId: config.get<string>('directoryEnclaveId'),
directoryTrustAnchor: config.get<string>('directoryTrustAnchor'),
directoryV2Url: config.get<string>('directoryV2Url'),
directoryV2PublicKey: config.get<string>('directoryV2PublicKey'),
directoryV2CodeHash: config.get<string>('directoryV2CodeHash'),
cdnUrl0: config.get<ConfigType>('cdn').get<string>('0'),
cdnUrl2: config.get<ConfigType>('cdn').get<string>('2'),
certificateAuthority: config.get<string>('certificateAuthority'),

View File

@ -4,6 +4,9 @@
"directoryUrl": "https://api-staging.directory.signal.org",
"directoryEnclaveId": "c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15",
"directoryTrustAnchor": "-----BEGIN CERTIFICATE-----\nMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV\nBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV\nBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0\nYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy\nMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL\nU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD\nDCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G\nCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR+tXc8u1EtJzLA10Feu1Wg+p7e\nLmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh\nrgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT\nL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe\nNpEJUmg4ktal4qgIAxk+QHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ\nbyinkNndn+Bgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H\nafuVeLHcDsRp6hol4P+ZFIhu8mmbI1u0hH3W/0C2BuYXB5PC+5izFFh/nP0lc2Lf\n6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM\nRoOaX4AS+909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX\nMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50\nL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW\nBBR4Q3t2pn680K9+QjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9+Qjfr\nNXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq\nhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir\nIEqucRiJSSx+HjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi+ripMtPZ\nsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi\nzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra\nUd4APK0wZTGtfPXU7w+IBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA\n152Sq049ESDz+1rRGc2NVEqh1KaGXmtXvqxXcTB+Ljy5Bw2ke0v8iGngFBPqCTVB\n3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5+xmBc388v9Dm21HGfcC8O\nDD+gT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R+mJTLwPXVMrv\nDaVzWh5aiEx+idkSGMnX\n-----END CERTIFICATE-----\n",
"directoryV2Url": "https://cdsh.staging.signal.org",
"directoryV2PublicKey": "052fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74",
"directoryV2CodeHash": "ec31a51880d19a5e9e0fed404740c1a3ff53a553125564b745acce475f0fded8",
"cdn": {
"0": "https://cdn-staging.signal.org",
"2": "https://cdn2-staging.signal.org"

View File

@ -74,7 +74,7 @@
"dependencies": {
"@popperjs/core": "2.9.2",
"@react-spring/web": "9.2.6",
"@signalapp/signal-client": "0.9.5",
"@signalapp/signal-client": "0.9.8",
"@sindresorhus/is": "0.8.0",
"abort-controller": "3.0.0",
"array-move": "2.1.0",

View File

@ -371,6 +371,9 @@ try {
directoryUrl: config.directoryUrl,
directoryEnclaveId: config.directoryEnclaveId,
directoryTrustAnchor: config.directoryTrustAnchor,
directoryV2Url: config.directoryV2Url,
directoryV2PublicKey: config.directoryV2PublicKey,
directoryV2CodeHash: config.directoryV2CodeHash,
cdnUrlObject: {
0: config.cdnUrl0,
2: config.cdnUrl2,

View File

@ -57,6 +57,9 @@ const WebAPI = initializeWebAPI({
directoryUrl: config.directoryUrl,
directoryEnclaveId: config.directoryEnclaveId,
directoryTrustAnchor: config.directoryTrustAnchor,
directoryV2Url: config.directoryV2Url,
directoryV2PublicKey: config.directoryV2PublicKey,
directoryV2CodeHash: config.directoryV2CodeHash,
cdnUrlObject: {
0: config.cdnUrl0,
2: config.cdnUrl2,

149
ts/textsecure/CDSSocket.ts Normal file
View File

@ -0,0 +1,149 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { EventEmitter } from 'events';
import type { HsmEnclaveClient } from '@signalapp/signal-client';
import type { connection as WebSocket } from 'websocket';
import Long from 'long';
import { strictAssert } from '../util/assert';
import { explodePromise } from '../util/explodePromise';
import * as durations from '../util/durations';
import type { UUIDStringType } from '../types/UUID';
import * as Bytes from '../Bytes';
import * as Timers from '../Timers';
import { splitUuids } from '../Crypto';
enum State {
Handshake,
Established,
Closed,
}
const HANDSHAKE_TIMEOUT = 10 * durations.SECOND;
const REQUEST_TIMEOUT = 10 * durations.SECOND;
export class CDSSocket extends EventEmitter {
private state = State.Handshake;
private readonly finishedHandshake: Promise<void>;
private readonly requestQueue = new Array<(buffer: Buffer) => void>();
constructor(
private readonly socket: WebSocket,
private readonly enclaveClient: HsmEnclaveClient
) {
super();
const {
promise: finishedHandshake,
resolve,
reject,
} = explodePromise<void>();
this.finishedHandshake = finishedHandshake;
const timer = Timers.setTimeout(() => {
reject(new Error('CDS handshake timed out'));
}, HANDSHAKE_TIMEOUT);
socket.on('message', ({ type, binaryData }) => {
strictAssert(type === 'binary', 'Invalid CDS socket packet');
strictAssert(binaryData, 'Invalid CDS socket packet');
if (this.state === State.Handshake) {
this.enclaveClient.completeHandshake(binaryData);
this.state = State.Established;
Timers.clearTimeout(timer);
resolve();
return;
}
const requestHandler = this.requestQueue.shift();
strictAssert(
requestHandler !== undefined,
'No handler for incoming CDS data'
);
requestHandler(this.enclaveClient.establishedRecv(binaryData));
});
socket.on('close', (code, reason) => {
this.state = State.Closed;
this.emit('close', code, reason);
});
socket.on('error', (error: Error) => this.emit('error', error));
socket.sendBytes(this.enclaveClient.initialRequest());
}
public close(code: number, reason: string): void {
this.socket.close(code, reason);
}
public async request({
e164s,
timeout = REQUEST_TIMEOUT,
}: {
e164s: ReadonlyArray<string>;
timeout?: number;
}): Promise<ReadonlyArray<UUIDStringType | null>> {
await this.finishedHandshake;
strictAssert(
this.state === State.Established,
'Connection not established'
);
const request = Bytes.concatenate([
new Uint8Array([0x01]),
...e164s.map(e164 => {
// Long.fromString handles numbers with or without a leading '+'
return new Uint8Array(Long.fromString(e164).toBytesBE());
}),
]);
const { promise, resolve, reject } = explodePromise<Buffer>();
const timer = Timers.setTimeout(() => {
reject(new Error('CDS request timed out'));
}, timeout);
this.socket.sendBytes(
this.enclaveClient.establishedSend(Buffer.from(request))
);
this.requestQueue.push(resolve);
strictAssert(
this.requestQueue.length === 1,
'Concurrent use of CDS shold not happen'
);
const uuids = await promise;
Timers.clearTimeout(timer);
return splitUuids(uuids);
}
// EventEmitter types
public on(
type: 'close',
callback: (code: number, reason?: string) => void
): this;
public on(type: 'error', callback: (error: Error) => void): this;
public on(
type: string | symbol,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
listener: (...args: Array<any>) => void
): this {
return super.on(type, listener);
}
public emit(type: 'close', code: number, reason?: string): boolean;
public emit(type: 'error', error: Error): boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public emit(type: string | symbol, ...args: Array<any>): boolean {
return super.emit(type, ...args);
}
}

View File

@ -0,0 +1,82 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import ProxyAgent from 'proxy-agent';
import { HsmEnclaveClient, PublicKey } from '@signalapp/signal-client';
import type { connection as WebSocket } from 'websocket';
import * as Bytes from '../Bytes';
import type { AbortableProcess } from '../util/AbortableProcess';
import * as log from '../logging/log';
import type { UUIDStringType } from '../types/UUID';
import { CDSSocket } from './CDSSocket';
import { connect as connectWebSocket } from './WebSocket';
export type CDSSocketManagerOptionsType = Readonly<{
url: string;
publicKey: string;
codeHash: string;
certificateAuthority: string;
proxyUrl?: string;
version: string;
}>;
export class CDSSocketManager {
private readonly publicKey: PublicKey;
private readonly codeHash: Buffer;
private readonly proxyAgent?: ReturnType<typeof ProxyAgent>;
constructor(private readonly options: CDSSocketManagerOptionsType) {
this.publicKey = PublicKey.deserialize(
Buffer.from(Bytes.fromHex(options.publicKey))
);
this.codeHash = Buffer.from(Bytes.fromHex(options.codeHash));
if (options.proxyUrl) {
this.proxyAgent = new ProxyAgent(options.proxyUrl);
}
}
public async request({
e164s,
timeout,
}: {
e164s: ReadonlyArray<string>;
timeout?: number;
}): Promise<ReadonlyArray<UUIDStringType | null>> {
log.info('CDSSocketManager: connecting socket');
const socket = await this.connect().getResult();
log.info('CDSSocketManager: connected socket');
try {
return await socket.request({ e164s, timeout });
} finally {
log.info('CDSSocketManager: closing socket');
socket.close(3000, 'Normal');
}
}
private connect(): AbortableProcess<CDSSocket> {
const enclaveClient = HsmEnclaveClient.new(this.publicKey, [this.codeHash]);
const {
publicKey: publicKeyHex,
codeHash: codeHashHex,
version,
} = this.options;
const url = `${this.options.url}/discovery/${publicKeyHex}/${codeHashHex}`;
return connectWebSocket<CDSSocket>({
url,
version,
proxyAgent: this.proxyAgent,
certificateAuthority: this.options.certificateAuthority,
createResource: (socket: WebSocket): CDSSocket => {
return new CDSSocket(socket, enclaveClient);
},
});
}
}

View File

@ -23,6 +23,7 @@ import { SenderKeys } from '../LibSignalStores';
import type { LinkPreviewType } from '../types/message/LinkPreviews';
import { MIMETypeToString } from '../types/MIME';
import type * as Attachment from '../types/Attachment';
import type { UUIDStringType } from '../types/UUID';
import type {
ChallengeType,
GroupCredentialsType,
@ -2058,10 +2059,16 @@ export default class MessageSender {
async getUuidsForE164s(
numbers: ReadonlyArray<string>
): Promise<Dictionary<string | null>> {
): Promise<Dictionary<UUIDStringType | null>> {
return this.server.getUuidsForE164s(numbers);
}
async getUuidsForE164sV2(
numbers: ReadonlyArray<string>
): Promise<Dictionary<UUIDStringType | null>> {
return this.server.getUuidsForE164sV2(numbers);
}
async getAvatar(path: string): Promise<ReturnType<WebAPIType['getAvatar']>> {
return this.server.getAvatar(path);
}

View File

@ -5,21 +5,18 @@ import URL from 'url';
import ProxyAgent from 'proxy-agent';
import type { RequestInit } from 'node-fetch';
import { Response, Headers } from 'node-fetch';
import { client as WebSocketClient } from 'websocket';
import type { connection as WebSocket } from 'websocket';
import qs from 'querystring';
import EventListener from 'events';
import { AbortableProcess } from '../util/AbortableProcess';
import type { AbortableProcess } from '../util/AbortableProcess';
import { strictAssert } from '../util/assert';
import { explodePromise } from '../util/explodePromise';
import { BackOff, FIBONACCI_TIMEOUTS } from '../util/BackOff';
import { getUserAgent } from '../util/getUserAgent';
import * as durations from '../util/durations';
import { sleep } from '../util/sleep';
import { SocketStatus } from '../types/SocketStatus';
import * as Errors from '../types/errors';
import * as Bytes from '../Bytes';
import * as Timers from '../Timers';
import * as log from '../logging/log';
import type {
@ -27,11 +24,9 @@ import type {
IncomingWebSocketRequest,
} from './WebsocketResources';
import WebSocketResource from './WebsocketResources';
import { ConnectTimeoutError, HTTPError } from './Errors';
import { handleStatusCode, translateError } from './Utils';
import { HTTPError } from './Errors';
import type { WebAPICredentials, IRequestHandler } from './Types.d';
const TEN_SECONDS = 10 * durations.SECOND;
import { connect as connectWebSocket } from './WebSocket';
const FIVE_MINUTES = 5 * durations.MINUTE;
@ -472,112 +467,29 @@ export class SocketManager extends EventListener {
path,
resourceOptions,
query = {},
timeout = TEN_SECONDS,
}: {
path: string;
resourceOptions: WebSocketResourceOptions;
query?: Record<string, string>;
timeout?: number;
}): AbortableProcess<WebSocketResource> {
const fixedScheme = this.options.url
.replace('https://', 'wss://')
.replace('http://', 'ws://');
const headers = {
'User-Agent': getUserAgent(this.options.version),
};
const client = new WebSocketClient({
tlsOptions: {
ca: this.options.certificateAuthority,
agent: this.proxyAgent,
},
maxReceivedFrameSize: 0x210000,
});
const queryWithDefaults = {
agent: 'OWD',
version: this.options.version,
...query,
};
client.connect(
`${fixedScheme}${path}?${qs.encode(queryWithDefaults)}`,
undefined,
undefined,
headers
);
const url = `${this.options.url}${path}?${qs.encode(queryWithDefaults)}`;
const { stack } = new Error();
return connectWebSocket({
url,
certificateAuthority: this.options.certificateAuthority,
version: this.options.version,
proxyAgent: this.proxyAgent,
const { promise, resolve, reject } = explodePromise<WebSocketResource>();
const timer = Timers.setTimeout(() => {
reject(new ConnectTimeoutError('Connection timed out'));
client.abort();
}, timeout);
let resource: WebSocketResource | undefined;
client.on('connect', socket => {
Timers.clearTimeout(timer);
resource = new WebSocketResource(socket, resourceOptions);
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', e => {
Timers.clearTimeout(timer);
reject(
new HTTPError('connectResource: connectFailed', {
code: -1,
headers: {},
response: e.toString(),
stack,
})
);
});
return new AbortableProcess<WebSocketResource>(
`SocketManager.connectResource(${path})`,
{
abort() {
if (resource) {
log.warn(`SocketManager closing socket ${path}`);
resource.close(3000, 'aborted');
} else {
log.warn(`SocketManager aborting connection ${path}`);
Timers.clearTimeout(timer);
client.abort();
}
},
createResource(socket: WebSocket): WebSocketResource {
return new WebSocketResource(socket, resourceOptions);
},
promise
);
});
}
private static async checkResource(

View File

@ -31,6 +31,7 @@ import { toWebSafeBase64 } from '../util/webSafeBase64';
import type { SocketStatus } from '../types/SocketStatus';
import { toLogFormat } from '../types/errors';
import { isPackIdValid, redactPackId } from '../types/Stickers';
import type { UUIDStringType } from '../types/UUID';
import * as Bytes from '../Bytes';
import {
constantTimeEqual,
@ -49,6 +50,7 @@ import type {
StorageServiceCredentials,
} from '../textsecure.d';
import { SocketManager } from './SocketManager';
import { CDSSocketManager } from './CDSSocketManager';
import type WebSocketResource from './WebsocketResources';
import { SignalService as Proto } from '../protobuf';
@ -566,6 +568,9 @@ type InitializeOptionsType = {
directoryEnclaveId: string;
directoryTrustAnchor: string;
directoryUrl: string;
directoryV2Url: string;
directoryV2PublicKey: string;
directoryV2CodeHash: string;
cdnUrlObject: {
readonly '0': string;
readonly [propName: string]: string;
@ -788,7 +793,10 @@ export type WebAPIType = {
getStorageRecords: MessageSender['getStorageRecords'];
getUuidsForE164s: (
e164s: ReadonlyArray<string>
) => Promise<Dictionary<string | null>>;
) => Promise<Dictionary<UUIDStringType | null>>;
getUuidsForE164sV2: (
e164s: ReadonlyArray<string>
) => Promise<Dictionary<UUIDStringType | null>>;
fetchLinkPreviewMetadata: (
href: string,
abortSignal: AbortSignal
@ -925,6 +933,9 @@ export function initialize({
directoryEnclaveId,
directoryTrustAnchor,
directoryUrl,
directoryV2Url,
directoryV2PublicKey,
directoryV2CodeHash,
cdnUrlObject,
certificateAuthority,
contentProxyUrl,
@ -949,6 +960,15 @@ export function initialize({
if (!is.string(directoryUrl)) {
throw new Error('WebAPI.initialize: Invalid directory url');
}
if (!is.string(directoryV2Url)) {
throw new Error('WebAPI.initialize: Invalid directory V2 url');
}
if (!is.string(directoryV2PublicKey)) {
throw new Error('WebAPI.initialize: Invalid directory V2 public key');
}
if (!is.string(directoryV2CodeHash)) {
throw new Error('WebAPI.initialize: Invalid directory V2 code hash');
}
if (!is.object(cdnUrlObject)) {
throw new Error('WebAPI.initialize: Invalid cdnUrlObject');
}
@ -1009,6 +1029,15 @@ export function initialize({
socketManager.authenticate({ username, password });
}
const cdsSocketManager = new CDSSocketManager({
url: directoryV2Url,
publicKey: directoryV2PublicKey,
codeHash: directoryV2CodeHash,
certificateAuthority,
version,
proxyUrl,
});
let fetchForLinkPreviews: linkPreviewFetch.FetchFn;
if (proxyUrl) {
const agent = new ProxyAgent(proxyUrl);
@ -1057,6 +1086,7 @@ export function initialize({
getStorageManifest,
getStorageRecords,
getUuidsForE164s,
getUuidsForE164sV2,
makeProxiedRequest,
makeSfuRequest,
modifyGroup,
@ -2758,7 +2788,7 @@ export function initialize({
async function getUuidsForE164s(
e164s: ReadonlyArray<string>
): Promise<Dictionary<string | null>> {
): Promise<Dictionary<UUIDStringType | null>> {
const directoryAuth = await getDirectoryAuth();
const attestationResult = await putRemoteAttestation(directoryAuth);
@ -2831,5 +2861,13 @@ export function initialize({
return zipObject(e164s, uuids);
}
async function getUuidsForE164sV2(
e164s: ReadonlyArray<string>
): Promise<Dictionary<UUIDStringType | null>> {
const uuids = await cdsSocketManager.request({ e164s });
return zipObject(e164s, uuids);
}
}
}

127
ts/textsecure/WebSocket.ts Normal file
View File

@ -0,0 +1,127 @@
// 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 * 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;
export type IResource = {
close(code: number, reason: string): void;
};
export type ConnectOptionsType<Resource extends IResource> = Readonly<{
url: string;
certificateAuthority: string;
version: string;
proxyAgent?: ReturnType<typeof ProxyAgent>;
timeout?: number;
createResource(socket: WebSocket): Resource;
}>;
export function connect<Resource extends IResource>({
url,
certificateAuthority,
version,
proxyAgent,
timeout = TEN_SECONDS,
createResource,
}: ConnectOptionsType<Resource>): AbortableProcess<Resource> {
const fixedScheme = url
.replace('https://', 'wss://')
.replace('http://', 'ws://');
const headers = {
'User-Agent': getUserAgent(version),
};
const client = new WebSocketClient({
tlsOptions: {
ca: certificateAuthority,
agent: proxyAgent,
},
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);
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', e => {
Timers.clearTimeout(timer);
reject(
new HTTPError('connectResource: connectFailed', {
code: -1,
headers: {},
response: e.toString(),
stack,
})
);
});
return new AbortableProcess<Resource>(
`WebSocket.connect(${url})`,
{
abort() {
if (resource) {
log.warn(`WebSocket: closing socket ${url}`);
resource.close(3000, 'aborted');
} else {
log.warn(`WebSocket: aborting connection ${url}`);
Timers.clearTimeout(timer);
client.abort();
}
},
},
promise
);
}

View File

@ -1717,10 +1717,10 @@
"@react-spring/shared" "~9.2.6-beta.0"
"@react-spring/types" "~9.2.6-beta.0"
"@signalapp/signal-client@0.9.5":
version "0.9.5"
resolved "https://registry.yarnpkg.com/@signalapp/signal-client/-/signal-client-0.9.5.tgz#b133305aa39c00ceae8743316efd0cc0bfe02e2d"
integrity sha512-cLKo6vzrp3MWyTv+LJnX1KGSLS4TUZVZz20NicAuOks926+E5C1xKZEYhD0sTMsbD1CdHLbPqqw8cQy5oXWHag==
"@signalapp/signal-client@0.9.8":
version "0.9.8"
resolved "https://registry.yarnpkg.com/@signalapp/signal-client/-/signal-client-0.9.8.tgz#e3bd9795eb29ee0246adf0a5245de06054d69180"
integrity sha512-ESm/n2rtVtDVdQvO9ad8y5Hh3grf+HWtM+nzZHYTw3QpvhaYONl3xUBAs1mbGYp/6Gwf83qJ/Le9rduFfw/lVQ==
dependencies:
node-gyp-build "^4.2.3"
uuid "^8.3.0"