unauthenticated WebSocket via libsignal: shadowing mode
Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
parent
d696a2c082
commit
9f40562b19
14 changed files with 636 additions and 119 deletions
|
@ -4343,7 +4343,7 @@ For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## attest 0.1.0, device-transfer 0.1.0, libsignal-bridge 0.1.0, libsignal-bridge-macros 0.1.0, libsignal-core 0.1.0, libsignal-ffi 0.40.1, libsignal-jni 0.40.1, libsignal-message-backup 0.1.0, libsignal-message-backup-macros 0.1.0, libsignal-net 0.1.0, libsignal-node 0.40.1, libsignal-protocol 0.1.0, libsignal-svr3 0.1.0, poksho 0.7.0, signal-crypto 0.1.0, signal-media 0.1.0, signal-neon-futures 0.1.0, signal-neon-futures-tests 0.1.0, signal-pin 0.1.0, usernames 0.1.0, zkcredential 0.1.0, zkgroup 0.9.0
|
## attest 0.1.0, device-transfer 0.1.0, libsignal-bridge 0.1.0, libsignal-bridge-macros 0.1.0, libsignal-core 0.1.0, libsignal-ffi 0.41.2, libsignal-jni 0.41.2, libsignal-message-backup 0.1.0, libsignal-message-backup-macros 0.1.0, libsignal-net 0.1.0, libsignal-node 0.41.2, libsignal-protocol 0.1.0, libsignal-svr3 0.1.0, poksho 0.7.0, signal-crypto 0.1.0, signal-media 0.1.0, signal-neon-futures 0.1.0, signal-neon-futures-tests 0.1.0, signal-pin 0.1.0, usernames 0.1.0, zkcredential 0.1.0, zkgroup 0.9.0
|
||||||
|
|
||||||
```
|
```
|
||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
@ -6069,7 +6069,7 @@ express Statement of Purpose.
|
||||||
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
|
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
|
||||||
```
|
```
|
||||||
|
|
||||||
## libloading 0.6.7, libloading 0.8.1
|
## libloading 0.8.1
|
||||||
|
|
||||||
```
|
```
|
||||||
Copyright © 2015, Simonas Kazlauskas
|
Copyright © 2015, Simonas Kazlauskas
|
||||||
|
@ -6479,7 +6479,7 @@ DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## bitflags 1.3.2, bitflags 2.4.2, glob 0.3.1, log 0.4.20, num-derive 0.4.2, num-integer 0.1.45, num-traits 0.2.17, range-map 0.2.0, regex 1.10.3, regex-automata 0.4.4, regex-syntax 0.8.2, semver 0.9.0
|
## bitflags 1.3.2, bitflags 2.4.2, glob 0.3.1, log 0.4.20, num-derive 0.4.2, num-integer 0.1.45, num-traits 0.2.17, range-map 0.2.0, regex 1.10.3, regex-automata 0.4.4, regex-syntax 0.8.2
|
||||||
|
|
||||||
```
|
```
|
||||||
Copyright (c) 2014 The Rust Project Developers
|
Copyright (c) 2014 The Rust Project Developers
|
||||||
|
@ -6681,7 +6681,7 @@ THE SOFTWARE.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## neon 0.10.1, neon-build 0.10.1, neon-macros 0.10.1, neon-runtime 0.10.1
|
## neon-macros 1.0.0
|
||||||
|
|
||||||
```
|
```
|
||||||
Copyright (c) 2015 David Herman
|
Copyright (c) 2015 David Herman
|
||||||
|
@ -7301,37 +7301,6 @@ DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## semver-parser 0.7.0
|
|
||||||
|
|
||||||
```
|
|
||||||
Copyright (c) 2016 Steve Klabnik
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any
|
|
||||||
person obtaining a copy of this software and associated
|
|
||||||
documentation files (the "Software"), to deal in the
|
|
||||||
Software without restriction, including without
|
|
||||||
limitation the rights to use, copy, modify, merge,
|
|
||||||
publish, distribute, sublicense, and/or sell copies of
|
|
||||||
the Software, and to permit persons to whom the Software
|
|
||||||
is furnished to do so, subject to the following
|
|
||||||
conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice
|
|
||||||
shall be included in all copies or substantial portions
|
|
||||||
of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
|
||||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
|
||||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
|
||||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
||||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
||||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
|
||||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
||||||
DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## lock_api 0.4.11, parking_lot 0.12.1, parking_lot_core 0.9.9, rustc_version 0.4.0
|
## lock_api 0.4.11, parking_lot 0.12.1, parking_lot_core 0.9.9, rustc_version 0.4.0
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -9459,6 +9428,33 @@ SOFTWARE.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## strum 0.26.1, strum_macros 0.26.1
|
||||||
|
|
||||||
|
```
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 Peter Glotfelty
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
## fslock 0.2.1
|
## fslock 0.2.1
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -9674,7 +9670,7 @@ SOFTWARE.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## cesu8 1.1.0, curve25519-dalek-derive 0.1.0, half 1.8.2, pqcrypto-internals 0.2.5, pqcrypto-kyber 0.7.9, pqcrypto-kyber 0.8.0, pqcrypto-traits 0.3.5
|
## cesu8 1.1.0, curve25519-dalek-derive 0.1.0, half 1.8.2, neon 1.0.0, pqcrypto-internals 0.2.5, pqcrypto-kyber 0.7.9, pqcrypto-kyber 0.8.0, pqcrypto-traits 0.3.5
|
||||||
|
|
||||||
```
|
```
|
||||||
MIT License
|
MIT License
|
||||||
|
@ -9788,7 +9784,7 @@ DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## adler 1.0.2, anyhow 1.0.79, async-trait 0.1.77, dyn-clone 1.0.16, fastrand 2.0.1, home 0.5.9, itoa 1.0.10, linkme 0.3.22, linkme-impl 0.3.22, linux-raw-sys 0.4.13, minimal-lexical 0.2.1, num_enum 0.6.1, num_enum_derive 0.6.1, once_cell 1.19.0, paste 1.0.14, pin-project-lite 0.2.13, prettyplease 0.2.16, proc-macro-crate 1.3.1, proc-macro2 1.0.78, quote 1.0.35, rustc-hash 1.1.0, rustix 0.38.30, semver 1.0.21, serde 1.0.195, serde_derive 1.0.195, serde_json 1.0.111, syn 1.0.109, syn 2.0.48, syn-mid 0.5.4, syn-mid 0.6.0, thiserror 1.0.56, thiserror-impl 1.0.56, unicode-ident 1.0.12, utf-8 0.7.6
|
## adler 1.0.2, anyhow 1.0.79, async-trait 0.1.77, dyn-clone 1.0.16, fastrand 2.0.1, home 0.5.9, itoa 1.0.10, linkme 0.3.22, linkme-impl 0.3.22, linux-raw-sys 0.4.13, minimal-lexical 0.2.1, num_enum 0.6.1, num_enum_derive 0.6.1, once_cell 1.19.0, paste 1.0.14, pin-project-lite 0.2.13, prettyplease 0.2.16, proc-macro-crate 1.3.1, proc-macro2 1.0.78, quote 1.0.35, rustc-hash 1.1.0, rustix 0.38.30, rustversion 1.0.14, semver 1.0.21, send_wrapper 0.6.0, serde 1.0.195, serde_derive 1.0.195, serde_json 1.0.111, syn 1.0.109, syn 2.0.48, syn-mid 0.6.0, thiserror 1.0.56, thiserror-impl 1.0.56, unicode-ident 1.0.12, utf-8 0.7.6
|
||||||
|
|
||||||
```
|
```
|
||||||
Permission is hereby granted, free of charge, to any
|
Permission is hereby granted, free of charge, to any
|
||||||
|
@ -10590,6 +10586,10 @@ written authorization of the copyright holder.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Kyber Patent License
|
||||||
|
|
||||||
|
<https://csrc.nist.gov/csrc/media/Projects/post-quantum-cryptography/documents/selected-algos-2022/nist-pqc-license-summary-and-excerpts.pdf>
|
||||||
|
|
||||||
# Acknowledgements for @signalapp/ringrtc
|
# Acknowledgements for @signalapp/ringrtc
|
||||||
|
|
||||||
RingRTC makes use of the following open source projects.
|
RingRTC makes use of the following open source projects.
|
||||||
|
|
|
@ -7368,6 +7368,10 @@
|
||||||
"messageformat": "{count, plural, one {# other is} other {# others are}} typing.",
|
"messageformat": "{count, plural, one {# other is} other {# others are}} typing.",
|
||||||
"description": "Group chat multiple person typing indicator when space isn't available to show every avatar, this is the count of avatars hidden."
|
"description": "Group chat multiple person typing indicator when space isn't available to show every avatar, this is the count of avatars hidden."
|
||||||
},
|
},
|
||||||
|
"icu:TransportError": {
|
||||||
|
"messageformat": "Experimental WebSocket Transport is seeing too many errors. Please submit a debug log",
|
||||||
|
"description": "A toast that is shown to alpha/beta version users when an experimental transport is seeing too many errors"
|
||||||
|
},
|
||||||
"icu:WhoCanFindMeReadOnlyToast": {
|
"icu:WhoCanFindMeReadOnlyToast": {
|
||||||
"messageformat": "To change this setting, set “Who can see my number” to “Nobody”.",
|
"messageformat": "To change this setting, set “Who can see my number” to “Nobody”.",
|
||||||
"description": "A toast displayed when user clicks disabled option in settings window"
|
"description": "A toast displayed when user clicks disabled option in settings window"
|
||||||
|
|
|
@ -102,7 +102,7 @@
|
||||||
"@react-aria/utils": "3.16.0",
|
"@react-aria/utils": "3.16.0",
|
||||||
"@react-spring/web": "9.5.5",
|
"@react-spring/web": "9.5.5",
|
||||||
"@signalapp/better-sqlite3": "8.7.1",
|
"@signalapp/better-sqlite3": "8.7.1",
|
||||||
"@signalapp/libsignal-client": "0.40.1",
|
"@signalapp/libsignal-client": "0.41.2",
|
||||||
"@signalapp/ringrtc": "2.39.1",
|
"@signalapp/ringrtc": "2.39.1",
|
||||||
"@signalapp/windows-dummy-keystroke": "1.0.0",
|
"@signalapp/windows-dummy-keystroke": "1.0.0",
|
||||||
"@types/fabric": "4.5.3",
|
"@types/fabric": "4.5.3",
|
||||||
|
|
|
@ -24,6 +24,8 @@ export type ConfigKeyType =
|
||||||
| 'desktop.retryRespondMaxAge'
|
| 'desktop.retryRespondMaxAge'
|
||||||
| 'desktop.senderKey.retry'
|
| 'desktop.senderKey.retry'
|
||||||
| 'desktop.senderKeyMaxAge'
|
| 'desktop.senderKeyMaxAge'
|
||||||
|
| 'desktop.experimentalTransportEnabled.alpha'
|
||||||
|
| 'desktop.experimentalTransportEnabled.beta'
|
||||||
| 'global.attachments.maxBytes'
|
| 'global.attachments.maxBytes'
|
||||||
| 'global.attachments.maxReceiveBytes'
|
| 'global.attachments.maxReceiveBytes'
|
||||||
| 'global.calling.maxGroupCallRingSize'
|
| 'global.calling.maxGroupCallRingSize'
|
||||||
|
|
|
@ -141,6 +141,8 @@ function getToast(toastType: ToastType): AnyToast {
|
||||||
return { toastType: ToastType.TapToViewExpiredIncoming };
|
return { toastType: ToastType.TapToViewExpiredIncoming };
|
||||||
case ToastType.TapToViewExpiredOutgoing:
|
case ToastType.TapToViewExpiredOutgoing:
|
||||||
return { toastType: ToastType.TapToViewExpiredOutgoing };
|
return { toastType: ToastType.TapToViewExpiredOutgoing };
|
||||||
|
case ToastType.TransportError:
|
||||||
|
return { toastType: ToastType.TransportError };
|
||||||
case ToastType.TooManyMessagesToDeleteForEveryone:
|
case ToastType.TooManyMessagesToDeleteForEveryone:
|
||||||
return {
|
return {
|
||||||
toastType: ToastType.TooManyMessagesToDeleteForEveryone,
|
toastType: ToastType.TooManyMessagesToDeleteForEveryone,
|
||||||
|
|
|
@ -470,6 +470,10 @@ export function renderToast({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (toastType === ToastType.TransportError) {
|
||||||
|
return <Toast onClose={hideToast}>{i18n('icu:TransportError')}</Toast>;
|
||||||
|
}
|
||||||
|
|
||||||
if (toastType === ToastType.UnableToLoadAttachment) {
|
if (toastType === ToastType.UnableToLoadAttachment) {
|
||||||
return (
|
return (
|
||||||
<Toast onClose={hideToast}>{i18n('icu:unableToLoadAttachment')}</Toast>
|
<Toast onClose={hideToast}>{i18n('icu:unableToLoadAttachment')}</Toast>
|
||||||
|
|
|
@ -127,9 +127,9 @@ describe('WebSocket-Resource', () => {
|
||||||
}).finish(),
|
}).finish(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { status, message } = await promise;
|
const response = await promise;
|
||||||
assert.strictEqual(message, 'OK');
|
assert.strictEqual(response.statusText, 'OK');
|
||||||
assert.strictEqual(status, 200);
|
assert.strictEqual(response.status, 200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,13 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import URL from 'url';
|
import URL from 'url';
|
||||||
import type { RequestInit } from 'node-fetch';
|
import type { RequestInit, Response } from 'node-fetch';
|
||||||
import { Response, Headers } from 'node-fetch';
|
import { Headers } from 'node-fetch';
|
||||||
import type { connection as WebSocket } from 'websocket';
|
import type { connection as WebSocket } from 'websocket';
|
||||||
import qs from 'querystring';
|
import qs from 'querystring';
|
||||||
import EventListener from 'events';
|
import EventListener from 'events';
|
||||||
|
|
||||||
import type { AbortableProcess } from '../util/AbortableProcess';
|
import { AbortableProcess } from '../util/AbortableProcess';
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
import { BackOff, FIBONACCI_TIMEOUTS } from '../util/BackOff';
|
import { BackOff, FIBONACCI_TIMEOUTS } from '../util/BackOff';
|
||||||
import * as durations from '../util/durations';
|
import * as durations from '../util/durations';
|
||||||
|
@ -20,18 +20,28 @@ import * as Bytes from '../Bytes';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
WebSocketResourceOptions,
|
|
||||||
IncomingWebSocketRequest,
|
IncomingWebSocketRequest,
|
||||||
|
IWebSocketResource,
|
||||||
|
WebSocketResourceOptions,
|
||||||
|
} from './WebsocketResources';
|
||||||
|
import WebSocketResource, {
|
||||||
|
LibsignalWebSocketResource,
|
||||||
|
TransportOption,
|
||||||
|
WebSocketResourceWithShadowing,
|
||||||
} from './WebsocketResources';
|
} from './WebsocketResources';
|
||||||
import WebSocketResource from './WebsocketResources';
|
|
||||||
import { HTTPError } from './Errors';
|
import { HTTPError } from './Errors';
|
||||||
import type { WebAPICredentials, IRequestHandler } from './Types.d';
|
import type { IRequestHandler, WebAPICredentials } from './Types.d';
|
||||||
import { connect as connectWebSocket } from './WebSocket';
|
import { connect as connectWebSocket } from './WebSocket';
|
||||||
|
import { isAlpha, isBeta, isStaging } from '../util/version';
|
||||||
|
|
||||||
const FIVE_MINUTES = 5 * durations.MINUTE;
|
const FIVE_MINUTES = 5 * durations.MINUTE;
|
||||||
|
|
||||||
const JITTER = 5 * durations.SECOND;
|
const JITTER = 5 * durations.SECOND;
|
||||||
|
|
||||||
|
export const UNAUTHENTICATED_CHANNEL_NAME = 'unauthenticated';
|
||||||
|
|
||||||
|
export const AUTHENTICATED_CHANNEL_NAME = 'authenticated';
|
||||||
|
|
||||||
export type SocketManagerOptions = Readonly<{
|
export type SocketManagerOptions = Readonly<{
|
||||||
url: string;
|
url: string;
|
||||||
artCreatorUrl: string;
|
artCreatorUrl: string;
|
||||||
|
@ -43,9 +53,9 @@ export type SocketManagerOptions = Readonly<{
|
||||||
|
|
||||||
// This class manages two websocket resources:
|
// This class manages two websocket resources:
|
||||||
//
|
//
|
||||||
// - Authenticated WebSocketResource which uses supplied WebAPICredentials and
|
// - Authenticated IWebSocketResource which uses supplied WebAPICredentials and
|
||||||
// automatically reconnects on closed socket (using back off)
|
// automatically reconnects on closed socket (using back off)
|
||||||
// - Unauthenticated WebSocketResource that is created on the first outgoing
|
// - Unauthenticated IWebSocketResource that is created on the first outgoing
|
||||||
// unauthenticated request and is periodically rotated (5 minutes since first
|
// unauthenticated request and is periodically rotated (5 minutes since first
|
||||||
// activity on the socket).
|
// activity on the socket).
|
||||||
//
|
//
|
||||||
|
@ -54,15 +64,15 @@ export type SocketManagerOptions = Readonly<{
|
||||||
// least one such request handler becomes available.
|
// least one such request handler becomes available.
|
||||||
//
|
//
|
||||||
// Incoming requests on unauthenticated resource are not currently supported.
|
// Incoming requests on unauthenticated resource are not currently supported.
|
||||||
// WebSocketResource is responsible for their immediate termination.
|
// IWebSocketResource is responsible for their immediate termination.
|
||||||
export class SocketManager extends EventListener {
|
export class SocketManager extends EventListener {
|
||||||
private backOff = new BackOff(FIBONACCI_TIMEOUTS, {
|
private backOff = new BackOff(FIBONACCI_TIMEOUTS, {
|
||||||
jitter: JITTER,
|
jitter: JITTER,
|
||||||
});
|
});
|
||||||
|
|
||||||
private authenticated?: AbortableProcess<WebSocketResource>;
|
private authenticated?: AbortableProcess<IWebSocketResource>;
|
||||||
|
|
||||||
private unauthenticated?: AbortableProcess<WebSocketResource>;
|
private unauthenticated?: AbortableProcess<IWebSocketResource>;
|
||||||
|
|
||||||
private unauthenticatedExpirationTimer?: NodeJS.Timeout;
|
private unauthenticatedExpirationTimer?: NodeJS.Timeout;
|
||||||
|
|
||||||
|
@ -138,11 +148,11 @@ export class SocketManager extends EventListener {
|
||||||
this.setStatus(SocketStatus.CONNECTING);
|
this.setStatus(SocketStatus.CONNECTING);
|
||||||
|
|
||||||
const process = this.connectResource({
|
const process = this.connectResource({
|
||||||
name: 'authenticated',
|
name: AUTHENTICATED_CHANNEL_NAME,
|
||||||
path: '/v1/websocket/',
|
path: '/v1/websocket/',
|
||||||
query: { login: username, password },
|
query: { login: username, password },
|
||||||
resourceOptions: {
|
resourceOptions: {
|
||||||
name: 'authenticated',
|
name: AUTHENTICATED_CHANNEL_NAME,
|
||||||
keepalive: { path: '/v1/keepalive' },
|
keepalive: { path: '/v1/keepalive' },
|
||||||
handleRequest: (req: IncomingWebSocketRequest): void => {
|
handleRequest: (req: IncomingWebSocketRequest): void => {
|
||||||
this.queueOrHandleRequest(req);
|
this.queueOrHandleRequest(req);
|
||||||
|
@ -190,7 +200,7 @@ export class SocketManager extends EventListener {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let authenticated: WebSocketResource;
|
let authenticated: IWebSocketResource;
|
||||||
try {
|
try {
|
||||||
authenticated = await process.getResult();
|
authenticated = await process.getResult();
|
||||||
this.setStatus(SocketStatus.OPEN);
|
this.setStatus(SocketStatus.OPEN);
|
||||||
|
@ -230,7 +240,7 @@ export class SocketManager extends EventListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
`SocketManager: connected authenticated socket (localPort: ${authenticated.localPort})`
|
`SocketManager: connected authenticated socket (localPort: ${authenticated.localPort()})`
|
||||||
);
|
);
|
||||||
|
|
||||||
window.logAuthenticatedConnect?.();
|
window.logAuthenticatedConnect?.();
|
||||||
|
@ -262,8 +272,8 @@ export class SocketManager extends EventListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Either returns currently connecting/active authenticated
|
// Either returns currently connecting/active authenticated
|
||||||
// WebSocketResource or connects a fresh one.
|
// IWebSocketResource or connects a fresh one.
|
||||||
public async getAuthenticatedResource(): Promise<WebSocketResource> {
|
public async getAuthenticatedResource(): Promise<IWebSocketResource> {
|
||||||
if (!this.authenticated) {
|
if (!this.authenticated) {
|
||||||
strictAssert(this.credentials !== undefined, 'Missing credentials');
|
strictAssert(this.credentials !== undefined, 'Missing credentials');
|
||||||
await this.authenticate(this.credentials);
|
await this.authenticate(this.credentials);
|
||||||
|
@ -273,10 +283,10 @@ export class SocketManager extends EventListener {
|
||||||
return this.authenticated.getResult();
|
return this.authenticated.getResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates new WebSocketResource for AccountManager's provisioning
|
// Creates new IWebSocketResource for AccountManager's provisioning
|
||||||
public async getProvisioningResource(
|
public async getProvisioningResource(
|
||||||
handler: IRequestHandler
|
handler: IRequestHandler
|
||||||
): Promise<WebSocketResource> {
|
): Promise<IWebSocketResource> {
|
||||||
return this.connectResource({
|
return this.connectResource({
|
||||||
name: 'provisioning',
|
name: 'provisioning',
|
||||||
path: '/v1/websocket/provisioning/',
|
path: '/v1/websocket/provisioning/',
|
||||||
|
@ -317,7 +327,7 @@ export class SocketManager extends EventListener {
|
||||||
public async fetch(url: string, init: RequestInit): Promise<Response> {
|
public async fetch(url: string, init: RequestInit): Promise<Response> {
|
||||||
const headers = new Headers(init.headers);
|
const headers = new Headers(init.headers);
|
||||||
|
|
||||||
let resource: WebSocketResource;
|
let resource: IWebSocketResource;
|
||||||
if (this.isAuthenticated(headers)) {
|
if (this.isAuthenticated(headers)) {
|
||||||
resource = await this.getAuthenticatedResource();
|
resource = await this.getAuthenticatedResource();
|
||||||
} else {
|
} else {
|
||||||
|
@ -343,34 +353,13 @@ export class SocketManager extends EventListener {
|
||||||
throw new Error(`Unsupported body type: ${typeof body}`);
|
throw new Error(`Unsupported body type: ${typeof body}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
return resource.sendRequest({
|
||||||
status,
|
|
||||||
message: statusText,
|
|
||||||
response,
|
|
||||||
headers: flatResponseHeaders,
|
|
||||||
} = await resource.sendRequest({
|
|
||||||
verb: method,
|
verb: method,
|
||||||
path,
|
path,
|
||||||
body: bodyBytes,
|
body: bodyBytes,
|
||||||
headers: Array.from(headers.entries()).map(([key, value]) => {
|
headers: Array.from(headers.entries()),
|
||||||
return `${key}:${value}`;
|
|
||||||
}),
|
|
||||||
timeout,
|
timeout,
|
||||||
});
|
});
|
||||||
|
|
||||||
const responseHeaders: Array<[string, string]> = flatResponseHeaders.map(
|
|
||||||
header => {
|
|
||||||
const [key, value] = header.split(':', 2);
|
|
||||||
strictAssert(value !== undefined, 'Invalid header!');
|
|
||||||
return [key, value];
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return new Response(response, {
|
|
||||||
status,
|
|
||||||
statusText,
|
|
||||||
headers: responseHeaders,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public registerRequestHandler(handler: IRequestHandler): void {
|
public registerRequestHandler(handler: IRequestHandler): void {
|
||||||
|
@ -427,7 +416,7 @@ export class SocketManager extends EventListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Puts SocketManager into "online" state and reconnects the authenticated
|
// Puts SocketManager into "online" state and reconnects the authenticated
|
||||||
// WebSocketResource (if there are valid credentials)
|
// IWebSocketResource (if there are valid credentials)
|
||||||
public async onOnline(): Promise<void> {
|
public async onOnline(): Promise<void> {
|
||||||
log.info('SocketManager.onOnline');
|
log.info('SocketManager.onOnline');
|
||||||
this.isOffline = false;
|
this.isOffline = false;
|
||||||
|
@ -477,7 +466,62 @@ export class SocketManager extends EventListener {
|
||||||
this.emit('statusChange');
|
this.emit('statusChange');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getUnauthenticatedResource(): Promise<WebSocketResource> {
|
private transportOption(): TransportOption {
|
||||||
|
const { hostname } = URL.parse(this.options.url);
|
||||||
|
|
||||||
|
// transport experiment doesn't support proxy
|
||||||
|
if (
|
||||||
|
this.proxyAgent ||
|
||||||
|
hostname == null ||
|
||||||
|
!hostname.endsWith('signal.org')
|
||||||
|
) {
|
||||||
|
return TransportOption.Original;
|
||||||
|
}
|
||||||
|
|
||||||
|
// in staging, switch to using libsignal transport
|
||||||
|
if (isStaging(this.options.version)) {
|
||||||
|
return TransportOption.Libsignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// in alpha, switch to using libsignal transport, unless user opts out,
|
||||||
|
// in which case switching to shadowing
|
||||||
|
if (isAlpha(this.options.version)) {
|
||||||
|
const configValue = window.Signal.RemoteConfig.isEnabled(
|
||||||
|
'desktop.experimentalTransportEnabled.alpha'
|
||||||
|
);
|
||||||
|
return configValue
|
||||||
|
? TransportOption.Libsignal
|
||||||
|
: TransportOption.ShadowingHigh;
|
||||||
|
}
|
||||||
|
|
||||||
|
// in beta, switch to using 'ShadowingHigh' mode, unless user opts out,
|
||||||
|
// in which case switching to `ShadowingLow`
|
||||||
|
if (isBeta(this.options.version)) {
|
||||||
|
const configValue = window.Signal.RemoteConfig.isEnabled(
|
||||||
|
'desktop.experimentalTransportEnabled.beta'
|
||||||
|
);
|
||||||
|
return configValue
|
||||||
|
? TransportOption.ShadowingHigh
|
||||||
|
: TransportOption.ShadowingLow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// in prod, using original
|
||||||
|
return TransportOption.ShadowingLow;
|
||||||
|
}
|
||||||
|
|
||||||
|
private connectLibsignalUnauthenticated(): AbortableProcess<IWebSocketResource> {
|
||||||
|
return new AbortableProcess<IWebSocketResource>(
|
||||||
|
`WebSocket.connect(libsignal.${UNAUTHENTICATED_CHANNEL_NAME})`,
|
||||||
|
{
|
||||||
|
abort() {
|
||||||
|
// noop
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Promise.resolve(new LibsignalWebSocketResource(this.options.version))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getUnauthenticatedResource(): Promise<IWebSocketResource> {
|
||||||
if (this.isOffline) {
|
if (this.isOffline) {
|
||||||
throw new HTTPError('SocketManager offline', {
|
throw new HTTPError('SocketManager offline', {
|
||||||
code: 0,
|
code: 0,
|
||||||
|
@ -490,19 +534,28 @@ export class SocketManager extends EventListener {
|
||||||
return this.unauthenticated.getResult();
|
return this.unauthenticated.getResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info('SocketManager: connecting unauthenticated socket');
|
const transportOption = this.transportOption();
|
||||||
|
log.info(
|
||||||
|
`SocketManager: connecting unauthenticated socket, transport option [${transportOption}]`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (transportOption === TransportOption.Libsignal) {
|
||||||
|
this.unauthenticated = this.connectLibsignalUnauthenticated();
|
||||||
|
return this.unauthenticated.getResult();
|
||||||
|
}
|
||||||
|
|
||||||
const process = this.connectResource({
|
const process = this.connectResource({
|
||||||
name: 'unauthenticated',
|
name: UNAUTHENTICATED_CHANNEL_NAME,
|
||||||
path: '/v1/websocket/',
|
path: '/v1/websocket/',
|
||||||
resourceOptions: {
|
resourceOptions: {
|
||||||
name: 'unauthenticated',
|
name: UNAUTHENTICATED_CHANNEL_NAME,
|
||||||
keepalive: { path: '/v1/keepalive' },
|
keepalive: { path: '/v1/keepalive' },
|
||||||
|
transportOption,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.unauthenticated = process;
|
this.unauthenticated = process;
|
||||||
|
|
||||||
let unauthenticated: WebSocketResource;
|
let unauthenticated: IWebSocketResource;
|
||||||
try {
|
try {
|
||||||
unauthenticated = await this.unauthenticated.getResult();
|
unauthenticated = await this.unauthenticated.getResult();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -515,7 +568,7 @@ export class SocketManager extends EventListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
`SocketManager: connected unauthenticated socket (localPort: ${unauthenticated.localPort})`
|
`SocketManager: connected unauthenticated socket (localPort: ${unauthenticated.localPort()})`
|
||||||
);
|
);
|
||||||
|
|
||||||
unauthenticated.addEventListener('close', ({ code, reason }): void => {
|
unauthenticated.addEventListener('close', ({ code, reason }): void => {
|
||||||
|
@ -546,7 +599,7 @@ export class SocketManager extends EventListener {
|
||||||
resourceOptions: WebSocketResourceOptions;
|
resourceOptions: WebSocketResourceOptions;
|
||||||
query?: Record<string, string>;
|
query?: Record<string, string>;
|
||||||
extraHeaders?: Record<string, string>;
|
extraHeaders?: Record<string, string>;
|
||||||
}): AbortableProcess<WebSocketResource> {
|
}): AbortableProcess<IWebSocketResource> {
|
||||||
const queryWithDefaults = {
|
const queryWithDefaults = {
|
||||||
agent: 'OWD',
|
agent: 'OWD',
|
||||||
version: this.options.version,
|
version: this.options.version,
|
||||||
|
@ -554,24 +607,32 @@ export class SocketManager extends EventListener {
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = `${this.options.url}${path}?${qs.encode(queryWithDefaults)}`;
|
const url = `${this.options.url}${path}?${qs.encode(queryWithDefaults)}`;
|
||||||
|
const { version } = this.options;
|
||||||
|
|
||||||
return connectWebSocket({
|
return connectWebSocket({
|
||||||
name,
|
name,
|
||||||
url,
|
url,
|
||||||
|
version,
|
||||||
certificateAuthority: this.options.certificateAuthority,
|
certificateAuthority: this.options.certificateAuthority,
|
||||||
version: this.options.version,
|
|
||||||
proxyAgent: this.proxyAgent,
|
proxyAgent: this.proxyAgent,
|
||||||
|
|
||||||
extraHeaders,
|
extraHeaders,
|
||||||
|
|
||||||
createResource(socket: WebSocket): WebSocketResource {
|
createResource(socket: WebSocket): IWebSocketResource {
|
||||||
return new WebSocketResource(socket, resourceOptions);
|
return !resourceOptions.transportOption ||
|
||||||
|
resourceOptions.transportOption === TransportOption.Original
|
||||||
|
? new WebSocketResource(socket, resourceOptions)
|
||||||
|
: new WebSocketResourceWithShadowing(
|
||||||
|
socket,
|
||||||
|
resourceOptions,
|
||||||
|
version
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async checkResource(
|
private static async checkResource(
|
||||||
process?: AbortableProcess<WebSocketResource>
|
process?: AbortableProcess<IWebSocketResource>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!process) {
|
if (!process) {
|
||||||
return;
|
return;
|
||||||
|
@ -582,7 +643,7 @@ export class SocketManager extends EventListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private dropAuthenticated(
|
private dropAuthenticated(
|
||||||
process: AbortableProcess<WebSocketResource>
|
process: AbortableProcess<IWebSocketResource>
|
||||||
): void {
|
): void {
|
||||||
if (this.authenticated !== process) {
|
if (this.authenticated !== process) {
|
||||||
return;
|
return;
|
||||||
|
@ -594,7 +655,7 @@ export class SocketManager extends EventListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private dropUnauthenticated(
|
private dropUnauthenticated(
|
||||||
process: AbortableProcess<WebSocketResource>
|
process: AbortableProcess<IWebSocketResource>
|
||||||
): void {
|
): void {
|
||||||
if (this.unauthenticated !== process) {
|
if (this.unauthenticated !== process) {
|
||||||
return;
|
return;
|
||||||
|
@ -609,7 +670,7 @@ export class SocketManager extends EventListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async startUnauthenticatedExpirationTimer(
|
private async startUnauthenticatedExpirationTimer(
|
||||||
expected: WebSocketResource
|
expected: IWebSocketResource
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const process = this.unauthenticated;
|
const process = this.unauthenticated;
|
||||||
strictAssert(
|
strictAssert(
|
||||||
|
|
|
@ -55,7 +55,6 @@ import { isBadgeImageFileUrlValid } from '../badges/isBadgeImageFileUrlValid';
|
||||||
import { SocketManager } from './SocketManager';
|
import { SocketManager } from './SocketManager';
|
||||||
import type { CDSAuthType, CDSResponseType } from './cds/Types.d';
|
import type { CDSAuthType, CDSResponseType } from './cds/Types.d';
|
||||||
import { CDSI } from './cds/CDSI';
|
import { CDSI } from './cds/CDSI';
|
||||||
import type WebSocketResource from './WebsocketResources';
|
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
|
|
||||||
import { HTTPError } from './Errors';
|
import { HTTPError } from './Errors';
|
||||||
|
@ -70,6 +69,7 @@ import { handleStatusCode, translateError } from './Utils';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import { maybeParseUrl, urlPathFromComponents } from '../util/url';
|
import { maybeParseUrl, urlPathFromComponents } from '../util/url';
|
||||||
import { SECOND } from '../util/durations';
|
import { SECOND } from '../util/durations';
|
||||||
|
import type { IWebSocketResource } from './WebsocketResources';
|
||||||
|
|
||||||
// Note: this will break some code that expects to be able to use err.response when a
|
// Note: this will break some code that expects to be able to use err.response when a
|
||||||
// web request fails, because it will force it to text. But it is very useful for
|
// web request fails, because it will force it to text. But it is very useful for
|
||||||
|
@ -1034,7 +1034,7 @@ export type WebAPIType = {
|
||||||
) => Promise<unknown>;
|
) => Promise<unknown>;
|
||||||
getProvisioningResource: (
|
getProvisioningResource: (
|
||||||
handler: IRequestHandler
|
handler: IRequestHandler
|
||||||
) => Promise<WebSocketResource>;
|
) => Promise<IWebSocketResource>;
|
||||||
getArtProvisioningSocket: (token: string) => Promise<WebSocket>;
|
getArtProvisioningSocket: (token: string) => Promise<WebSocket>;
|
||||||
getSenderCertificate: (
|
getSenderCertificate: (
|
||||||
withUuid?: boolean
|
withUuid?: boolean
|
||||||
|
@ -3473,7 +3473,7 @@ export function initialize({
|
||||||
|
|
||||||
function getProvisioningResource(
|
function getProvisioningResource(
|
||||||
handler: IRequestHandler
|
handler: IRequestHandler
|
||||||
): Promise<WebSocketResource> {
|
): Promise<IWebSocketResource> {
|
||||||
return socketManager.getProvisioningResource(handler);
|
return socketManager.getProvisioningResource(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,9 +23,19 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-namespace */
|
||||||
|
/* eslint-disable @typescript-eslint/brace-style */
|
||||||
|
|
||||||
|
import { Net } from '@signalapp/libsignal-client';
|
||||||
import type { connection as WebSocket, IMessage } from 'websocket';
|
import type { connection as WebSocket, IMessage } from 'websocket';
|
||||||
import Long from 'long';
|
import Long from 'long';
|
||||||
import pTimeout from 'p-timeout';
|
import pTimeout from 'p-timeout';
|
||||||
|
import { Response } from 'node-fetch';
|
||||||
|
import net from 'net';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { clearInterval } from 'timers';
|
||||||
|
import { random } from 'lodash';
|
||||||
|
import type { DebugInfo } from '@signalapp/libsignal-client/Native';
|
||||||
|
|
||||||
import type { EventHandler } from './EventTarget';
|
import type { EventHandler } from './EventTarget';
|
||||||
import EventTarget from './EventTarget';
|
import EventTarget from './EventTarget';
|
||||||
|
@ -38,11 +48,121 @@ import * as Errors from '../types/errors';
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import * as Timers from '../Timers';
|
import * as Timers from '../Timers';
|
||||||
|
import type { IResource } from './WebSocket';
|
||||||
|
import { isProduction, isStaging } from '../util/version';
|
||||||
|
|
||||||
|
import { ToastType } from '../types/Toast';
|
||||||
|
import { drop } from '../util/drop';
|
||||||
|
|
||||||
const THIRTY_SECONDS = 30 * durations.SECOND;
|
const THIRTY_SECONDS = 30 * durations.SECOND;
|
||||||
|
|
||||||
|
const HEALTHCHECK_TIMEOUT = durations.SECOND;
|
||||||
|
|
||||||
|
const STATS_UPDATE_INTERVAL = durations.MINUTE;
|
||||||
|
|
||||||
const MAX_MESSAGE_SIZE = 512 * 1024;
|
const MAX_MESSAGE_SIZE = 512 * 1024;
|
||||||
|
|
||||||
|
const AGGREGATED_STATS_KEY = 'websocketStats';
|
||||||
|
|
||||||
|
export enum IpVersion {
|
||||||
|
IPv4 = 'ipv4',
|
||||||
|
IPv6 = 'ipv6',
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace IpVersion {
|
||||||
|
export function fromDebugInfoCode(ipType: number): IpVersion | undefined {
|
||||||
|
switch (ipType) {
|
||||||
|
case 1:
|
||||||
|
return IpVersion.IPv4;
|
||||||
|
case 2:
|
||||||
|
return IpVersion.IPv6;
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const AggregatedStatsSchema = z.object({
|
||||||
|
requestsCompared: z.number(),
|
||||||
|
ipVersionMismatches: z.number(),
|
||||||
|
unexpectedReconnects: z.number(),
|
||||||
|
healthcheckFailures: z.number(),
|
||||||
|
healthcheckBadStatus: z.number(),
|
||||||
|
lastToastTimestamp: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AggregatedStats = z.infer<typeof AggregatedStatsSchema>;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||||
|
export namespace AggregatedStats {
|
||||||
|
export function loadOrCreateEmpty(name: string): AggregatedStats {
|
||||||
|
const key = localStorageKey(name);
|
||||||
|
try {
|
||||||
|
const json = localStorage.getItem(key);
|
||||||
|
return json != null
|
||||||
|
? AggregatedStatsSchema.parse(JSON.parse(json))
|
||||||
|
: createEmpty();
|
||||||
|
} catch (error) {
|
||||||
|
log.warn(
|
||||||
|
`Could not load [${key}] from local storage. Possibly, attempting to load for the first time`,
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
return createEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function store(stats: AggregatedStats, name: string): void {
|
||||||
|
const key = localStorageKey(name);
|
||||||
|
try {
|
||||||
|
const json = JSON.stringify(stats);
|
||||||
|
localStorage.setItem(key, json);
|
||||||
|
} catch (error) {
|
||||||
|
log.warn(
|
||||||
|
`Failed to store key [${key}] to the local storage`,
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function add(a: AggregatedStats, b: AggregatedStats): AggregatedStats {
|
||||||
|
return {
|
||||||
|
requestsCompared: a.requestsCompared + b.requestsCompared,
|
||||||
|
healthcheckFailures: a.healthcheckFailures + b.healthcheckFailures,
|
||||||
|
ipVersionMismatches: a.ipVersionMismatches + b.ipVersionMismatches,
|
||||||
|
unexpectedReconnects: a.unexpectedReconnects + b.unexpectedReconnects,
|
||||||
|
healthcheckBadStatus: a.healthcheckBadStatus + b.healthcheckBadStatus,
|
||||||
|
lastToastTimestamp: Math.max(a.lastToastTimestamp, b.lastToastTimestamp),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createEmpty(): AggregatedStats {
|
||||||
|
return {
|
||||||
|
requestsCompared: 0,
|
||||||
|
ipVersionMismatches: 0,
|
||||||
|
unexpectedReconnects: 0,
|
||||||
|
healthcheckFailures: 0,
|
||||||
|
healthcheckBadStatus: 0,
|
||||||
|
lastToastTimestamp: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldReportError(stats: AggregatedStats): boolean {
|
||||||
|
const timeSinceLastToast = Date.now() - stats.lastToastTimestamp;
|
||||||
|
if (timeSinceLastToast < durations.DAY || stats.requestsCompared < 1000) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
stats.healthcheckBadStatus + stats.healthcheckFailures > 20 ||
|
||||||
|
stats.ipVersionMismatches > 50 ||
|
||||||
|
stats.unexpectedReconnects > 50
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function localStorageKey(name: string): string {
|
||||||
|
return `${AGGREGATED_STATS_KEY}.${name}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class IncomingWebSocketRequest {
|
export class IncomingWebSocketRequest {
|
||||||
private readonly id: Long;
|
private readonly id: Long;
|
||||||
|
|
||||||
|
@ -84,7 +204,7 @@ export type SendRequestOptions = Readonly<{
|
||||||
path: string;
|
path: string;
|
||||||
body?: Uint8Array;
|
body?: Uint8Array;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
headers?: ReadonlyArray<string>;
|
headers?: ReadonlyArray<[string, string]>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type SendRequestResult = Readonly<{
|
export type SendRequestResult = Readonly<{
|
||||||
|
@ -94,10 +214,29 @@ export type SendRequestResult = Readonly<{
|
||||||
headers: ReadonlyArray<string>;
|
headers: ReadonlyArray<string>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export enum TransportOption {
|
||||||
|
// Only original transport is used
|
||||||
|
Original = 'original',
|
||||||
|
// All requests are going through the original transport,
|
||||||
|
// but for every request that completes sucessfully we're initiating
|
||||||
|
// a healthcheck request via libsignal transport,
|
||||||
|
// collecting comparison statistics, and if we see many inconsistencies,
|
||||||
|
// we're showing a toast asking user to submit a debug log
|
||||||
|
ShadowingHigh = 'shadowingHigh',
|
||||||
|
// Similar to `shadowingHigh`, however, only 10% of requests
|
||||||
|
// will trigger a healthcheck, and toast is never shown.
|
||||||
|
// Statistics data is still added to the debug logs,
|
||||||
|
// so it will be available to us with all the debug log uploads.
|
||||||
|
ShadowingLow = 'shadowingLow',
|
||||||
|
// Only libsignal transport is used
|
||||||
|
Libsignal = 'libsignal',
|
||||||
|
}
|
||||||
|
|
||||||
export type WebSocketResourceOptions = {
|
export type WebSocketResourceOptions = {
|
||||||
name: string;
|
name: string;
|
||||||
handleRequest?: (request: IncomingWebSocketRequest) => void;
|
handleRequest?: (request: IncomingWebSocketRequest) => void;
|
||||||
keepalive?: KeepAliveOptionsType;
|
keepalive?: KeepAliveOptionsType;
|
||||||
|
transportOption?: TransportOption;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class CloseEvent extends Event {
|
export class CloseEvent extends Event {
|
||||||
|
@ -106,7 +245,225 @@ export class CloseEvent extends Event {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class WebSocketResource extends EventTarget {
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
export interface IWebSocketResource extends IResource {
|
||||||
|
sendRequest(options: SendRequestOptions): Promise<Response>;
|
||||||
|
|
||||||
|
addEventListener(name: 'close', handler: (ev: CloseEvent) => void): void;
|
||||||
|
|
||||||
|
forceKeepAlive(): void;
|
||||||
|
|
||||||
|
shutdown(): void;
|
||||||
|
|
||||||
|
close(): void;
|
||||||
|
|
||||||
|
localPort(): number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LibsignalWebSocketResource implements IWebSocketResource {
|
||||||
|
private readonly net: Net.Net;
|
||||||
|
|
||||||
|
constructor(version: string) {
|
||||||
|
this.net = new Net.Net(
|
||||||
|
isStaging(version) ? Net.Environment.Staging : Net.Environment.Production
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public localPort(): number | undefined {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addEventListener(
|
||||||
|
_name: 'close',
|
||||||
|
_handler: (ev: CloseEvent) => void
|
||||||
|
): void {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
|
public close(_code?: number, _reason?: string): void {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
this.net.disconnectChatService();
|
||||||
|
}
|
||||||
|
|
||||||
|
public shutdown(): void {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
this.net.disconnectChatService();
|
||||||
|
}
|
||||||
|
|
||||||
|
public forceKeepAlive(): void {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendRequest(options: SendRequestOptions): Promise<Response> {
|
||||||
|
const [response] = await this.sendRequestGetDebugInfo(options);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendRequestGetDebugInfo(
|
||||||
|
options: SendRequestOptions
|
||||||
|
): Promise<[Response, DebugInfo]> {
|
||||||
|
const { response, debugInfo } = await this.net.unauthenticatedFetchAndDebug(
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
statusText: response.message,
|
||||||
|
headers: [...response.headers],
|
||||||
|
}),
|
||||||
|
debugInfo,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WebSocketResourceWithShadowing implements IWebSocketResource {
|
||||||
|
private main: WebSocketResource;
|
||||||
|
|
||||||
|
private shadowing: LibsignalWebSocketResource;
|
||||||
|
|
||||||
|
private stats: AggregatedStats;
|
||||||
|
|
||||||
|
private statsTimer: NodeJS.Timer;
|
||||||
|
|
||||||
|
private shadowingWithReporting: boolean;
|
||||||
|
|
||||||
|
private logId: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
socket: WebSocket,
|
||||||
|
options: WebSocketResourceOptions,
|
||||||
|
version: string
|
||||||
|
) {
|
||||||
|
this.main = new WebSocketResource(socket, options);
|
||||||
|
this.shadowing = new LibsignalWebSocketResource(version);
|
||||||
|
this.stats = AggregatedStats.createEmpty();
|
||||||
|
this.logId = `WebSocketResourceWithShadowing(${options.name})`;
|
||||||
|
this.statsTimer = setInterval(
|
||||||
|
() => this.updateStats(options.name),
|
||||||
|
STATS_UPDATE_INTERVAL
|
||||||
|
);
|
||||||
|
this.shadowingWithReporting =
|
||||||
|
options.transportOption === TransportOption.ShadowingHigh;
|
||||||
|
|
||||||
|
this.addEventListener('close', (_ev): void => {
|
||||||
|
clearInterval(this.statsTimer);
|
||||||
|
this.updateStats(options.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateStats(name: string) {
|
||||||
|
const storedStats = AggregatedStats.loadOrCreateEmpty(name);
|
||||||
|
const updatedStats = AggregatedStats.add(storedStats, this.stats);
|
||||||
|
if (
|
||||||
|
this.shadowingWithReporting &&
|
||||||
|
AggregatedStats.shouldReportError(updatedStats) &&
|
||||||
|
!isProduction(window.getVersion())
|
||||||
|
) {
|
||||||
|
window.reduxActions.toast.showToast({
|
||||||
|
toastType: ToastType.TransportError,
|
||||||
|
});
|
||||||
|
updatedStats.lastToastTimestamp = Date.now();
|
||||||
|
}
|
||||||
|
AggregatedStats.store(updatedStats, name);
|
||||||
|
this.stats = AggregatedStats.createEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public localPort(): number | undefined {
|
||||||
|
return this.main.localPort();
|
||||||
|
}
|
||||||
|
|
||||||
|
public addEventListener(
|
||||||
|
name: 'close',
|
||||||
|
handler: (ev: CloseEvent) => void
|
||||||
|
): void {
|
||||||
|
this.main.addEventListener(name, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public close(): void {
|
||||||
|
this.main.close();
|
||||||
|
this.shadowing.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public shutdown(): void {
|
||||||
|
this.main.shutdown();
|
||||||
|
this.shadowing.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public forceKeepAlive(): void {
|
||||||
|
this.main.forceKeepAlive();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendRequest(options: SendRequestOptions): Promise<Response> {
|
||||||
|
const responsePromise = this.main.sendRequest(options);
|
||||||
|
const response = await responsePromise;
|
||||||
|
|
||||||
|
// if we're received a response from the main channel and the status was successful,
|
||||||
|
// attempting to run a healthcheck on a libsignal transport.
|
||||||
|
if (
|
||||||
|
isSuccessfulStatusCode(response.status) &&
|
||||||
|
this.shouldSendShadowRequest()
|
||||||
|
) {
|
||||||
|
drop(this.sendShadowRequest());
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendShadowRequest(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const [healthCheckResult, debugInfo] =
|
||||||
|
await this.shadowing.sendRequestGetDebugInfo({
|
||||||
|
verb: 'GET',
|
||||||
|
path: '/v1/keepalive',
|
||||||
|
timeout: HEALTHCHECK_TIMEOUT,
|
||||||
|
});
|
||||||
|
this.stats.requestsCompared += 1;
|
||||||
|
if (!isSuccessfulStatusCode(healthCheckResult.status)) {
|
||||||
|
this.stats.healthcheckBadStatus += 1;
|
||||||
|
log.warn(
|
||||||
|
`${this.logId}: keepalive via libsignal responded with status [${healthCheckResult.status}]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const ipVersion = IpVersion.fromDebugInfoCode(debugInfo.ipType);
|
||||||
|
if (this.main.ipVersion() !== ipVersion) {
|
||||||
|
this.stats.ipVersionMismatches += 1;
|
||||||
|
log.warn(
|
||||||
|
`${
|
||||||
|
this.logId
|
||||||
|
}: keepalive via libsignal using IP [${ipVersion}] while main is using IP [${this.main.ipVersion()}]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (debugInfo.reconnectCount > 1) {
|
||||||
|
this.stats.unexpectedReconnects = debugInfo.reconnectCount - 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.stats.healthcheckFailures += 1;
|
||||||
|
log.warn(
|
||||||
|
`${this.logId}: failed to send keepalive via libsignal`,
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldSendShadowRequest(): boolean {
|
||||||
|
return this.shadowingWithReporting || random(0, 100) < 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSuccessfulStatusCode(status: number): boolean {
|
||||||
|
return status >= 200 && status < 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class WebSocketResource
|
||||||
|
extends EventTarget
|
||||||
|
implements IWebSocketResource
|
||||||
|
{
|
||||||
private outgoingId = Long.fromNumber(1, true);
|
private outgoingId = Long.fromNumber(1, true);
|
||||||
|
|
||||||
private closed = false;
|
private closed = false;
|
||||||
|
@ -126,7 +483,9 @@ export default class WebSocketResource extends EventTarget {
|
||||||
|
|
||||||
private readonly logId: string;
|
private readonly logId: string;
|
||||||
|
|
||||||
public readonly localPort: number | undefined;
|
private readonly localSocketPort: number | undefined;
|
||||||
|
|
||||||
|
private readonly socketIpVersion: IpVersion | undefined;
|
||||||
|
|
||||||
// Public for tests
|
// Public for tests
|
||||||
public readonly keepalive?: KeepAlive;
|
public readonly keepalive?: KeepAlive;
|
||||||
|
@ -138,7 +497,20 @@ export default class WebSocketResource extends EventTarget {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.logId = `WebSocketResource(${options.name})`;
|
this.logId = `WebSocketResource(${options.name})`;
|
||||||
this.localPort = socket.socket.localPort;
|
this.localSocketPort = socket.socket.localPort;
|
||||||
|
|
||||||
|
if (!socket.socket.localAddress) {
|
||||||
|
this.socketIpVersion = undefined;
|
||||||
|
}
|
||||||
|
if (socket.socket.localAddress == null) {
|
||||||
|
this.socketIpVersion = undefined;
|
||||||
|
} else if (net.isIPv4(socket.socket.localAddress)) {
|
||||||
|
this.socketIpVersion = IpVersion.IPv4;
|
||||||
|
} else if (net.isIPv6(socket.socket.localAddress)) {
|
||||||
|
this.socketIpVersion = IpVersion.IPv6;
|
||||||
|
} else {
|
||||||
|
this.socketIpVersion = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
this.boundOnMessage = this.onMessage.bind(this);
|
this.boundOnMessage = this.onMessage.bind(this);
|
||||||
|
|
||||||
|
@ -169,6 +541,14 @@ export default class WebSocketResource extends EventTarget {
|
||||||
this.addEventListener('close', () => this.onClose());
|
this.addEventListener('close', () => this.onClose());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ipVersion(): IpVersion | undefined {
|
||||||
|
return this.socketIpVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public localPort(): number | undefined {
|
||||||
|
return this.localSocketPort;
|
||||||
|
}
|
||||||
|
|
||||||
public override addEventListener(
|
public override addEventListener(
|
||||||
name: 'close',
|
name: 'close',
|
||||||
handler: (ev: CloseEvent) => void
|
handler: (ev: CloseEvent) => void
|
||||||
|
@ -178,9 +558,7 @@ export default class WebSocketResource extends EventTarget {
|
||||||
return super.addEventListener(name, handler);
|
return super.addEventListener(name, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sendRequest(
|
public async sendRequest(options: SendRequestOptions): Promise<Response> {
|
||||||
options: SendRequestOptions
|
|
||||||
): Promise<SendRequestResult> {
|
|
||||||
const id = this.outgoingId;
|
const id = this.outgoingId;
|
||||||
const idString = id.toString();
|
const idString = id.toString();
|
||||||
strictAssert(!this.outgoingMap.has(idString), 'Duplicate outgoing request');
|
strictAssert(!this.outgoingMap.has(idString), 'Duplicate outgoing request');
|
||||||
|
@ -194,7 +572,13 @@ export default class WebSocketResource extends EventTarget {
|
||||||
verb: options.verb,
|
verb: options.verb,
|
||||||
path: options.path,
|
path: options.path,
|
||||||
body: options.body,
|
body: options.body,
|
||||||
headers: options.headers ? options.headers.slice() : undefined,
|
headers: options.headers
|
||||||
|
? options.headers
|
||||||
|
.map(([key, value]) => {
|
||||||
|
return `${key}:${value}`;
|
||||||
|
})
|
||||||
|
.slice()
|
||||||
|
: undefined,
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
}).finish();
|
}).finish();
|
||||||
|
@ -239,7 +623,8 @@ export default class WebSocketResource extends EventTarget {
|
||||||
|
|
||||||
this.socket.sendBytes(Buffer.from(bytes));
|
this.socket.sendBytes(Buffer.from(bytes));
|
||||||
|
|
||||||
return promise;
|
const requestResult = await promise;
|
||||||
|
return WebSocketResource.intoResponse(requestResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
public forceKeepAlive(): void {
|
public forceKeepAlive(): void {
|
||||||
|
@ -399,6 +784,27 @@ export default class WebSocketResource extends EventTarget {
|
||||||
log.info(`${this.logId}.removeActive: shutdown complete`);
|
log.info(`${this.logId}.removeActive: shutdown complete`);
|
||||||
this.close(3000, 'Shutdown');
|
this.close(3000, 'Shutdown');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static intoResponse(sendRequestResult: SendRequestResult): Response {
|
||||||
|
const {
|
||||||
|
status,
|
||||||
|
message: statusText,
|
||||||
|
response,
|
||||||
|
headers: flatResponseHeaders,
|
||||||
|
} = sendRequestResult;
|
||||||
|
|
||||||
|
const headers: Array<[string, string]> = flatResponseHeaders.map(header => {
|
||||||
|
const [key, value] = header.split(':', 2);
|
||||||
|
strictAssert(value !== undefined, 'Invalid header!');
|
||||||
|
return [key, value];
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(response, {
|
||||||
|
status,
|
||||||
|
statusText,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type KeepAliveOptionsType = {
|
export type KeepAliveOptionsType = {
|
||||||
|
|
2
ts/textsecure/cds/Types.d.ts
vendored
2
ts/textsecure/cds/Types.d.ts
vendored
|
@ -4,7 +4,7 @@
|
||||||
import type { Net } from '@signalapp/libsignal-client';
|
import type { Net } from '@signalapp/libsignal-client';
|
||||||
import type { AciString, PniString } from '../../types/ServiceId';
|
import type { AciString, PniString } from '../../types/ServiceId';
|
||||||
|
|
||||||
export type CDSAuthType = Net.CDSAuthType;
|
export type CDSAuthType = Net.ServiceAuth;
|
||||||
export type CDSResponseEntryType = Net.CDSResponseEntryType<
|
export type CDSResponseEntryType = Net.CDSResponseEntryType<
|
||||||
AciString,
|
AciString,
|
||||||
PniString
|
PniString
|
||||||
|
|
|
@ -56,6 +56,7 @@ export enum ToastType {
|
||||||
TapToViewExpiredOutgoing = 'TapToViewExpiredOutgoing',
|
TapToViewExpiredOutgoing = 'TapToViewExpiredOutgoing',
|
||||||
TooManyMessagesToDeleteForEveryone = 'TooManyMessagesToDeleteForEveryone',
|
TooManyMessagesToDeleteForEveryone = 'TooManyMessagesToDeleteForEveryone',
|
||||||
TooManyMessagesToForward = 'TooManyMessagesToForward',
|
TooManyMessagesToForward = 'TooManyMessagesToForward',
|
||||||
|
TransportError = 'TransportError',
|
||||||
UnableToLoadAttachment = 'UnableToLoadAttachment',
|
UnableToLoadAttachment = 'UnableToLoadAttachment',
|
||||||
UnsupportedMultiAttachment = 'UnsupportedMultiAttachment',
|
UnsupportedMultiAttachment = 'UnsupportedMultiAttachment',
|
||||||
UnsupportedOS = 'UnsupportedOS',
|
UnsupportedOS = 'UnsupportedOS',
|
||||||
|
@ -136,6 +137,7 @@ export type AnyToast =
|
||||||
parameters: { count: number };
|
parameters: { count: number };
|
||||||
}
|
}
|
||||||
| { toastType: ToastType.TooManyMessagesToForward }
|
| { toastType: ToastType.TooManyMessagesToForward }
|
||||||
|
| { toastType: ToastType.TransportError }
|
||||||
| { toastType: ToastType.UnableToLoadAttachment }
|
| { toastType: ToastType.UnableToLoadAttachment }
|
||||||
| { toastType: ToastType.UnsupportedMultiAttachment }
|
| { toastType: ToastType.UnsupportedMultiAttachment }
|
||||||
| { toastType: ToastType.UnsupportedOS }
|
| { toastType: ToastType.UnsupportedOS }
|
||||||
|
|
|
@ -22,6 +22,8 @@ import type {
|
||||||
WindowsNotificationData,
|
WindowsNotificationData,
|
||||||
} from '../../services/notifications';
|
} from '../../services/notifications';
|
||||||
import { isAdhocCallingEnabled } from '../../util/isAdhocCallingEnabled';
|
import { isAdhocCallingEnabled } from '../../util/isAdhocCallingEnabled';
|
||||||
|
import { AggregatedStats } from '../../textsecure/WebsocketResources';
|
||||||
|
import { UNAUTHENTICATED_CHANNEL_NAME } from '../../textsecure/SocketManager';
|
||||||
|
|
||||||
// It is important to call this as early as possible
|
// It is important to call this as early as possible
|
||||||
window.i18n = SignalContext.i18n;
|
window.i18n = SignalContext.i18n;
|
||||||
|
@ -171,6 +173,15 @@ if (config.ciMode !== 'full' && config.environment !== 'test') {
|
||||||
window.eval = global.eval = () => null;
|
window.eval = global.eval = () => null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NetworkStatistics = {
|
||||||
|
signalConnectionCount?: string;
|
||||||
|
unauthorizedRequestsCompared?: string;
|
||||||
|
unauthorizedHealthcheckFailures?: string;
|
||||||
|
unauthorizedHealthcheckBadStatus?: string;
|
||||||
|
unauthorizedUnexpectedReconnects?: string;
|
||||||
|
unauthorizedIpVersionMismatches?: string;
|
||||||
|
};
|
||||||
|
|
||||||
ipc.on('additional-log-data-request', async event => {
|
ipc.on('additional-log-data-request', async event => {
|
||||||
const ourConversation = window.ConversationController.getOurConversation();
|
const ourConversation = window.ConversationController.getOurConversation();
|
||||||
const ourCapabilities = ourConversation
|
const ourCapabilities = ourConversation
|
||||||
|
@ -186,6 +197,33 @@ ipc.on('additional-log-data-request', async event => {
|
||||||
statistics = {};
|
statistics = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let networkStatistics: NetworkStatistics = {
|
||||||
|
signalConnectionCount: formatCountForLogging(getSignalConnections().length),
|
||||||
|
};
|
||||||
|
const unauthorizedStats = AggregatedStats.loadOrCreateEmpty(
|
||||||
|
UNAUTHENTICATED_CHANNEL_NAME
|
||||||
|
);
|
||||||
|
if (unauthorizedStats.requestsCompared > 0) {
|
||||||
|
networkStatistics = {
|
||||||
|
...networkStatistics,
|
||||||
|
unauthorizedRequestsCompared: formatCountForLogging(
|
||||||
|
unauthorizedStats.requestsCompared
|
||||||
|
),
|
||||||
|
unauthorizedHealthcheckFailures: formatCountForLogging(
|
||||||
|
unauthorizedStats.healthcheckFailures
|
||||||
|
),
|
||||||
|
unauthorizedHealthcheckBadStatus: formatCountForLogging(
|
||||||
|
unauthorizedStats.healthcheckBadStatus
|
||||||
|
),
|
||||||
|
unauthorizedUnexpectedReconnects: formatCountForLogging(
|
||||||
|
unauthorizedStats.unexpectedReconnects
|
||||||
|
),
|
||||||
|
unauthorizedIpVersionMismatches: formatCountForLogging(
|
||||||
|
unauthorizedStats.ipVersionMismatches
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const ourAci = window.textsecure.storage.user.getAci();
|
const ourAci = window.textsecure.storage.user.getAci();
|
||||||
const ourPni = window.textsecure.storage.user.getPni();
|
const ourPni = window.textsecure.storage.user.getPni();
|
||||||
|
|
||||||
|
@ -198,9 +236,7 @@ ipc.on('additional-log-data-request', async event => {
|
||||||
}),
|
}),
|
||||||
statistics: {
|
statistics: {
|
||||||
...statistics,
|
...statistics,
|
||||||
signalConnectionCount: formatCountForLogging(
|
...networkStatistics,
|
||||||
getSignalConnections().length
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
deviceId: window.textsecure.storage.user.getDeviceId(),
|
deviceId: window.textsecure.storage.user.getDeviceId(),
|
||||||
|
|
|
@ -3983,10 +3983,10 @@
|
||||||
bindings "^1.5.0"
|
bindings "^1.5.0"
|
||||||
tar "^6.1.0"
|
tar "^6.1.0"
|
||||||
|
|
||||||
"@signalapp/libsignal-client@0.40.1":
|
"@signalapp/libsignal-client@0.41.2":
|
||||||
version "0.40.1"
|
version "0.41.2"
|
||||||
resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.40.1.tgz#07ed6d3da9d2458aa7e66f5a71a8a7ef6b4a467a"
|
resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.41.2.tgz#da8d415168ab1f89c0b6e05d259feab60893e717"
|
||||||
integrity sha512-5BLDpOrP7eXT9U1Vf8F3HVOt4dnLeFMCzalkSyGdFgzD59i/wva3PHtQtbfKhxX7pNaXwhele1tAbIXsKYtXUQ==
|
integrity sha512-GQh0AJ1gwifYbZhe+974e17Qj0Fymiv5qM1u0NHtr5SK0bTly7stTi14JYJ9exlOysaLMq1QdAt/CoQFwfQahg==
|
||||||
dependencies:
|
dependencies:
|
||||||
node-gyp-build "^4.2.3"
|
node-gyp-build "^4.2.3"
|
||||||
type-fest "^3.5.0"
|
type-fest "^3.5.0"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue