Faster WebSocket reconnects
This commit is contained in:
parent
3cac4a19e1
commit
17e6ec468e
25 changed files with 940 additions and 677 deletions
|
@ -168,15 +168,15 @@
|
|||
this.interval = setInterval(() => {
|
||||
const status = window.getSocketStatus();
|
||||
switch (status) {
|
||||
case WebSocket.CONNECTING:
|
||||
case 'CONNECTING':
|
||||
break;
|
||||
case WebSocket.OPEN:
|
||||
case 'OPEN':
|
||||
clearInterval(this.interval);
|
||||
// if we've connected, we can wait for real empty event
|
||||
this.interval = null;
|
||||
break;
|
||||
case WebSocket.CLOSING:
|
||||
case WebSocket.CLOSED:
|
||||
case 'CLOSING':
|
||||
case 'CLOSED':
|
||||
clearInterval(this.interval);
|
||||
this.interval = null;
|
||||
// if we failed to connect, we pretend we got an empty event
|
||||
|
@ -184,7 +184,7 @@
|
|||
break;
|
||||
default:
|
||||
window.log.warn(
|
||||
'startConnectionListener: Found unexpected socket status; calling onEmpty() manually.'
|
||||
`startConnectionListener: Found unexpected socket status ${status}; calling onEmpty() manually.`
|
||||
);
|
||||
this.onEmpty();
|
||||
break;
|
||||
|
|
|
@ -14,7 +14,12 @@ const fakeAPI = {
|
|||
getAvatar: fakeCall,
|
||||
getDevices: fakeCall,
|
||||
// getKeysForIdentifier : fakeCall,
|
||||
getMessageSocket: () => new window.MockSocket('ws://localhost:8081/'),
|
||||
getMessageSocket: async () => ({
|
||||
on() {},
|
||||
removeListener() {},
|
||||
close() {},
|
||||
sendBytes() {},
|
||||
}),
|
||||
getMyKeys: fakeCall,
|
||||
getProfile: fakeCall,
|
||||
getProvisioningSocket: fakeCall,
|
||||
|
|
|
@ -37,7 +37,6 @@
|
|||
<script type="text/javascript" src="crypto_test.js"></script>
|
||||
<script type="text/javascript" src="contacts_parser_test.js"></script>
|
||||
<script type="text/javascript" src="generate_keys_test.js"></script>
|
||||
<script type="text/javascript" src="websocket-resources_test.js"></script>
|
||||
<script type="text/javascript" src="task_with_timeout_test.js"></script>
|
||||
<script type="text/javascript" src="account_manager_test.js"></script>
|
||||
<script type="text/javascript" src="message_receiver_test.js"></script>
|
||||
|
|
|
@ -1,237 +0,0 @@
|
|||
// Copyright 2015-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
describe('WebSocket-Resource', () => {
|
||||
describe('requests and responses', () => {
|
||||
it('receives requests and sends responses', done => {
|
||||
// mock socket
|
||||
const requestId = '1';
|
||||
const socket = {
|
||||
send(data) {
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
data
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE
|
||||
);
|
||||
assert.strictEqual(message.response.message, 'OK');
|
||||
assert.strictEqual(message.response.status, 200);
|
||||
assert.strictEqual(message.response.id.toString(), requestId);
|
||||
done();
|
||||
},
|
||||
addEventListener() {},
|
||||
};
|
||||
|
||||
// actual test
|
||||
this.resource = new window.textsecure.WebSocketResource(socket, {
|
||||
handleRequest(request) {
|
||||
assert.strictEqual(request.verb, 'PUT');
|
||||
assert.strictEqual(request.path, '/some/path');
|
||||
assertEqualArrayBuffers(
|
||||
request.body.toArrayBuffer(),
|
||||
window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([1, 2, 3])
|
||||
)
|
||||
);
|
||||
request.respond(200, 'OK');
|
||||
},
|
||||
});
|
||||
|
||||
// mock socket request
|
||||
socket.onmessage({
|
||||
data: new Blob([
|
||||
new window.textsecure.protobuf.WebSocketMessage({
|
||||
type: window.textsecure.protobuf.WebSocketMessage.Type.REQUEST,
|
||||
request: {
|
||||
id: requestId,
|
||||
verb: 'PUT',
|
||||
path: '/some/path',
|
||||
body: window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([1, 2, 3])
|
||||
),
|
||||
},
|
||||
})
|
||||
.encode()
|
||||
.toArrayBuffer(),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('sends requests and receives responses', done => {
|
||||
// mock socket and request handler
|
||||
let requestId;
|
||||
const socket = {
|
||||
send(data) {
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
data
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
assert.strictEqual(message.request.verb, 'PUT');
|
||||
assert.strictEqual(message.request.path, '/some/path');
|
||||
assertEqualArrayBuffers(
|
||||
message.request.body.toArrayBuffer(),
|
||||
window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([1, 2, 3])
|
||||
)
|
||||
);
|
||||
requestId = message.request.id;
|
||||
},
|
||||
addEventListener() {},
|
||||
};
|
||||
|
||||
// actual test
|
||||
const resource = new window.textsecure.WebSocketResource(socket);
|
||||
resource.sendRequest({
|
||||
verb: 'PUT',
|
||||
path: '/some/path',
|
||||
body: window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([1, 2, 3])
|
||||
),
|
||||
error: done,
|
||||
success(message, status) {
|
||||
assert.strictEqual(message, 'OK');
|
||||
assert.strictEqual(status, 200);
|
||||
done();
|
||||
},
|
||||
});
|
||||
|
||||
// mock socket response
|
||||
socket.onmessage({
|
||||
data: new Blob([
|
||||
new window.textsecure.protobuf.WebSocketMessage({
|
||||
type: window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
|
||||
response: { id: requestId, message: 'OK', status: 200 },
|
||||
})
|
||||
.encode()
|
||||
.toArrayBuffer(),
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('close', () => {
|
||||
before(() => {
|
||||
window.WebSocket = MockSocket;
|
||||
});
|
||||
after(() => {
|
||||
window.WebSocket = WebSocket;
|
||||
});
|
||||
it('closes the connection', done => {
|
||||
const mockServer = new MockServer('ws://localhost:8081');
|
||||
mockServer.on('connection', server => {
|
||||
server.on('close', done);
|
||||
});
|
||||
const resource = new window.textsecure.WebSocketResource(
|
||||
new WebSocket('ws://localhost:8081')
|
||||
);
|
||||
resource.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('with a keepalive config', function thisNeeded() {
|
||||
before(() => {
|
||||
window.WebSocket = MockSocket;
|
||||
});
|
||||
after(() => {
|
||||
window.WebSocket = WebSocket;
|
||||
});
|
||||
this.timeout(60000);
|
||||
it('sends keepalives once a minute', done => {
|
||||
const mockServer = new MockServer('ws://localhost:8081');
|
||||
mockServer.on('connection', server => {
|
||||
server.on('message', data => {
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
data
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
assert.strictEqual(message.request.verb, 'GET');
|
||||
assert.strictEqual(message.request.path, '/v1/keepalive');
|
||||
server.close();
|
||||
done();
|
||||
});
|
||||
});
|
||||
this.resource = new window.textsecure.WebSocketResource(
|
||||
new WebSocket('ws://loc1alhost:8081'),
|
||||
{
|
||||
keepalive: { path: '/v1/keepalive' },
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('uses / as a default path', done => {
|
||||
const mockServer = new MockServer('ws://localhost:8081');
|
||||
mockServer.on('connection', server => {
|
||||
server.on('message', data => {
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
data
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
assert.strictEqual(message.request.verb, 'GET');
|
||||
assert.strictEqual(message.request.path, '/');
|
||||
server.close();
|
||||
done();
|
||||
});
|
||||
});
|
||||
this.resource = new window.textsecure.WebSocketResource(
|
||||
new WebSocket('ws://localhost:8081'),
|
||||
{
|
||||
keepalive: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('optionally disconnects if no response', function thisNeeded1(done) {
|
||||
this.timeout(65000);
|
||||
const mockServer = new MockServer('ws://localhost:8081');
|
||||
const socket = new WebSocket('ws://localhost:8081');
|
||||
mockServer.on('connection', server => {
|
||||
server.on('close', done);
|
||||
});
|
||||
this.resource = new window.textsecure.WebSocketResource(socket, {
|
||||
keepalive: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('allows resetting the keepalive timer', function thisNeeded2(done) {
|
||||
this.timeout(65000);
|
||||
const mockServer = new MockServer('ws://localhost:8081');
|
||||
const socket = new WebSocket('ws://localhost:8081');
|
||||
const startTime = Date.now();
|
||||
mockServer.on('connection', server => {
|
||||
server.on('message', data => {
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
data
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
assert.strictEqual(message.request.verb, 'GET');
|
||||
assert.strictEqual(message.request.path, '/');
|
||||
assert(
|
||||
Date.now() > startTime + 60000,
|
||||
'keepalive time should be longer than a minute'
|
||||
);
|
||||
server.close();
|
||||
done();
|
||||
});
|
||||
});
|
||||
const resource = new window.textsecure.WebSocketResource(socket, {
|
||||
keepalive: true,
|
||||
});
|
||||
setTimeout(() => {
|
||||
resource.resetKeepAliveTimer();
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
});
|
9
main.js
9
main.js
|
@ -123,6 +123,7 @@ const {
|
|||
} = require('./ts/types/Settings');
|
||||
const { Environment } = require('./ts/environment');
|
||||
const { ChallengeMainHandler } = require('./ts/main/challengeMain');
|
||||
const { PowerChannel } = require('./ts/main/powerChannel');
|
||||
const { maybeParseUrl, setUrlSearchParams } = require('./ts/util/url');
|
||||
|
||||
const sql = new MainSQL();
|
||||
|
@ -1265,6 +1266,14 @@ app.on('ready', async () => {
|
|||
cleanupOrphanedAttachments,
|
||||
});
|
||||
sqlChannels.initialize(sql);
|
||||
PowerChannel.initialize({
|
||||
send(event) {
|
||||
if (!mainWindow) {
|
||||
return;
|
||||
}
|
||||
mainWindow.webContents.send(event);
|
||||
},
|
||||
});
|
||||
|
||||
// Run window preloading in parallel with database initialization.
|
||||
await createWindow();
|
||||
|
|
|
@ -179,6 +179,15 @@ try {
|
|||
ipc.on('challenge:response', (_event, response) => {
|
||||
Whisper.events.trigger('challengeResponse', response);
|
||||
});
|
||||
|
||||
ipc.on('power-channel:suspend', () => {
|
||||
Whisper.events.trigger('powerMonitorSuspend');
|
||||
});
|
||||
|
||||
ipc.on('power-channel:resume', () => {
|
||||
Whisper.events.trigger('powerMonitorResume');
|
||||
});
|
||||
|
||||
window.sendChallengeRequest = request =>
|
||||
ipc.send('challenge:request', request);
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import { DataMessageClass } from './textsecure.d';
|
|||
import { MessageAttributesType } from './model-types.d';
|
||||
import { WhatIsThis } from './window.d';
|
||||
import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings';
|
||||
import { SocketStatus } from './types/SocketStatus';
|
||||
import { DEFAULT_CONVERSATION_COLOR } from './types/Colors';
|
||||
import { ChallengeHandler } from './challenge';
|
||||
import { isWindowDragElement } from './util/isWindowDragElement';
|
||||
|
@ -38,6 +39,7 @@ import { connectToServerWithStoredCredentials } from './util/connectToServerWith
|
|||
import * as universalExpireTimer from './util/universalExpireTimer';
|
||||
import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation';
|
||||
import { getSendOptions } from './util/getSendOptions';
|
||||
import { BackOff } from './util/BackOff';
|
||||
|
||||
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
||||
|
||||
|
@ -96,6 +98,15 @@ export async function startApp(): Promise<void> {
|
|||
resolveOnAppView = resolve;
|
||||
});
|
||||
|
||||
// Fibonacci timeouts
|
||||
const reconnectBackOff = new BackOff([
|
||||
5 * 1000,
|
||||
10 * 1000,
|
||||
15 * 1000,
|
||||
25 * 1000,
|
||||
40 * 1000,
|
||||
]);
|
||||
|
||||
window.textsecure.protobuf.onLoad(() => {
|
||||
window.storage.onready(() => {
|
||||
senderCertificateService.initialize({
|
||||
|
@ -302,15 +313,15 @@ export async function startApp(): Promise<void> {
|
|||
});
|
||||
|
||||
let messageReceiver: WhatIsThis;
|
||||
let preMessageReceiverStatus: WhatIsThis;
|
||||
let preMessageReceiverStatus: SocketStatus | undefined;
|
||||
window.getSocketStatus = () => {
|
||||
if (messageReceiver) {
|
||||
return messageReceiver.getStatus();
|
||||
}
|
||||
if (window._.isNumber(preMessageReceiverStatus)) {
|
||||
if (preMessageReceiverStatus) {
|
||||
return preMessageReceiverStatus;
|
||||
}
|
||||
return WebSocket.CLOSED;
|
||||
return SocketStatus.CLOSED;
|
||||
};
|
||||
window.Whisper.events = window._.clone(window.Backbone.Events);
|
||||
let accountManager: typeof window.textsecure.AccountManager;
|
||||
|
@ -1549,6 +1560,19 @@ export async function startApp(): Promise<void> {
|
|||
}
|
||||
});
|
||||
|
||||
window.Whisper.events.on('powerMonitorSuspend', () => {
|
||||
window.log.info('powerMonitor: suspend');
|
||||
});
|
||||
|
||||
window.Whisper.events.on('powerMonitorResume', () => {
|
||||
window.log.info('powerMonitor: resume');
|
||||
if (!messageReceiver) {
|
||||
return;
|
||||
}
|
||||
|
||||
messageReceiver.checkSocket();
|
||||
});
|
||||
|
||||
const reconnectToWebSocketQueue = new LatestQueue();
|
||||
|
||||
const enqueueReconnectToWebSocket = () => {
|
||||
|
@ -1884,7 +1908,8 @@ export async function startApp(): Promise<void> {
|
|||
function isSocketOnline() {
|
||||
const socketStatus = window.getSocketStatus();
|
||||
return (
|
||||
socketStatus === WebSocket.CONNECTING || socketStatus === WebSocket.OPEN
|
||||
socketStatus === SocketStatus.CONNECTING ||
|
||||
socketStatus === SocketStatus.OPEN
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1937,7 +1962,7 @@ export async function startApp(): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
preMessageReceiverStatus = WebSocket.CONNECTING;
|
||||
preMessageReceiverStatus = SocketStatus.CONNECTING;
|
||||
|
||||
if (messageReceiver) {
|
||||
await messageReceiver.stopProcessing();
|
||||
|
@ -2020,7 +2045,7 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
window.Signal.Services.initializeGroupCredentialFetcher();
|
||||
|
||||
preMessageReceiverStatus = null;
|
||||
preMessageReceiverStatus = undefined;
|
||||
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
function addQueuedEventListener(name: string, handler: WhatIsThis) {
|
||||
|
@ -2258,6 +2283,8 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
// Intentionally not awaiting
|
||||
challengeHandler.onOnline();
|
||||
|
||||
reconnectBackOff.reset();
|
||||
} finally {
|
||||
connecting = false;
|
||||
}
|
||||
|
@ -3380,8 +3407,10 @@ export async function startApp(): Promise<void> {
|
|||
) {
|
||||
// Failed to connect to server
|
||||
if (navigator.onLine) {
|
||||
window.log.info('retrying in 1 minute');
|
||||
reconnectTimer = setTimeout(connect, 60000);
|
||||
const timeout = reconnectBackOff.getAndIncrement();
|
||||
|
||||
window.log.info(`retrying in ${timeout}ms`);
|
||||
reconnectTimer = setTimeout(connect, timeout);
|
||||
|
||||
window.Whisper.events.trigger('reconnectTimer');
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import { boolean, select } from '@storybook/addon-knobs';
|
|||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { NetworkStatus } from './NetworkStatus';
|
||||
import { SocketStatus } from '../types/SocketStatus';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
|
@ -16,7 +17,7 @@ const defaultProps = {
|
|||
hasNetworkDialog: true,
|
||||
i18n,
|
||||
isOnline: true,
|
||||
socketStatus: 0,
|
||||
socketStatus: SocketStatus.CONNECTING,
|
||||
manualReconnect: action('manual-reconnect'),
|
||||
withinConnectingGracePeriod: false,
|
||||
challengeStatus: 'idle' as const,
|
||||
|
@ -26,19 +27,19 @@ const permutations = [
|
|||
{
|
||||
title: 'Connecting',
|
||||
props: {
|
||||
socketStatus: 0,
|
||||
socketStatus: SocketStatus.CONNECTING,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Closing (online)',
|
||||
props: {
|
||||
socketStatus: 2,
|
||||
socketStatus: SocketStatus.CLOSING,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Closed (online)',
|
||||
props: {
|
||||
socketStatus: 3,
|
||||
socketStatus: SocketStatus.CLOSED,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -56,12 +57,12 @@ storiesOf('Components/NetworkStatus', module)
|
|||
const socketStatus = select(
|
||||
'socketStatus',
|
||||
{
|
||||
CONNECTING: 0,
|
||||
OPEN: 1,
|
||||
CLOSING: 2,
|
||||
CLOSED: 3,
|
||||
CONNECTING: SocketStatus.CONNECTING,
|
||||
OPEN: SocketStatus.OPEN,
|
||||
CLOSING: SocketStatus.CLOSING,
|
||||
CLOSED: SocketStatus.CLOSED,
|
||||
},
|
||||
0
|
||||
SocketStatus.CONNECTING
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { SocketStatus } from '../types/SocketStatus';
|
||||
import { NetworkStateType } from '../state/ducks/network';
|
||||
|
||||
const FIVE_SECONDS = 5 * 1000;
|
||||
|
@ -100,12 +101,12 @@ export const NetworkStatus = ({
|
|||
let renderActionableButton;
|
||||
|
||||
switch (socketStatus) {
|
||||
case WebSocket.CONNECTING:
|
||||
case SocketStatus.CONNECTING:
|
||||
subtext = i18n('connectingHangOn');
|
||||
title = i18n('connecting');
|
||||
break;
|
||||
case WebSocket.CLOSED:
|
||||
case WebSocket.CLOSING:
|
||||
case SocketStatus.CLOSED:
|
||||
case SocketStatus.CLOSING:
|
||||
default:
|
||||
renderActionableButton = manualReconnectButton;
|
||||
title = i18n('disconnected');
|
||||
|
|
26
ts/main/powerChannel.ts
Normal file
26
ts/main/powerChannel.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { powerMonitor } from 'electron';
|
||||
|
||||
export type InitializeOptions = {
|
||||
send(event: string): void;
|
||||
};
|
||||
|
||||
export class PowerChannel {
|
||||
private static isInitialized = false;
|
||||
|
||||
static initialize({ send }: InitializeOptions): void {
|
||||
if (PowerChannel.isInitialized) {
|
||||
throw new Error('PowerChannel already initialized');
|
||||
}
|
||||
PowerChannel.isInitialized = true;
|
||||
|
||||
powerMonitor.on('suspend', () => {
|
||||
send('power-channel:suspend');
|
||||
});
|
||||
powerMonitor.on('resume', () => {
|
||||
send('power-channel:resume');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from '../util/zkgroup';
|
||||
|
||||
import { GroupCredentialType } from '../textsecure/WebAPI';
|
||||
import { BackOff } from '../util/BackOff';
|
||||
import { sleep } from '../util/sleep';
|
||||
|
||||
export const GROUP_CREDENTIALS_KEY = 'groupCredentials';
|
||||
|
@ -50,33 +51,28 @@ export async function initializeGroupCredentialFetcher(): Promise<void> {
|
|||
await runWithRetry(maybeFetchNewCredentials, { scheduleAnother: 4 * HOUR });
|
||||
}
|
||||
|
||||
type BackoffType = {
|
||||
[key: number]: number | undefined;
|
||||
max: number;
|
||||
};
|
||||
const BACKOFF: BackoffType = {
|
||||
0: SECOND,
|
||||
1: 5 * SECOND,
|
||||
2: 30 * SECOND,
|
||||
3: 2 * MINUTE,
|
||||
max: 5 * MINUTE,
|
||||
};
|
||||
const BACKOFF_TIMEOUTS = [
|
||||
SECOND,
|
||||
5 * SECOND,
|
||||
30 * SECOND,
|
||||
2 * MINUTE,
|
||||
5 * MINUTE,
|
||||
];
|
||||
|
||||
export async function runWithRetry(
|
||||
fn: () => Promise<void>,
|
||||
options: { scheduleAnother?: number } = {}
|
||||
): Promise<void> {
|
||||
let count = 0;
|
||||
const backOff = new BackOff(BACKOFF_TIMEOUTS);
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
try {
|
||||
count += 1;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await fn();
|
||||
return;
|
||||
} catch (error) {
|
||||
const wait = BACKOFF[count] || BACKOFF.max;
|
||||
const wait = backOff.getAndIncrement();
|
||||
window.log.info(
|
||||
`runWithRetry: ${fn.name} failed. Waiting ${wait}ms for retry. Error: ${error.stack}`
|
||||
);
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
toGroupV2Record,
|
||||
} from './storageRecordOps';
|
||||
import { ConversationModel } from '../models/conversations';
|
||||
import { BackOff } from '../util/BackOff';
|
||||
import { storageJobQueue } from '../util/JobQueue';
|
||||
import { sleep } from '../util/sleep';
|
||||
import { isMoreRecentThan } from '../util/timestamp';
|
||||
|
@ -45,8 +46,6 @@ const {
|
|||
updateConversation,
|
||||
} = dataInterface;
|
||||
|
||||
let consecutiveStops = 0;
|
||||
let consecutiveConflicts = 0;
|
||||
const uploadBucket: Array<number> = [];
|
||||
|
||||
const validRecordTypes = new Set([
|
||||
|
@ -57,24 +56,18 @@ const validRecordTypes = new Set([
|
|||
4, // ACCOUNT
|
||||
]);
|
||||
|
||||
type BackoffType = {
|
||||
[key: number]: number | undefined;
|
||||
max: number;
|
||||
};
|
||||
const SECOND = 1000;
|
||||
const MINUTE = 60 * SECOND;
|
||||
const BACKOFF: BackoffType = {
|
||||
0: SECOND,
|
||||
1: 5 * SECOND,
|
||||
2: 30 * SECOND,
|
||||
3: 2 * MINUTE,
|
||||
max: 5 * MINUTE,
|
||||
};
|
||||
|
||||
function backOff(count: number) {
|
||||
const ms = BACKOFF[count] || BACKOFF.max;
|
||||
return sleep(ms);
|
||||
}
|
||||
const backOff = new BackOff([
|
||||
SECOND,
|
||||
5 * SECOND,
|
||||
30 * SECOND,
|
||||
2 * MINUTE,
|
||||
5 * MINUTE,
|
||||
]);
|
||||
|
||||
const conflictBackOff = new BackOff([SECOND, 5 * SECOND, 30 * SECOND]);
|
||||
|
||||
function redactStorageID(storageID: string): string {
|
||||
return storageID.substring(0, 3);
|
||||
|
@ -494,16 +487,15 @@ async function uploadManifest(
|
|||
);
|
||||
|
||||
if (err.code === 409) {
|
||||
if (consecutiveConflicts > 3) {
|
||||
if (conflictBackOff.isFull()) {
|
||||
window.log.error(
|
||||
'storageService.uploadManifest: Exceeded maximum consecutive conflicts'
|
||||
);
|
||||
return;
|
||||
}
|
||||
consecutiveConflicts += 1;
|
||||
|
||||
window.log.info(
|
||||
`storageService.uploadManifest: Conflict found with v${version}, running sync job times(${consecutiveConflicts})`
|
||||
`storageService.uploadManifest: Conflict found with v${version}, running sync job times(${conflictBackOff.getIndex()})`
|
||||
);
|
||||
|
||||
throw err;
|
||||
|
@ -517,8 +509,8 @@ async function uploadManifest(
|
|||
version
|
||||
);
|
||||
window.storage.put('manifestVersion', version);
|
||||
consecutiveConflicts = 0;
|
||||
consecutiveStops = 0;
|
||||
conflictBackOff.reset();
|
||||
backOff.reset();
|
||||
await window.textsecure.messaging.sendFetchManifestSyncMessage();
|
||||
}
|
||||
|
||||
|
@ -527,21 +519,21 @@ async function stopStorageServiceSync() {
|
|||
|
||||
await window.storage.remove('storageKey');
|
||||
|
||||
if (consecutiveStops < 5) {
|
||||
await backOff(consecutiveStops);
|
||||
if (backOff.isFull()) {
|
||||
window.log.info(
|
||||
'storageService.stopStorageServiceSync: requesting new keys'
|
||||
'storageService.stopStorageServiceSync: too many consecutive stops'
|
||||
);
|
||||
consecutiveStops += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
await sleep(backOff.getAndIncrement());
|
||||
window.log.info('storageService.stopStorageServiceSync: requesting new keys');
|
||||
setTimeout(() => {
|
||||
if (!window.textsecure.messaging) {
|
||||
throw new Error(
|
||||
'storageService.stopStorageServiceSync: We are offline!'
|
||||
);
|
||||
throw new Error('storageService.stopStorageServiceSync: We are offline!');
|
||||
}
|
||||
window.textsecure.messaging.sendRequestKeySyncMessage();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function createNewManifest() {
|
||||
|
@ -976,7 +968,7 @@ async function processRemoteRecords(
|
|||
return conflictCount;
|
||||
}
|
||||
|
||||
consecutiveConflicts = 0;
|
||||
conflictBackOff.reset();
|
||||
} catch (err) {
|
||||
window.log.error(
|
||||
'storageService.processRemoteRecords: failed!',
|
||||
|
@ -1082,7 +1074,7 @@ async function upload(fromSync = false): Promise<void> {
|
|||
window.log.info(
|
||||
'storageService.upload: no storageKey, requesting new keys'
|
||||
);
|
||||
consecutiveStops = 0;
|
||||
backOff.reset();
|
||||
await window.textsecure.messaging.sendRequestKeySyncMessage();
|
||||
return;
|
||||
}
|
||||
|
@ -1108,7 +1100,7 @@ async function upload(fromSync = false): Promise<void> {
|
|||
await uploadManifest(version, generatedManifest);
|
||||
} catch (err) {
|
||||
if (err.code === 409) {
|
||||
await backOff(consecutiveConflicts);
|
||||
await sleep(conflictBackOff.getAndIncrement());
|
||||
window.log.info('storageService.upload: pushing sync on the queue');
|
||||
// The sync job will check for conflicts and as part of that conflict
|
||||
// check if an item needs sync and doesn't match with the remote record
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export function getSocketStatus(): number {
|
||||
import { SocketStatus } from '../types/SocketStatus';
|
||||
|
||||
export function getSocketStatus(): SocketStatus {
|
||||
const { getSocketStatus: getMessageReceiverStatus } = window;
|
||||
|
||||
return getMessageReceiverStatus();
|
||||
|
|
|
@ -98,7 +98,7 @@ export const actions = {
|
|||
export function getEmptyState(): NetworkStateType {
|
||||
return {
|
||||
isOnline: navigator.onLine,
|
||||
socketStatus: WebSocket.OPEN,
|
||||
socketStatus: SocketStatus.OPEN,
|
||||
withinConnectingGracePeriod: true,
|
||||
challengeStatus: 'idle',
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@ import { createSelector } from 'reselect';
|
|||
import { StateType } from '../reducer';
|
||||
import { NetworkStateType } from '../ducks/network';
|
||||
import { isDone } from '../../util/registration';
|
||||
import { SocketStatus } from '../../types/SocketStatus';
|
||||
|
||||
const getNetwork = (state: StateType): NetworkStateType => state.network;
|
||||
|
||||
|
@ -18,9 +19,10 @@ export const hasNetworkDialog = createSelector(
|
|||
): boolean =>
|
||||
isRegistrationDone &&
|
||||
(!isOnline ||
|
||||
(socketStatus === WebSocket.CONNECTING && !withinConnectingGracePeriod) ||
|
||||
socketStatus === WebSocket.CLOSED ||
|
||||
socketStatus === WebSocket.CLOSING)
|
||||
(socketStatus === SocketStatus.CONNECTING &&
|
||||
!withinConnectingGracePeriod) ||
|
||||
socketStatus === SocketStatus.CLOSED ||
|
||||
socketStatus === SocketStatus.CLOSING)
|
||||
);
|
||||
|
||||
export const isChallengePending = createSelector(
|
||||
|
|
45
ts/test-both/util/BackOff_test.ts
Normal file
45
ts/test-both/util/BackOff_test.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { BackOff } from '../../util/BackOff';
|
||||
|
||||
describe('BackOff', () => {
|
||||
it('should return increasing timeouts', () => {
|
||||
const b = new BackOff([1, 2, 3]);
|
||||
|
||||
assert.strictEqual(b.getIndex(), 0);
|
||||
assert.strictEqual(b.isFull(), false);
|
||||
|
||||
assert.strictEqual(b.get(), 1);
|
||||
assert.strictEqual(b.getAndIncrement(), 1);
|
||||
assert.strictEqual(b.get(), 2);
|
||||
assert.strictEqual(b.getIndex(), 1);
|
||||
assert.strictEqual(b.isFull(), false);
|
||||
|
||||
assert.strictEqual(b.getAndIncrement(), 2);
|
||||
assert.strictEqual(b.getIndex(), 2);
|
||||
assert.strictEqual(b.isFull(), true);
|
||||
|
||||
assert.strictEqual(b.getAndIncrement(), 3);
|
||||
assert.strictEqual(b.getIndex(), 2);
|
||||
assert.strictEqual(b.isFull(), true);
|
||||
|
||||
assert.strictEqual(b.getAndIncrement(), 3);
|
||||
assert.strictEqual(b.getIndex(), 2);
|
||||
assert.strictEqual(b.isFull(), true);
|
||||
});
|
||||
|
||||
it('should reset', () => {
|
||||
const b = new BackOff([1, 2, 3]);
|
||||
|
||||
assert.strictEqual(b.getAndIncrement(), 1);
|
||||
assert.strictEqual(b.getAndIncrement(), 2);
|
||||
|
||||
b.reset();
|
||||
|
||||
assert.strictEqual(b.getAndIncrement(), 1);
|
||||
assert.strictEqual(b.getAndIncrement(), 2);
|
||||
});
|
||||
});
|
264
ts/test-electron/WebsocketResources_test.ts
Normal file
264
ts/test-electron/WebsocketResources_test.ts
Normal file
|
@ -0,0 +1,264 @@
|
|||
// Copyright 2015-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
/* eslint-disable
|
||||
class-methods-use-this,
|
||||
no-new,
|
||||
@typescript-eslint/no-empty-function,
|
||||
@typescript-eslint/no-explicit-any
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import EventEmitter from 'events';
|
||||
import { connection as WebSocket } from 'websocket';
|
||||
|
||||
import WebSocketResource from '../textsecure/WebsocketResources';
|
||||
|
||||
describe('WebSocket-Resource', () => {
|
||||
class FakeSocket extends EventEmitter {
|
||||
public sendBytes(_: Uint8Array) {}
|
||||
|
||||
public close() {}
|
||||
}
|
||||
|
||||
describe('requests and responses', () => {
|
||||
it('receives requests and sends responses', done => {
|
||||
// mock socket
|
||||
const requestId = '1';
|
||||
const socket = new FakeSocket();
|
||||
|
||||
sinon.stub(socket, 'sendBytes').callsFake((data: Uint8Array) => {
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
data
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE
|
||||
);
|
||||
assert.strictEqual(message.response?.message, 'OK');
|
||||
assert.strictEqual(message.response?.status, 200);
|
||||
assert.strictEqual(message.response?.id.toString(), requestId);
|
||||
done();
|
||||
});
|
||||
|
||||
// actual test
|
||||
new WebSocketResource(socket as WebSocket, {
|
||||
handleRequest(request: any) {
|
||||
assert.strictEqual(request.verb, 'PUT');
|
||||
assert.strictEqual(request.path, '/some/path');
|
||||
assert.ok(
|
||||
window.Signal.Crypto.constantTimeEqual(
|
||||
request.body.toArrayBuffer(),
|
||||
window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([1, 2, 3])
|
||||
)
|
||||
)
|
||||
);
|
||||
request.respond(200, 'OK');
|
||||
},
|
||||
});
|
||||
|
||||
// mock socket request
|
||||
socket.emit('message', {
|
||||
type: 'binary',
|
||||
binaryData: new Uint8Array(
|
||||
new window.textsecure.protobuf.WebSocketMessage({
|
||||
type: window.textsecure.protobuf.WebSocketMessage.Type.REQUEST,
|
||||
request: {
|
||||
id: requestId,
|
||||
verb: 'PUT',
|
||||
path: '/some/path',
|
||||
body: window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([1, 2, 3])
|
||||
),
|
||||
},
|
||||
})
|
||||
.encode()
|
||||
.toArrayBuffer()
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it('sends requests and receives responses', done => {
|
||||
// mock socket and request handler
|
||||
let requestId: Long | undefined;
|
||||
const socket = new FakeSocket();
|
||||
|
||||
sinon.stub(socket, 'sendBytes').callsFake((data: Uint8Array) => {
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
data
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
assert.strictEqual(message.request?.verb, 'PUT');
|
||||
assert.strictEqual(message.request?.path, '/some/path');
|
||||
assert.ok(
|
||||
window.Signal.Crypto.constantTimeEqual(
|
||||
message.request?.body.toArrayBuffer(),
|
||||
window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([1, 2, 3])
|
||||
)
|
||||
)
|
||||
);
|
||||
requestId = message.request?.id;
|
||||
});
|
||||
|
||||
// actual test
|
||||
const resource = new WebSocketResource(socket as WebSocket);
|
||||
resource.sendRequest({
|
||||
verb: 'PUT',
|
||||
path: '/some/path',
|
||||
body: window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([1, 2, 3])
|
||||
),
|
||||
error: done,
|
||||
success(message: string, status: number) {
|
||||
assert.strictEqual(message, 'OK');
|
||||
assert.strictEqual(status, 200);
|
||||
done();
|
||||
},
|
||||
});
|
||||
|
||||
// mock socket response
|
||||
socket.emit('message', {
|
||||
type: 'binary',
|
||||
binaryData: new Uint8Array(
|
||||
new window.textsecure.protobuf.WebSocketMessage({
|
||||
type: window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
|
||||
response: { id: requestId, message: 'OK', status: 200 },
|
||||
})
|
||||
.encode()
|
||||
.toArrayBuffer()
|
||||
),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('close', () => {
|
||||
it('closes the connection', done => {
|
||||
const socket = new FakeSocket();
|
||||
|
||||
sinon.stub(socket, 'close').callsFake(() => done());
|
||||
|
||||
const resource = new WebSocketResource(socket as WebSocket);
|
||||
resource.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a keepalive config', () => {
|
||||
const NOW = Date.now();
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
this.sandbox = sinon.createSandbox();
|
||||
this.clock = this.sandbox.useFakeTimers({
|
||||
now: NOW,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function afterEach() {
|
||||
this.sandbox.restore();
|
||||
});
|
||||
|
||||
it('sends keepalives once a minute', function test(done) {
|
||||
const socket = new FakeSocket();
|
||||
|
||||
sinon.stub(socket, 'sendBytes').callsFake(data => {
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
data
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
assert.strictEqual(message.request?.verb, 'GET');
|
||||
assert.strictEqual(message.request?.path, '/v1/keepalive');
|
||||
done();
|
||||
});
|
||||
|
||||
new WebSocketResource(socket as WebSocket, {
|
||||
keepalive: { path: '/v1/keepalive' },
|
||||
});
|
||||
|
||||
this.clock.next();
|
||||
});
|
||||
|
||||
it('uses / as a default path', function test(done) {
|
||||
const socket = new FakeSocket();
|
||||
|
||||
sinon.stub(socket, 'sendBytes').callsFake(data => {
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
data
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
assert.strictEqual(message.request?.verb, 'GET');
|
||||
assert.strictEqual(message.request?.path, '/');
|
||||
done();
|
||||
});
|
||||
|
||||
new WebSocketResource(socket as WebSocket, {
|
||||
keepalive: true,
|
||||
});
|
||||
|
||||
this.clock.next();
|
||||
});
|
||||
|
||||
it('optionally disconnects if no response', function thisNeeded1(done) {
|
||||
const socket = new FakeSocket();
|
||||
|
||||
sinon.stub(socket, 'close').callsFake(() => done());
|
||||
|
||||
new WebSocketResource(socket as WebSocket, {
|
||||
keepalive: true,
|
||||
});
|
||||
|
||||
// One to trigger send
|
||||
this.clock.next();
|
||||
|
||||
// Another to trigger send timeout
|
||||
this.clock.next();
|
||||
});
|
||||
|
||||
it('allows resetting the keepalive timer', function thisNeeded2(done) {
|
||||
const startTime = Date.now();
|
||||
|
||||
const socket = new FakeSocket();
|
||||
|
||||
sinon.stub(socket, 'sendBytes').callsFake(data => {
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
data
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
assert.strictEqual(message.request?.verb, 'GET');
|
||||
assert.strictEqual(message.request?.path, '/');
|
||||
assert.strictEqual(
|
||||
Date.now(),
|
||||
startTime + 60000,
|
||||
'keepalive time should be one minute'
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
const resource = new WebSocketResource(socket as WebSocket, {
|
||||
keepalive: true,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
resource.keepalive?.reset();
|
||||
}, 5000);
|
||||
|
||||
// Trigger setTimeout above
|
||||
this.clock.next();
|
||||
|
||||
// Trigger sendBytes
|
||||
this.clock.next();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -199,19 +199,22 @@ export default class AccountManager extends EventTarget {
|
|||
const queueTask = this.queueTask.bind(this);
|
||||
const provisioningCipher = new ProvisioningCipher();
|
||||
let gotProvisionEnvelope = false;
|
||||
return provisioningCipher.getPublicKey().then(
|
||||
async (pubKey: ArrayBuffer) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const socket = getSocket();
|
||||
socket.onclose = event => {
|
||||
window.log.info('provisioning socket closed. Code:', event.code);
|
||||
const pubKey = await provisioningCipher.getPublicKey();
|
||||
|
||||
const socket = await getSocket();
|
||||
|
||||
window.log.info('provisioning socket open');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
socket.on('close', (code, reason) => {
|
||||
window.log.info(
|
||||
`provisioning socket closed. Code: ${code} Reason: ${reason}`
|
||||
);
|
||||
if (!gotProvisionEnvelope) {
|
||||
reject(new Error('websocket closed'));
|
||||
}
|
||||
};
|
||||
socket.onopen = () => {
|
||||
window.log.info('provisioning socket open');
|
||||
};
|
||||
});
|
||||
|
||||
const wsr = new WebSocketResource(socket, {
|
||||
keepalive: { path: '/v1/keepalive/provisioning' },
|
||||
handleRequest(request: IncomingWebSocketRequest) {
|
||||
|
@ -300,8 +303,7 @@ export default class AccountManager extends EventTarget {
|
|||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async refreshPreKeys() {
|
||||
|
|
|
@ -10,9 +10,10 @@
|
|||
/* eslint-disable max-classes-per-file */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
|
||||
import { isNumber, map, omit, noop } from 'lodash';
|
||||
import { isNumber, map, omit } from 'lodash';
|
||||
import PQueue from 'p-queue';
|
||||
import { v4 as getGuid } from 'uuid';
|
||||
import { connection as WebSocket } from 'websocket';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
|
@ -41,6 +42,7 @@ import {
|
|||
SignedPreKeys,
|
||||
} from '../LibSignalStores';
|
||||
import { BatcherType, createBatcher } from '../util/batcher';
|
||||
import { sleep } from '../util/sleep';
|
||||
import { parseIntOrThrow } from '../util/parseIntOrThrow';
|
||||
import { Zone } from '../util/Zone';
|
||||
import EventTarget from './EventTarget';
|
||||
|
@ -53,6 +55,7 @@ import Crypto from './Crypto';
|
|||
import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto';
|
||||
import { ContactBuffer, GroupBuffer } from './ContactsParser';
|
||||
import { isByteBufferEmpty } from '../util/isByteBufferEmpty';
|
||||
import { SocketStatus } from '../types/SocketStatus';
|
||||
|
||||
import {
|
||||
AttachmentPointerClass,
|
||||
|
@ -68,13 +71,12 @@ import {
|
|||
} from '../textsecure.d';
|
||||
import { ByteBufferClass } from '../window.d';
|
||||
|
||||
import { WebSocket } from './WebSocket';
|
||||
|
||||
import { deriveGroupFields, MASTER_KEY_LENGTH } from '../groups';
|
||||
|
||||
const GROUPV1_ID_LENGTH = 16;
|
||||
const GROUPV2_ID_LENGTH = 32;
|
||||
const RETRY_TIMEOUT = 2 * 60 * 1000;
|
||||
const RECONNECT_DELAY = 1 * 1000;
|
||||
|
||||
const decryptionErrorTypeSchema = z
|
||||
.object({
|
||||
|
@ -169,7 +171,9 @@ enum TaskType {
|
|||
}
|
||||
|
||||
class MessageReceiverInner extends EventTarget {
|
||||
_onClose?: (ev: any) => Promise<void>;
|
||||
_onClose?: (code: number, reason: string) => Promise<void>;
|
||||
|
||||
_onError?: (error: Error) => Promise<void>;
|
||||
|
||||
appQueue: PQueue;
|
||||
|
||||
|
@ -185,7 +189,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
|
||||
deviceId?: number;
|
||||
|
||||
hasConnected?: boolean;
|
||||
hasConnected = false;
|
||||
|
||||
incomingQueue: PQueue;
|
||||
|
||||
|
@ -209,6 +213,8 @@ class MessageReceiverInner extends EventTarget {
|
|||
|
||||
socket?: WebSocket;
|
||||
|
||||
socketStatus = SocketStatus.CLOSED;
|
||||
|
||||
stoppingProcessing?: boolean;
|
||||
|
||||
username: string;
|
||||
|
@ -304,7 +310,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
static arrayBufferToStringBase64 = (arrayBuffer: ArrayBuffer): string =>
|
||||
window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
|
||||
|
||||
connect() {
|
||||
async connect(): Promise<void> {
|
||||
if (this.calledClose) {
|
||||
return;
|
||||
}
|
||||
|
@ -322,17 +328,43 @@ class MessageReceiverInner extends EventTarget {
|
|||
|
||||
this.hasConnected = true;
|
||||
|
||||
if (this.socket && this.socket.readyState !== WebSocket.CLOSED) {
|
||||
if (this.socket && this.socket.connected) {
|
||||
this.socket.close();
|
||||
this.socket = undefined;
|
||||
if (this.wsr) {
|
||||
this.wsr.close();
|
||||
this.wsr = undefined;
|
||||
}
|
||||
}
|
||||
this.socketStatus = SocketStatus.CONNECTING;
|
||||
|
||||
// initialize the socket and start listening for messages
|
||||
this.socket = this.server.getMessageSocket();
|
||||
this.socket.onclose = this.onclose.bind(this);
|
||||
this.socket.onerror = this.onerror.bind(this);
|
||||
this.socket.onopen = this.onopen.bind(this);
|
||||
try {
|
||||
this.socket = await this.server.getMessageSocket();
|
||||
} catch (error) {
|
||||
this.socketStatus = SocketStatus.CLOSED;
|
||||
|
||||
const event = new Event('error');
|
||||
event.error = error;
|
||||
await this.dispatchAndWait(event);
|
||||
return;
|
||||
}
|
||||
|
||||
this.socketStatus = SocketStatus.OPEN;
|
||||
|
||||
window.log.info('websocket open');
|
||||
window.logMessageReceiverConnect();
|
||||
|
||||
if (!this._onClose) {
|
||||
this._onClose = this.onclose.bind(this);
|
||||
}
|
||||
if (!this._onError) {
|
||||
this._onError = this.onerror.bind(this);
|
||||
}
|
||||
|
||||
this.socket.on('close', this._onClose);
|
||||
this.socket.on('error', this._onError);
|
||||
|
||||
this.wsr = new WebSocketResource(this.socket, {
|
||||
handleRequest: this.handleRequest.bind(this),
|
||||
keepalive: {
|
||||
|
@ -342,7 +374,6 @@ class MessageReceiverInner extends EventTarget {
|
|||
});
|
||||
|
||||
// Because sometimes the socket doesn't properly emit its close event
|
||||
this._onClose = this.onclose.bind(this);
|
||||
if (this._onClose) {
|
||||
this.wsr.addEventListener('close', this._onClose);
|
||||
}
|
||||
|
@ -362,9 +393,12 @@ class MessageReceiverInner extends EventTarget {
|
|||
|
||||
shutdown() {
|
||||
if (this.socket) {
|
||||
this.socket.onclose = noop;
|
||||
this.socket.onerror = noop;
|
||||
this.socket.onopen = noop;
|
||||
if (this._onClose) {
|
||||
this.socket.removeListener('close', this._onClose);
|
||||
}
|
||||
if (this._onError) {
|
||||
this.socket.removeListener('error', this._onError);
|
||||
}
|
||||
|
||||
this.socket = undefined;
|
||||
}
|
||||
|
@ -380,6 +414,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
async close() {
|
||||
window.log.info('MessageReceiver.close()');
|
||||
this.calledClose = true;
|
||||
this.socketStatus = SocketStatus.CLOSING;
|
||||
|
||||
// Our WebSocketResource instance will close the socket and emit a 'close' event
|
||||
// if the socket doesn't emit one quickly enough.
|
||||
|
@ -392,13 +427,8 @@ class MessageReceiverInner extends EventTarget {
|
|||
return this.drain();
|
||||
}
|
||||
|
||||
onopen() {
|
||||
window.log.info('websocket open');
|
||||
window.logMessageReceiverConnect();
|
||||
}
|
||||
|
||||
onerror() {
|
||||
window.log.error('websocket error');
|
||||
async onerror(error: Error): Promise<void> {
|
||||
window.log.error('websocket error', error);
|
||||
}
|
||||
|
||||
async dispatchAndWait(event: Event) {
|
||||
|
@ -407,35 +437,41 @@ class MessageReceiverInner extends EventTarget {
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async onclose(ev: any) {
|
||||
async onclose(code: number, reason: string): Promise<void> {
|
||||
window.log.info(
|
||||
'websocket closed',
|
||||
ev.code,
|
||||
ev.reason || '',
|
||||
code,
|
||||
reason || '',
|
||||
'calledClose:',
|
||||
this.calledClose
|
||||
);
|
||||
|
||||
this.socketStatus = SocketStatus.CLOSED;
|
||||
|
||||
this.shutdown();
|
||||
|
||||
if (this.calledClose) {
|
||||
return Promise.resolve();
|
||||
return;
|
||||
}
|
||||
if (ev.code === 3000) {
|
||||
return Promise.resolve();
|
||||
if (code === 3000) {
|
||||
return;
|
||||
}
|
||||
if (ev.code === 3001) {
|
||||
if (code === 3001) {
|
||||
this.onEmpty();
|
||||
}
|
||||
// possible 403 or network issue. Make an request to confirm
|
||||
return this.server
|
||||
.getDevices()
|
||||
.then(this.connect.bind(this)) // No HTTP error? Reconnect
|
||||
.catch(async e => {
|
||||
const event = new Event('error');
|
||||
event.error = e;
|
||||
return this.dispatchAndWait(event);
|
||||
});
|
||||
|
||||
await sleep(RECONNECT_DELAY);
|
||||
|
||||
// Try to reconnect (if there is an error - we'll get an
|
||||
// `error` event from `connect()` and hit the retry backoff logic in
|
||||
// `ts/background.ts`)
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
checkSocket(): void {
|
||||
if (this.wsr) {
|
||||
this.wsr.forceKeepAlive();
|
||||
}
|
||||
}
|
||||
|
||||
handleRequest(request: IncomingWebSocketRequest) {
|
||||
|
@ -1076,14 +1112,8 @@ class MessageReceiverInner extends EventTarget {
|
|||
throw new Error('Received message with no content and no legacyMessage');
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
if (this.socket) {
|
||||
return this.socket.readyState;
|
||||
}
|
||||
if (this.hasConnected) {
|
||||
return WebSocket.CLOSED;
|
||||
}
|
||||
return -1;
|
||||
getStatus(): SocketStatus {
|
||||
return this.socketStatus;
|
||||
}
|
||||
|
||||
async onDeliveryReceipt(envelope: EnvelopeClass): Promise<void> {
|
||||
|
@ -2693,6 +2723,7 @@ export default class MessageReceiver {
|
|||
this.hasEmptied = inner.hasEmptied.bind(inner);
|
||||
this.removeEventListener = inner.removeEventListener.bind(inner);
|
||||
this.stopProcessing = inner.stopProcessing.bind(inner);
|
||||
this.checkSocket = inner.checkSocket.bind(inner);
|
||||
this.unregisterBatchers = inner.unregisterBatchers.bind(inner);
|
||||
|
||||
inner.connect();
|
||||
|
@ -2707,7 +2738,7 @@ export default class MessageReceiver {
|
|||
attachment: AttachmentPointerClass
|
||||
) => Promise<DownloadAttachmentType>;
|
||||
|
||||
getStatus: () => number;
|
||||
getStatus: () => SocketStatus;
|
||||
|
||||
hasEmptied: () => boolean;
|
||||
|
||||
|
@ -2717,6 +2748,8 @@ export default class MessageReceiver {
|
|||
|
||||
unregisterBatchers: () => void;
|
||||
|
||||
checkSocket: () => void;
|
||||
|
||||
getProcessedCount: () => number;
|
||||
|
||||
static stringToArrayBuffer = MessageReceiverInner.stringToArrayBuffer;
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
import fetch, { Response } from 'node-fetch';
|
||||
import ProxyAgent from 'proxy-agent';
|
||||
import { Agent } from 'https';
|
||||
import { Agent, RequestOptions } from 'https';
|
||||
import pProps from 'p-props';
|
||||
import {
|
||||
compact,
|
||||
|
@ -25,9 +25,11 @@ import { pki } from 'node-forge';
|
|||
import is from '@sindresorhus/is';
|
||||
import PQueue from 'p-queue';
|
||||
import { v4 as getGuid } from 'uuid';
|
||||
import { client as WebSocketClient, connection as WebSocket } from 'websocket';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Long } from '../window.d';
|
||||
import { assert } from '../util/assert';
|
||||
import { getUserAgent } from '../util/getUserAgent';
|
||||
import { toWebSafeBase64 } from '../util/webSafeBase64';
|
||||
import { isPackIdValid, redactPackId } from '../../js/modules/stickers';
|
||||
|
@ -59,7 +61,6 @@ import {
|
|||
StorageServiceCredentials,
|
||||
} from '../textsecure.d';
|
||||
|
||||
import { WebSocket } from './WebSocket';
|
||||
import MessageSender from './SendMessage';
|
||||
|
||||
// Note: this will break some code that expects to be able to use err.response when a
|
||||
|
@ -261,31 +262,85 @@ function _validateResponse(response: any, schema: any) {
|
|||
return true;
|
||||
}
|
||||
|
||||
function _createSocket(
|
||||
export type ConnectSocketOptions = Readonly<{
|
||||
certificateAuthority: string;
|
||||
proxyUrl?: string;
|
||||
version: string;
|
||||
timeout?: number;
|
||||
}>;
|
||||
|
||||
const TEN_SECONDS = 1000 * 10;
|
||||
|
||||
async function _connectSocket(
|
||||
url: string,
|
||||
{
|
||||
certificateAuthority,
|
||||
proxyUrl,
|
||||
version,
|
||||
}: { certificateAuthority: string; proxyUrl?: string; version: string }
|
||||
) {
|
||||
let requestOptions;
|
||||
if (proxyUrl) {
|
||||
requestOptions = {
|
||||
timeout = TEN_SECONDS,
|
||||
}: ConnectSocketOptions
|
||||
): Promise<WebSocket> {
|
||||
let tlsOptions: RequestOptions = {
|
||||
ca: certificateAuthority,
|
||||
};
|
||||
if (proxyUrl) {
|
||||
tlsOptions = {
|
||||
...tlsOptions,
|
||||
agent: new ProxyAgent(proxyUrl),
|
||||
};
|
||||
} else {
|
||||
requestOptions = {
|
||||
ca: certificateAuthority,
|
||||
};
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'User-Agent': getUserAgent(version),
|
||||
};
|
||||
return new WebSocket(url, undefined, undefined, headers, requestOptions, {
|
||||
const client = new WebSocketClient({
|
||||
tlsOptions,
|
||||
maxReceivedFrameSize: 0x210000,
|
||||
});
|
||||
|
||||
client.connect(url, undefined, undefined, headers);
|
||||
|
||||
const { stack } = new Error();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error('Connection timed out'));
|
||||
|
||||
client.abort();
|
||||
}, timeout);
|
||||
|
||||
client.on('connect', socket => {
|
||||
clearTimeout(timer);
|
||||
resolve(socket);
|
||||
});
|
||||
|
||||
client.on('httpResponse', async response => {
|
||||
clearTimeout(timer);
|
||||
|
||||
const statusCode = response.statusCode || -1;
|
||||
await _handleStatusCode(statusCode);
|
||||
|
||||
const error = makeHTTPError(
|
||||
'promiseAjax: invalid websocket response',
|
||||
statusCode || -1,
|
||||
{}, // headers
|
||||
undefined,
|
||||
stack
|
||||
);
|
||||
|
||||
const translatedError = _translateError(error);
|
||||
assert(
|
||||
translatedError,
|
||||
'`httpResponse` event cannot be emitted with 200 status code'
|
||||
);
|
||||
|
||||
reject(translatedError);
|
||||
});
|
||||
client.on('connectFailed', error => {
|
||||
clearTimeout(timer);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const FIVE_MINUTES = 1000 * 60 * 5;
|
||||
|
@ -403,6 +458,56 @@ function getHostname(url: string): string {
|
|||
return urlObject.hostname;
|
||||
}
|
||||
|
||||
async function _handleStatusCode(
|
||||
status: number,
|
||||
unauthenticated = false
|
||||
): Promise<void> {
|
||||
if (status === 499) {
|
||||
window.log.error('Got 499 from Signal Server. Build is expired.');
|
||||
await window.storage.put('remoteBuildExpiration', Date.now());
|
||||
window.reduxActions.expiration.hydrateExpirationStatus(true);
|
||||
}
|
||||
if (!unauthenticated && status === 401) {
|
||||
window.log.error('Got 401 from Signal Server. We might be unlinked.');
|
||||
window.Whisper.events.trigger('mightBeUnlinked');
|
||||
}
|
||||
}
|
||||
|
||||
function _translateError(error: Error): Error | undefined {
|
||||
const { code } = error;
|
||||
if (code === 200) {
|
||||
// Happens sometimes when we get no response. Might be nice to get 204 instead.
|
||||
return undefined;
|
||||
}
|
||||
let message: string;
|
||||
switch (code) {
|
||||
case -1:
|
||||
message =
|
||||
'Failed to connect to the server, please check your network connection.';
|
||||
break;
|
||||
case 413:
|
||||
message = 'Rate limit exceeded, please try again later.';
|
||||
break;
|
||||
case 403:
|
||||
message = 'Invalid code, please try again.';
|
||||
break;
|
||||
case 417:
|
||||
message = 'Number already registered.';
|
||||
break;
|
||||
case 401:
|
||||
message =
|
||||
'Invalid authentication, most likely someone re-registered and invalidated our registration.';
|
||||
break;
|
||||
case 404:
|
||||
message = 'Number is not registered.';
|
||||
break;
|
||||
default:
|
||||
message = 'The server rejected our query, please file a bug report.';
|
||||
}
|
||||
error.message = `${message} (original: ${error.message})`;
|
||||
return error;
|
||||
}
|
||||
|
||||
async function _promiseAjax(
|
||||
providedUrl: string | null,
|
||||
options: PromiseAjaxOptionsType
|
||||
|
@ -487,25 +592,11 @@ async function _promiseAjax(
|
|||
|
||||
fetch(url, fetchOptions)
|
||||
.then(async response => {
|
||||
if (options.serverUrl) {
|
||||
if (
|
||||
response.status === 499 &&
|
||||
options.serverUrl &&
|
||||
getHostname(options.serverUrl) === getHostname(url)
|
||||
) {
|
||||
window.log.error('Got 499 from Signal Server. Build is expired.');
|
||||
await window.storage.put('remoteBuildExpiration', Date.now());
|
||||
window.reduxActions.expiration.hydrateExpirationStatus(true);
|
||||
}
|
||||
if (
|
||||
!unauthenticated &&
|
||||
response.status === 401 &&
|
||||
getHostname(options.serverUrl) === getHostname(url)
|
||||
) {
|
||||
window.log.error(
|
||||
'Got 401 from Signal Server. We might be unlinked.'
|
||||
);
|
||||
window.Whisper.events.trigger('mightBeUnlinked');
|
||||
}
|
||||
await _handleStatusCode(response.status, unauthenticated);
|
||||
}
|
||||
|
||||
let resultPromise;
|
||||
|
@ -863,7 +954,7 @@ export type WebAPIType = {
|
|||
deviceId?: number,
|
||||
options?: { accessKey?: string }
|
||||
) => Promise<ServerKeysType>;
|
||||
getMessageSocket: () => WebSocket;
|
||||
getMessageSocket: () => Promise<WebSocket>;
|
||||
getMyKeys: () => Promise<number>;
|
||||
getProfile: (
|
||||
identifier: string,
|
||||
|
@ -880,7 +971,7 @@ export type WebAPIType = {
|
|||
profileKeyCredentialRequest?: string;
|
||||
}
|
||||
) => Promise<any>;
|
||||
getProvisioningSocket: () => WebSocket;
|
||||
getProvisioningSocket: () => Promise<WebSocket>;
|
||||
getSenderCertificate: (
|
||||
withUuid?: boolean
|
||||
) => Promise<{ certificate: string }>;
|
||||
|
@ -1153,39 +1244,10 @@ export function initialize({
|
|||
unauthenticated: param.unauthenticated,
|
||||
accessKey: param.accessKey,
|
||||
}).catch((e: Error) => {
|
||||
const { code } = e;
|
||||
if (code === 200) {
|
||||
// Happens sometimes when we get no response. Might be nice to get 204 instead.
|
||||
return null;
|
||||
const translatedError = _translateError(e);
|
||||
if (translatedError) {
|
||||
throw translatedError;
|
||||
}
|
||||
let message: string;
|
||||
switch (code) {
|
||||
case -1:
|
||||
message =
|
||||
'Failed to connect to the server, please check your network connection.';
|
||||
break;
|
||||
case 413:
|
||||
message = 'Rate limit exceeded, please try again later.';
|
||||
break;
|
||||
case 403:
|
||||
message = 'Invalid code, please try again.';
|
||||
break;
|
||||
case 417:
|
||||
message = 'Number already registered.';
|
||||
break;
|
||||
case 401:
|
||||
message =
|
||||
'Invalid authentication, most likely someone re-registered and invalidated our registration.';
|
||||
break;
|
||||
case 404:
|
||||
message = 'Number is not registered.';
|
||||
break;
|
||||
default:
|
||||
message =
|
||||
'The server rejected our query, please file a bug report.';
|
||||
}
|
||||
e.message = `${message} (original: ${e.message})`;
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2318,7 +2380,7 @@ export function initialize({
|
|||
};
|
||||
}
|
||||
|
||||
function getMessageSocket() {
|
||||
function getMessageSocket(): Promise<WebSocket> {
|
||||
window.log.info('opening message socket', url);
|
||||
const fixedScheme = url
|
||||
.replace('https://', 'wss://')
|
||||
|
@ -2327,20 +2389,20 @@ export function initialize({
|
|||
const pass = encodeURIComponent(password);
|
||||
const clientVersion = encodeURIComponent(version);
|
||||
|
||||
return _createSocket(
|
||||
return _connectSocket(
|
||||
`${fixedScheme}/v1/websocket/?login=${login}&password=${pass}&agent=OWD&version=${clientVersion}`,
|
||||
{ certificateAuthority, proxyUrl, version }
|
||||
);
|
||||
}
|
||||
|
||||
function getProvisioningSocket() {
|
||||
function getProvisioningSocket(): Promise<WebSocket> {
|
||||
window.log.info('opening provisioning socket', url);
|
||||
const fixedScheme = url
|
||||
.replace('https://', 'wss://')
|
||||
.replace('http://', 'ws://');
|
||||
const clientVersion = encodeURIComponent(version);
|
||||
|
||||
return _createSocket(
|
||||
return _connectSocket(
|
||||
`${fixedScheme}/v1/websocket/provisioning/?agent=OWD&version=${clientVersion}`,
|
||||
{ certificateAuthority, proxyUrl, version }
|
||||
);
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { w3cwebsocket } from 'websocket';
|
||||
|
||||
type ModifiedEventSource = Omit<EventSource, 'onerror'>;
|
||||
|
||||
declare class ModifiedWebSocket
|
||||
extends w3cwebsocket
|
||||
implements ModifiedEventSource {
|
||||
withCredentials: boolean;
|
||||
|
||||
addEventListener: EventSource['addEventListener'];
|
||||
|
||||
removeEventListener: EventSource['removeEventListener'];
|
||||
|
||||
dispatchEvent: EventSource['dispatchEvent'];
|
||||
}
|
||||
|
||||
export type WebSocket = ModifiedWebSocket;
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const WebSocket = w3cwebsocket as typeof ModifiedWebSocket;
|
|
@ -27,12 +27,12 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import { connection as WebSocket, IMessage } from 'websocket';
|
||||
|
||||
import { ByteBufferClass } from '../window.d';
|
||||
|
||||
import EventTarget from './EventTarget';
|
||||
|
||||
import { WebSocket } from './WebSocket';
|
||||
|
||||
class Request {
|
||||
verb: string;
|
||||
|
||||
|
@ -92,14 +92,13 @@ export class IncomingWebSocketRequest {
|
|||
this.headers = request.headers;
|
||||
|
||||
this.respond = (status, message) => {
|
||||
socket.send(
|
||||
new window.textsecure.protobuf.WebSocketMessage({
|
||||
const ab = new window.textsecure.protobuf.WebSocketMessage({
|
||||
type: window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
|
||||
response: { id: request.id, message, status },
|
||||
})
|
||||
.encode()
|
||||
.toArrayBuffer()
|
||||
);
|
||||
.toArrayBuffer();
|
||||
socket.sendBytes(Buffer.from(ab));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -111,8 +110,7 @@ class OutgoingWebSocketRequest {
|
|||
constructor(options: any, socket: WebSocket) {
|
||||
const request = new Request(options);
|
||||
outgoing[request.id] = request;
|
||||
socket.send(
|
||||
new window.textsecure.protobuf.WebSocketMessage({
|
||||
const ab = new window.textsecure.protobuf.WebSocketMessage({
|
||||
type: window.textsecure.protobuf.WebSocketMessage.Type.REQUEST,
|
||||
request: {
|
||||
verb: request.verb,
|
||||
|
@ -123,8 +121,8 @@ class OutgoingWebSocketRequest {
|
|||
},
|
||||
})
|
||||
.encode()
|
||||
.toArrayBuffer()
|
||||
);
|
||||
.toArrayBuffer();
|
||||
socket.sendBytes(Buffer.from(ab));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -149,11 +147,13 @@ export default class WebSocketResource extends EventTarget {
|
|||
this.sendRequest = options => new OutgoingWebSocketRequest(options, socket);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
socket.onmessage = socketMessage => {
|
||||
const blob = socketMessage.data;
|
||||
const handleArrayBuffer = (buffer: ArrayBuffer) => {
|
||||
const onMessage = ({ type, binaryData }: IMessage): void => {
|
||||
if (type !== 'binary' || !binaryData) {
|
||||
throw new Error(`Unsupported websocket message type: ${type}`);
|
||||
}
|
||||
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
buffer
|
||||
binaryData
|
||||
);
|
||||
if (
|
||||
message.type ===
|
||||
|
@ -198,17 +198,7 @@ export default class WebSocketResource extends EventTarget {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (blob instanceof ArrayBuffer) {
|
||||
handleArrayBuffer(blob);
|
||||
} else {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
handleArrayBuffer(reader.result as ArrayBuffer);
|
||||
};
|
||||
reader.readAsArrayBuffer(blob as any);
|
||||
}
|
||||
};
|
||||
socket.on('message', onMessage);
|
||||
|
||||
if (opts.keepalive) {
|
||||
this.keepalive = new KeepAlive(this, {
|
||||
|
@ -217,15 +207,13 @@ export default class WebSocketResource extends EventTarget {
|
|||
});
|
||||
const resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive);
|
||||
|
||||
socket.addEventListener('open', resetKeepAliveTimer);
|
||||
socket.addEventListener('message', resetKeepAliveTimer);
|
||||
socket.addEventListener(
|
||||
'close',
|
||||
this.keepalive.stop.bind(this.keepalive)
|
||||
);
|
||||
this.keepalive.reset();
|
||||
|
||||
socket.on('message', resetKeepAliveTimer);
|
||||
socket.on('close', this.keepalive.stop.bind(this.keepalive));
|
||||
}
|
||||
|
||||
socket.addEventListener('close', () => {
|
||||
socket.on('close', () => {
|
||||
this.closed = true;
|
||||
});
|
||||
|
||||
|
@ -242,7 +230,7 @@ export default class WebSocketResource extends EventTarget {
|
|||
socket.close(code, reason);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
socket.onmessage = undefined;
|
||||
socket.removeListener('message', onMessage);
|
||||
|
||||
// On linux the socket can wait a long time to emit its close event if we've
|
||||
// lost the internet connection. On the order of minutes. This speeds that
|
||||
|
@ -261,6 +249,13 @@ export default class WebSocketResource extends EventTarget {
|
|||
}, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
public forceKeepAlive(): void {
|
||||
if (!this.keepalive) {
|
||||
return;
|
||||
}
|
||||
this.keepalive.send();
|
||||
}
|
||||
}
|
||||
|
||||
type KeepAliveOptionsType = {
|
||||
|
@ -269,15 +264,15 @@ type KeepAliveOptionsType = {
|
|||
};
|
||||
|
||||
class KeepAlive {
|
||||
keepAliveTimer: any;
|
||||
private keepAliveTimer: NodeJS.Timeout | undefined;
|
||||
|
||||
disconnectTimer: any;
|
||||
private disconnectTimer: NodeJS.Timeout | undefined;
|
||||
|
||||
path: string;
|
||||
private path: string;
|
||||
|
||||
disconnect: boolean;
|
||||
private disconnect: boolean;
|
||||
|
||||
wsr: WebSocketResource;
|
||||
private wsr: WebSocketResource;
|
||||
|
||||
constructor(
|
||||
websocketResource: WebSocketResource,
|
||||
|
@ -292,30 +287,46 @@ class KeepAlive {
|
|||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
clearTimeout(this.keepAliveTimer);
|
||||
clearTimeout(this.disconnectTimer);
|
||||
public stop(): void {
|
||||
this.clearTimers();
|
||||
}
|
||||
|
||||
reset() {
|
||||
clearTimeout(this.keepAliveTimer);
|
||||
clearTimeout(this.disconnectTimer);
|
||||
this.keepAliveTimer = setTimeout(() => {
|
||||
public send(): void {
|
||||
this.clearTimers();
|
||||
|
||||
if (this.disconnect) {
|
||||
// automatically disconnect if server doesn't ack
|
||||
this.disconnectTimer = setTimeout(() => {
|
||||
clearTimeout(this.keepAliveTimer);
|
||||
this.clearTimers();
|
||||
|
||||
this.wsr.close(3001, 'No response to keepalive request');
|
||||
}, 10000);
|
||||
} else {
|
||||
this.reset();
|
||||
}
|
||||
window.log.info('Sending a keepalive message');
|
||||
|
||||
window.log.info('WebSocketResources: Sending a keepalive message');
|
||||
this.wsr.sendRequest({
|
||||
verb: 'GET',
|
||||
path: this.path,
|
||||
success: this.reset.bind(this),
|
||||
});
|
||||
}, 55000);
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.clearTimers();
|
||||
|
||||
this.keepAliveTimer = setTimeout(() => this.send(), 55000);
|
||||
}
|
||||
|
||||
private clearTimers(): void {
|
||||
if (this.keepAliveTimer) {
|
||||
clearTimeout(this.keepAliveTimer);
|
||||
this.keepAliveTimer = undefined;
|
||||
}
|
||||
if (this.disconnectTimer) {
|
||||
clearTimeout(this.disconnectTimer);
|
||||
this.disconnectTimer = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
// Maps to values found here: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
|
||||
// which are returned by libtextsecure's MessageReceiver
|
||||
export enum SocketStatus {
|
||||
CONNECTING,
|
||||
OPEN,
|
||||
CLOSING,
|
||||
CLOSED,
|
||||
CONNECTING = 'CONNECTING',
|
||||
OPEN = 'OPEN',
|
||||
CLOSING = 'CLOSING',
|
||||
CLOSED = 'CLOSED',
|
||||
}
|
||||
|
|
33
ts/util/BackOff.ts
Normal file
33
ts/util/BackOff.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export class BackOff {
|
||||
private count = 0;
|
||||
|
||||
constructor(private readonly timeouts: ReadonlyArray<number>) {}
|
||||
|
||||
public get(): number {
|
||||
return this.timeouts[this.count];
|
||||
}
|
||||
|
||||
public getAndIncrement(): number {
|
||||
const result = this.get();
|
||||
if (!this.isFull()) {
|
||||
this.count += 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
public isFull(): boolean {
|
||||
return this.count === this.timeouts.length - 1;
|
||||
}
|
||||
|
||||
public getIndex(): number {
|
||||
return this.count;
|
||||
}
|
||||
}
|
3
ts/window.d.ts
vendored
3
ts/window.d.ts
vendored
|
@ -108,6 +108,7 @@ import { ElectronLocaleType } from './util/mapToSupportLocale';
|
|||
import { SignalProtocolStore } from './SignalProtocolStore';
|
||||
import { StartupQueue } from './util/StartupQueue';
|
||||
import * as synchronousCrypto from './util/synchronousCrypto';
|
||||
import { SocketStatus } from './types/SocketStatus';
|
||||
import SyncRequest from './textsecure/SyncRequest';
|
||||
import { ConversationColorType, CustomColorType } from './types/Colors';
|
||||
|
||||
|
@ -190,7 +191,7 @@ declare global {
|
|||
getNodeVersion: () => string;
|
||||
getServerPublicParams: () => string;
|
||||
getSfuUrl: () => string;
|
||||
getSocketStatus: () => number;
|
||||
getSocketStatus: () => SocketStatus;
|
||||
getSyncRequest: (timeoutMillis?: number) => SyncRequest;
|
||||
getTitle: () => string;
|
||||
waitForEmptyEventQueue: () => Promise<void>;
|
||||
|
|
Loading…
Reference in a new issue