libsignal authenticated websocket

This commit is contained in:
Sergey Skrobotov 2024-08-06 14:21:15 -07:00 committed by GitHub
parent 31bcb1e4cc
commit de33410be1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 470 additions and 286 deletions

View file

@ -26,6 +26,7 @@ export type ConfigKeyType =
| 'desktop.retryRespondMaxAge'
| 'desktop.senderKey.retry'
| 'desktop.senderKeyMaxAge'
| 'desktop.experimentalTransport.enableAuth'
| 'desktop.experimentalTransportEnabled.alpha'
| 'desktop.experimentalTransportEnabled.beta'
| 'desktop.experimentalTransportEnabled.prod'

View file

@ -7,7 +7,7 @@ import { assert } from 'chai';
import Long from 'long';
import MessageReceiver from '../textsecure/MessageReceiver';
import { IncomingWebSocketRequest } from '../textsecure/WebsocketResources';
import { IncomingWebSocketRequestLegacy } from '../textsecure/WebsocketResources';
import type { WebAPIType } from '../textsecure/WebAPI';
import type { DecryptionErrorEvent } from '../textsecure/messageReceiverEvents';
import { generateAci } from '../types/ServiceId';
@ -53,7 +53,7 @@ describe('MessageReceiver', () => {
}).finish();
messageReceiver.handleRequest(
new IncomingWebSocketRequest(
new IncomingWebSocketRequestLegacy(
{
id: Long.fromNumber(1),
verb: 'PUT',

View file

@ -16,7 +16,9 @@ import Long from 'long';
import { dropNull } from '../util/dropNull';
import { SignalService as Proto } from '../protobuf';
import WebSocketResource from '../textsecure/WebsocketResources';
import WebSocketResource, {
ServerRequestType,
} from '../textsecure/WebsocketResources';
describe('WebSocket-Resource', () => {
class FakeSocket extends EventEmitter {
@ -72,8 +74,7 @@ describe('WebSocket-Resource', () => {
new WebSocketResource(socket as WebSocket, {
name: 'test',
handleRequest(request: any) {
assert.strictEqual(request.verb, 'PUT');
assert.strictEqual(request.path, '/some/path');
assert.strictEqual(request.requestType, ServerRequestType.ApiMessage);
assert.deepEqual(request.body, new Uint8Array([1, 2, 3]));
request.respond(200, 'OK');
},
@ -87,7 +88,7 @@ describe('WebSocket-Resource', () => {
request: {
id: requestId,
verb: 'PUT',
path: '/some/path',
path: ServerRequestType.ApiMessage.toString(),
body: new Uint8Array([1, 2, 3]),
},
}).finish(),

View file

@ -15,39 +15,40 @@ import type {
WebAPIType,
} from './WebAPI';
import type {
CompatSignedPreKeyType,
CompatPreKeyType,
CompatSignedPreKeyType,
KeyPairType,
KyberPreKeyType,
PniKeyMaterialType,
} from './Types.d';
import ProvisioningCipher from './ProvisioningCipher';
import type { IncomingWebSocketRequest } from './WebsocketResources';
import { ServerRequestType } from './WebsocketResources';
import createTaskWithTimeout from './TaskWithTimeout';
import * as Bytes from '../Bytes';
import * as Errors from '../types/errors';
import { senderCertificateService } from '../services/senderCertificate';
import { backupsService } from '../services/backups';
import {
decryptDeviceName,
deriveAccessKey,
deriveStorageServiceKey,
encryptDeviceName,
generateRegistrationId,
getRandomBytes,
decryptDeviceName,
encryptDeviceName,
deriveStorageServiceKey,
} from '../Crypto';
import {
generateKeyPair,
generateSignedPreKey,
generatePreKey,
generateKyberPreKey,
generatePreKey,
generateSignedPreKey,
} from '../Curve';
import type { ServiceIdString, AciString, PniString } from '../types/ServiceId';
import type { AciString, PniString, ServiceIdString } from '../types/ServiceId';
import {
ServiceIdKind,
normalizePni,
toTaggedPni,
isUntaggedPniString,
normalizePni,
ServiceIdKind,
toTaggedPni,
} from '../types/ServiceId';
import { normalizeAci } from '../util/normalizeAci';
import { drop } from '../util/drop';
@ -367,8 +368,7 @@ export default class AccountManager extends EventTarget {
const wsr = await this.server.getProvisioningResource({
handleRequest(request: IncomingWebSocketRequest) {
if (
request.path === '/v1/address' &&
request.verb === 'PUT' &&
request.requestType === ServerRequestType.ProvisioningAddress &&
request.body
) {
const proto = Proto.ProvisioningUuid.decode(request.body);
@ -388,8 +388,7 @@ export default class AccountManager extends EventTarget {
setProvisioningUrl(url);
request.respond(200, 'OK');
} else if (
request.path === '/v1/message' &&
request.verb === 'PUT' &&
request.requestType === ServerRequestType.ProvisioningMessage &&
request.body
) {
const envelope = Proto.ProvisionEnvelope.decode(request.body);
@ -397,7 +396,7 @@ export default class AccountManager extends EventTarget {
wsr.close();
envelopeCallbacks?.resolve(envelope);
} else {
log.error('Unknown websocket message', request.path);
log.error('Unknown websocket message', request.requestType);
}
},
});

View file

@ -13,13 +13,13 @@ import type {
UnidentifiedSenderMessageContent,
} from '@signalapp/libsignal-client';
import {
ContentHint,
CiphertextMessageType,
ContentHint,
DecryptionErrorMessage,
groupDecrypt,
PlaintextContent,
PreKeySignalMessage,
Pni,
PreKeySignalMessage,
processSenderKeyDistributionMessage,
ProtocolAddress,
PublicKey,
@ -40,7 +40,7 @@ import {
SignedPreKeys,
} from '../LibSignalStores';
import { verifySignature } from '../Curve';
import { strictAssert, assertDev } from '../util/assert';
import { assertDev, strictAssert } from '../util/assert';
import type { BatcherType } from '../util/batcher';
import { createBatcher } from '../util/batcher';
import { drop } from '../util/drop';
@ -48,6 +48,7 @@ import { dropNull } from '../util/dropNull';
import { parseIntOrThrow } from '../util/parseIntOrThrow';
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
import { Zone } from '../util/Zone';
import * as durations from '../util/durations';
import { DurationInSeconds, SECOND } from '../util/durations';
import type { AttachmentType } from '../types/Attachment';
import { Address } from '../types/Address';
@ -55,13 +56,13 @@ import { QualifiedAddress } from '../types/QualifiedAddress';
import { normalizeStoryDistributionId } from '../types/StoryDistributionId';
import type { ServiceIdString } from '../types/ServiceId';
import {
ServiceIdKind,
normalizeServiceId,
normalizePni,
isPniString,
isUntaggedPniString,
isServiceIdString,
fromPniObject,
isPniString,
isServiceIdString,
isUntaggedPniString,
normalizePni,
normalizeServiceId,
ServiceIdKind,
toTaggedPni,
} from '../types/ServiceId';
import { normalizeAci } from '../util/normalizeAci';
@ -75,70 +76,70 @@ import createTaskWithTimeout from './TaskWithTimeout';
import {
processAttachment,
processDataMessage,
processPreview,
processGroupV2Context,
processPreview,
} from './processDataMessage';
import { processSyncMessage } from './processSyncMessage';
import type { EventHandler } from './EventTarget';
import EventTarget from './EventTarget';
import { downloadAttachment } from './downloadAttachment';
import type { IncomingWebSocketRequest } from './WebsocketResources';
import { ServerRequestType } from './WebsocketResources';
import { parseContactsV2 } from './ContactsParser';
import type { WebAPIType } from './WebAPI';
import type { Storage } from './Storage';
import { WarnOnlyError } from './Errors';
import * as Bytes from '../Bytes';
import type {
IRequestHandler,
ProcessedAttachment,
ProcessedDataMessage,
ProcessedPreview,
ProcessedSyncMessage,
ProcessedSent,
ProcessedEnvelope,
IRequestHandler,
ProcessedPreview,
ProcessedSent,
ProcessedSyncMessage,
UnprocessedType,
} from './Types.d';
import type {
ConversationToDelete,
DeleteForMeSyncEventData,
DeleteForMeSyncTarget,
MessageToDelete,
ReadSyncEventData,
ViewSyncEventData,
} from './messageReceiverEvents';
import {
CallEventSyncEvent,
CallLinkUpdateSyncEvent,
CallLogEventSyncEvent,
ConfigurationEvent,
ContactSyncEvent,
DecryptionErrorEvent,
DeleteForMeSyncEvent,
DeliveryEvent,
EmptyEvent,
EnvelopeQueuedEvent,
EnvelopeUnsealedEvent,
ProgressEvent,
TypingEvent,
ErrorEvent,
DeliveryEvent,
DecryptionErrorEvent,
SentEvent,
ProfileKeyUpdateEvent,
InvalidPlaintextEvent,
MessageEvent,
RetryRequestEvent,
ReadEvent,
ViewEvent,
ConfigurationEvent,
ViewOnceOpenSyncEvent,
MessageRequestResponseEvent,
FetchLatestEvent,
InvalidPlaintextEvent,
KeysEvent,
StickerPackEvent,
MessageEvent,
MessageRequestResponseEvent,
ProfileKeyUpdateEvent,
ProgressEvent,
ReadEvent,
ReadSyncEvent,
ViewSyncEvent,
ContactSyncEvent,
RetryRequestEvent,
SentEvent,
StickerPackEvent,
StoryRecipientUpdateEvent,
CallLogEventSyncEvent,
CallLinkUpdateSyncEvent,
DeleteForMeSyncEvent,
} from './messageReceiverEvents';
import type {
MessageToDelete,
DeleteForMeSyncEventData,
DeleteForMeSyncTarget,
ConversationToDelete,
ViewSyncEventData,
ReadSyncEventData,
TypingEvent,
ViewEvent,
ViewOnceOpenSyncEvent,
ViewSyncEvent,
} from './messageReceiverEvents';
import * as log from '../logging/log';
import * as durations from '../util/durations';
import { areArraysMatchingSets } from '../util/areArraysMatchingSets';
import { generateBlurHash } from '../util/generateBlurHash';
import { TEXT_ATTACHMENT } from '../types/MIME';
@ -385,11 +386,11 @@ export default class MessageReceiver
public handleRequest(request: IncomingWebSocketRequest): void {
// We do the message decryption here, instead of in the ordered pending queue,
// to avoid exposing the time it took us to process messages through the time-to-ack.
log.info('MessageReceiver: got request', request.verb, request.path);
if (request.path !== '/api/v1/message') {
log.info('MessageReceiver: got request', request.requestType);
if (request.requestType !== ServerRequestType.ApiMessage) {
request.respond(200, 'OK');
if (request.verb === 'PUT' && request.path === '/api/v1/queue/empty') {
if (request.requestType === ServerRequestType.ApiEmptyQueue) {
drop(
this.incomingQueue.add(
createTaskWithTimeout(
@ -406,8 +407,6 @@ export default class MessageReceiver
}
const job = async () => {
const headers = request.headers || [];
if (!request.body) {
throw new Error(
'MessageReceiver.handleRequest: request.body was falsey!'
@ -429,7 +428,10 @@ export default class MessageReceiver
receivedAtCounter: incrementMessageCounter(),
receivedAtDate: Date.now(),
// Calculate the message age (time on server).
messageAgeSec: this.calculateMessageAge(headers, serverTimestamp),
messageAgeSec: this.calculateMessageAge(
request.timestamp,
serverTimestamp
),
// Proto.Envelope fields
type: decoded.type ?? Proto.Envelope.Type.UNKNOWN,
@ -728,32 +730,15 @@ export default class MessageReceiver
}
private calculateMessageAge(
headers: ReadonlyArray<string>,
serverTimestamp?: number
timestamp: number | undefined,
serverTimestamp: number | undefined
): number {
let messageAgeSec = 0; // Default to 0 in case of unreliable parameters.
if (serverTimestamp) {
// The 'X-Signal-Timestamp' is usually the last item, so start there.
let it = headers.length;
// eslint-disable-next-line no-plusplus
while (--it >= 0) {
const match = headers[it].match(/^X-Signal-Timestamp:\s*(\d+)\s*$/i);
if (match && match.length === 2) {
const timestamp = Number(match[1]);
// One final sanity check, the timestamp when a message is pulled from
// the server should be later than when it was pushed.
if (timestamp > serverTimestamp) {
messageAgeSec = Math.floor((timestamp - serverTimestamp) / 1000);
}
break;
}
}
}
return messageAgeSec;
// Default to 0 in case of unreliable parameters.
// One final sanity check, the timestamp when a message is pulled from
// the server should be later than when it was pushed.
return serverTimestamp && timestamp && timestamp > serverTimestamp
? Math.floor((timestamp - serverTimestamp) / 1000)
: 0;
}
private async addToQueue<T>(

View file

@ -13,14 +13,14 @@ import { AbortableProcess } from '../util/AbortableProcess';
import { strictAssert } from '../util/assert';
import {
BackOff,
FIBONACCI_TIMEOUTS,
EXTENDED_FIBONACCI_TIMEOUTS,
FIBONACCI_TIMEOUTS,
} from '../util/BackOff';
import * as durations from '../util/durations';
import { sleep } from '../util/sleep';
import { drop } from '../util/drop';
import { createProxyAgent } from '../util/createProxyAgent';
import type { ProxyAgent } from '../util/createProxyAgent';
import { createProxyAgent } from '../util/createProxyAgent';
import { SocketStatus } from '../types/SocketStatus';
import * as Errors from '../types/errors';
import * as Bytes from '../Bytes';
@ -32,7 +32,8 @@ import type {
WebSocketResourceOptions,
} from './WebsocketResources';
import WebSocketResource, {
LibsignalWebSocketResource,
connectAuthenticatedLibsignal,
connectUnauthenticatedLibsignal,
TransportOption,
WebSocketResourceWithShadowing,
} from './WebsocketResources';
@ -166,22 +167,38 @@ export class SocketManager extends EventListener {
this.setStatus(SocketStatus.CONNECTING);
const process = this.connectResource({
name: AUTHENTICATED_CHANNEL_NAME,
path: '/v1/websocket/',
query: { login: username, password },
proxyAgent: await this.getProxyAgent(),
resourceOptions: {
name: AUTHENTICATED_CHANNEL_NAME,
keepalive: { path: '/v1/keepalive' },
handleRequest: (req: IncomingWebSocketRequest): void => {
this.queueOrHandleRequest(req);
},
},
extraHeaders: {
'X-Signal-Receive-Stories': String(!this.hasStoriesDisabled),
},
});
const proxyAgent = await this.getProxyAgent();
const useLibsignalTransport =
window.Signal.RemoteConfig.isEnabled(
'desktop.experimentalTransport.enableAuth'
) && this.transportOption(proxyAgent) === TransportOption.Libsignal;
const process = useLibsignalTransport
? connectAuthenticatedLibsignal({
libsignalNet: this.libsignalNet,
name: AUTHENTICATED_CHANNEL_NAME,
credentials: this.credentials,
handler: (req: IncomingWebSocketRequest): void => {
this.queueOrHandleRequest(req);
},
receiveStories: !this.hasStoriesDisabled,
})
: this.connectResource({
name: AUTHENTICATED_CHANNEL_NAME,
path: '/v1/websocket/',
query: { login: username, password },
resourceOptions: {
name: AUTHENTICATED_CHANNEL_NAME,
keepalive: { path: '/v1/keepalive' },
handleRequest: (req: IncomingWebSocketRequest): void => {
this.queueOrHandleRequest(req);
},
},
extraHeaders: {
'X-Signal-Receive-Stories': String(!this.hasStoriesDisabled),
},
proxyAgent,
});
// Cancel previous connect attempt or close socket
this.authenticated?.abort();
@ -575,10 +592,10 @@ export class SocketManager extends EventListener {
}
private connectLibsignalUnauthenticated(): AbortableProcess<IWebSocketResource> {
return LibsignalWebSocketResource.connect(
this.libsignalNet,
UNAUTHENTICATED_CHANNEL_NAME
);
return connectUnauthenticatedLibsignal({
libsignalNet: this.libsignalNet,
name: UNAUTHENTICATED_CHANNEL_NAME,
});
}
private async getUnauthenticatedResource(): Promise<IWebSocketResource> {
@ -724,10 +741,10 @@ export class SocketManager extends EventListener {
options: WebSocketResourceOptions
): AbortableProcess<IWebSocketResource> {
// creating an `AbortableProcess` of libsignal websocket connection
const shadowingConnection = LibsignalWebSocketResource.connect(
this.libsignalNet,
options.name
);
const shadowingConnection = connectUnauthenticatedLibsignal({
libsignalNet: this.libsignalNet,
name: options.name,
});
const shadowWrapper = async () => {
// if main connection results in an error,
// it's propagated as the error of the resulting process

View file

@ -37,6 +37,8 @@ import { random } from 'lodash';
import type { ChatServiceDebugInfo } from '@signalapp/libsignal-client/Native';
import type { Net } from '@signalapp/libsignal-client';
import { Buffer } from 'node:buffer';
import type { ChatServerMessageAck } from '@signalapp/libsignal-client/dist/net';
import type { EventHandler } from './EventTarget';
import EventTarget from './EventTarget';
@ -54,6 +56,7 @@ import { isProduction } from '../util/version';
import { ToastType } from '../types/Toast';
import { AbortableProcess } from '../util/AbortableProcess';
import type { WebAPICredentials } from './Types';
const THIRTY_SECONDS = 30 * durations.SECOND;
@ -166,16 +169,49 @@ export namespace AggregatedStats {
}
}
export class IncomingWebSocketRequest {
export enum ServerRequestType {
ApiMessage = '/api/v1/message',
ApiEmptyQueue = '/api/v1/queue/empty',
ProvisioningMessage = '/v1/message',
ProvisioningAddress = '/v1/address',
Unknown = 'unknown',
}
export type IncomingWebSocketRequest = {
readonly requestType: ServerRequestType;
readonly body: Uint8Array | undefined;
readonly timestamp: number | undefined;
respond(status: number, message: string): void;
};
export class IncomingWebSocketRequestLibsignal
implements IncomingWebSocketRequest
{
constructor(
readonly requestType: ServerRequestType,
readonly body: Uint8Array | undefined,
readonly timestamp: number | undefined,
private readonly ack: ChatServerMessageAck | undefined
) {}
respond(status: number, _message: string): void {
if (this.ack) {
drop(this.ack.send(status));
}
}
}
export class IncomingWebSocketRequestLegacy
implements IncomingWebSocketRequest
{
private readonly id: Long;
public readonly verb: string;
public readonly path: string;
public readonly requestType: ServerRequestType;
public readonly body: Uint8Array | undefined;
public readonly headers: ReadonlyArray<string>;
public readonly timestamp: number | undefined;
constructor(
request: Proto.IWebSocketRequestMessage,
@ -186,10 +222,9 @@ export class IncomingWebSocketRequest {
strictAssert(request.path, 'request without path');
this.id = request.id;
this.verb = request.verb;
this.path = request.path;
this.requestType = resolveType(request.path, request.verb);
this.body = dropNull(request.body);
this.headers = request.headers || [];
this.timestamp = resolveTimestamp(request.headers || []);
}
public respond(status: number, message: string): void {
@ -202,6 +237,35 @@ export class IncomingWebSocketRequest {
}
}
function resolveType(path: string, verb: string): ServerRequestType {
if (path === ServerRequestType.ApiMessage) {
return ServerRequestType.ApiMessage;
}
if (path === ServerRequestType.ApiEmptyQueue && verb === 'PUT') {
return ServerRequestType.ApiEmptyQueue;
}
if (path === ServerRequestType.ProvisioningAddress && verb === 'PUT') {
return ServerRequestType.ProvisioningAddress;
}
if (path === ServerRequestType.ProvisioningMessage && verb === 'PUT') {
return ServerRequestType.ProvisioningMessage;
}
return ServerRequestType.Unknown;
}
function resolveTimestamp(headers: ReadonlyArray<string>): number | undefined {
// The 'X-Signal-Timestamp' is usually the last item, so start there.
let it = headers.length;
// eslint-disable-next-line no-plusplus
while (--it >= 0) {
const match = headers[it].match(/^X-Signal-Timestamp:\s*(\d+)\s*$/i);
if (match && match.length === 2) {
return Number(match[1]);
}
}
return undefined;
}
export type SendRequestOptions = Readonly<{
verb: string;
path: string;
@ -266,6 +330,100 @@ export interface IWebSocketResource extends IResource {
localPort(): number | undefined;
}
export function connectUnauthenticatedLibsignal({
libsignalNet,
name,
}: {
libsignalNet: Net.Net;
name: string;
}): AbortableProcess<LibsignalWebSocketResource> {
return connectLibsignal(libsignalNet.newUnauthenticatedChatService(), name);
}
export function connectAuthenticatedLibsignal({
libsignalNet,
name,
credentials,
handler,
receiveStories,
}: {
libsignalNet: Net.Net;
name: string;
credentials: WebAPICredentials;
handler: (request: IncomingWebSocketRequest) => void;
receiveStories: boolean;
}): AbortableProcess<LibsignalWebSocketResource> {
const listener = {
onIncomingMessage(
envelope: Buffer,
timestamp: number,
ack: ChatServerMessageAck
): void {
const request = new IncomingWebSocketRequestLibsignal(
ServerRequestType.ApiMessage,
envelope,
timestamp,
ack
);
handler(request);
},
onQueueEmpty(): void {
const request = new IncomingWebSocketRequestLibsignal(
ServerRequestType.ApiEmptyQueue,
undefined,
undefined,
undefined
);
handler(request);
},
onConnectionInterrupted(): void {
log.warn(`LibsignalWebSocketResource(${name}): connection interrupted`);
},
};
return connectLibsignal(
libsignalNet.newAuthenticatedChatService(
credentials.username,
credentials.password,
receiveStories,
listener
),
name
);
}
function connectLibsignal(
chatService: Net.ChatService,
name: string
): AbortableProcess<LibsignalWebSocketResource> {
const connectAsync = async () => {
try {
const debugInfo = await chatService.connect();
log.info(`LibsignalWebSocketResource(${name}) connected`, debugInfo);
return new LibsignalWebSocketResource(
chatService,
IpVersion.fromDebugInfoCode(debugInfo.ipType)
);
} catch (error) {
// Handle any errors that occur during connection
log.error(
`LibsignalWebSocketResource(${name}) connection failed`,
Errors.toLogFormat(error)
);
throw error;
}
};
return new AbortableProcess<LibsignalWebSocketResource>(
`LibsignalWebSocketResource.connect(${name})`,
{
abort() {
// if interrupted, trying to disconnect
drop(chatService.disconnect());
},
},
connectAsync()
);
}
export class LibsignalWebSocketResource
extends EventTarget
implements IWebSocketResource
@ -277,40 +435,6 @@ export class LibsignalWebSocketResource
super();
}
public static connect(
libsignalNet: Net.Net,
name: string
): AbortableProcess<LibsignalWebSocketResource> {
const chatService = libsignalNet.newChatService();
const connectAsync = async () => {
try {
const debugInfo = await chatService.connectUnauthenticated();
log.info(`LibsignalWebSocketResource(${name}) connected`, debugInfo);
return new LibsignalWebSocketResource(
chatService,
IpVersion.fromDebugInfoCode(debugInfo.ipType)
);
} catch (error) {
// Handle any errors that occur during connection
log.error(
`LibsignalWebSocketResource(${name}) connection failed`,
Errors.toLogFormat(error)
);
throw error;
}
};
return new AbortableProcess<LibsignalWebSocketResource>(
`LibsignalWebSocketResource.connect(${name})`,
{
abort() {
// if interrupted, trying to disconnect
drop(chatService.disconnect());
},
},
connectAsync()
);
}
public localPort(): number | undefined {
return undefined;
}
@ -348,14 +472,13 @@ export class LibsignalWebSocketResource
public async sendRequestGetDebugInfo(
options: SendRequestOptions
): Promise<[Response, ChatServiceDebugInfo]> {
const { response, debugInfo } =
await this.chatService.unauthenticatedFetchAndDebug({
verb: options.verb,
path: options.path,
headers: options.headers ? options.headers : [],
body: options.body,
timeoutMillis: options.timeout,
});
const { response, debugInfo } = await this.chatService.fetchAndDebug({
verb: options.verb,
path: options.path,
headers: options.headers ? options.headers : [],
body: options.body,
timeoutMillis: options.timeout,
});
return [
new Response(response.body, {
status: response.status,
@ -765,7 +888,7 @@ export default class WebSocketResource
this.options.handleRequest ||
(request => request.respond(404, 'Not found'));
const incomingRequest = new IncomingWebSocketRequest(
const incomingRequest = new IncomingWebSocketRequestLegacy(
message.request,
(bytes: Buffer): void => {
this.removeActive(incomingRequest);