diff --git a/ACKNOWLEDGMENTS.md b/ACKNOWLEDGMENTS.md index 5183c756be..fa4a606222 100644 --- a/ACKNOWLEDGMENTS.md +++ b/ACKNOWLEDGMENTS.md @@ -4733,7 +4733,7 @@ For more information on this, and how to apply and follow the GNU AGPL, see ``` -## attest 0.1.0, libsignal-ffi 0.54.0, libsignal-jni 0.54.0, libsignal-jni-testing 0.54.0, libsignal-node 0.54.0, signal-neon-futures 0.1.0, signal-neon-futures-tests 0.1.0, libsignal-bridge 0.1.0, libsignal-bridge-macros 0.1.0, libsignal-bridge-testing 0.1.0, libsignal-bridge-types 0.1.0, libsignal-core 0.1.0, signal-crypto 0.1.0, device-transfer 0.1.0, signal-media 0.1.0, libsignal-message-backup 0.1.0, libsignal-message-backup-macros 0.1.0, libsignal-net 0.1.0, signal-pin 0.1.0, poksho 0.7.0, libsignal-protocol 0.1.0, libsignal-svr3 0.1.0, usernames 0.1.0, zkcredential 0.1.0, zkgroup 0.9.0 +## attest 0.1.0, libsignal-ffi 0.55.0, libsignal-jni 0.55.0, libsignal-jni-testing 0.55.0, libsignal-node 0.55.0, signal-neon-futures 0.1.0, signal-neon-futures-tests 0.1.0, libsignal-bridge 0.1.0, libsignal-bridge-macros 0.1.0, libsignal-bridge-testing 0.1.0, libsignal-bridge-types 0.1.0, libsignal-core 0.1.0, signal-crypto 0.1.0, device-transfer 0.1.0, signal-media 0.1.0, libsignal-message-backup 0.1.0, libsignal-message-backup-macros 0.1.0, libsignal-net 0.1.0, signal-pin 0.1.0, poksho 0.7.0, libsignal-protocol 0.1.0, libsignal-svr3 0.1.0, usernames 0.1.0, zkcredential 0.1.0, zkgroup 0.9.0 ``` GNU AFFERO GENERAL PUBLIC LICENSE @@ -5986,7 +5986,7 @@ limitations under the License. ``` -## boring 4.6.0 +## boring 4.9.0 ``` Copyright 2011-2017 Google Inc. @@ -6421,7 +6421,7 @@ express Statement of Purpose. CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ``` -## boring-sys 4.6.0 +## boring-sys 4.9.0 ``` /* Copyright (c) 2015, Google Inc. @@ -6792,7 +6792,7 @@ DEALINGS IN THE SOFTWARE. ``` -## boring-sys 4.6.0 +## boring-sys 4.9.0 ``` Copyright (c) 2014 Alex Crichton @@ -7113,7 +7113,7 @@ THE SOFTWARE. ``` -## anstyle-wincon 3.0.4 +## anstyle-wincon 3.0.3 ``` Copyright (c) 2015 Josh Triplett, 2022 The rust-cli Developers @@ -7200,7 +7200,7 @@ DEALINGS IN THE SOFTWARE. ``` -## gimli 0.29.0, heck 0.5.0, peeking_take_while 0.1.2, unicode-bidi 0.3.15, unicode-normalization 0.1.23 +## gimli 0.29.0, heck 0.4.1, heck 0.5.0, peeking_take_while 0.1.2, unicode-bidi 0.3.15, unicode-normalization 0.1.23 ``` Copyright (c) 2015 The Rust Project Developers @@ -7351,7 +7351,7 @@ DEALINGS IN THE SOFTWARE. ``` -## boring-sys 4.6.0 +## boring-sys 4.9.0 ``` Copyright (c) 2015-2016 the fiat-crypto authors (see @@ -7579,7 +7579,7 @@ DEALINGS IN THE SOFTWARE. ``` -## hashbrown 0.14.5 +## hashbrown 0.12.3, hashbrown 0.14.5 ``` Copyright (c) 2016 Amanieu d'Antras @@ -7641,7 +7641,7 @@ DEALINGS IN THE SOFTWARE. ``` -## anstyle-parse 0.2.5 +## anstyle-parse 0.2.4 ``` Copyright (c) 2016 Joe Wilm and individual contributors @@ -7822,7 +7822,7 @@ SOFTWARE. ``` -## tokio-boring 4.6.0 +## tokio-boring 4.9.0 ``` Copyright (c) 2016 Tokio contributors @@ -7885,7 +7885,7 @@ DEALINGS IN THE SOFTWARE. ``` -## indexmap 2.2.6 +## indexmap 1.9.3, indexmap 2.2.6 ``` Copyright (c) 2016--2017 @@ -8757,7 +8757,7 @@ DEALINGS IN THE SOFTWARE. ``` -## protobuf-codegen 3.5.0, protobuf-json-mapping 3.5.0, protobuf-support 3.5.0, protobuf 3.5.0 +## protobuf-codegen 3.4.0, protobuf-json-mapping 3.4.0, protobuf-parse 3.4.0, protobuf-support 3.4.0, protobuf 3.4.0 ``` Copyright (c) 2019 Stepan Koltsov @@ -8779,7 +8779,6 @@ 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. - ``` ## ppv-lite86 0.2.17 @@ -9306,7 +9305,7 @@ DEALINGS IN THE SOFTWARE. ``` -## anstyle 1.0.8 +## anstyle 1.0.7 ``` Copyright (c) 2022 The rust-cli Developers @@ -9518,7 +9517,7 @@ SOFTWARE. ``` -## anstream 0.6.15, anstyle-query 1.1.1, clap 4.5.11, colorchoice 1.0.2, env_filter 0.1.2, env_logger 0.11.5, is_terminal_polyfill 1.70.1, toml_datetime 0.6.7, toml_edit 0.21.1 +## anstream 0.6.14, anstyle-query 1.1.0, clap 4.4.18, colorchoice 1.0.1, env_filter 0.1.1, env_logger 0.11.4, is_terminal_polyfill 1.70.0, toml_datetime 0.6.6, toml_edit 0.21.1 ``` Copyright (c) Individual contributors @@ -10248,7 +10247,7 @@ SOFTWARE. ``` -## cesu8 1.1.0, half 2.4.1, pqcrypto-internals 0.2.5, pqcrypto-kyber 0.7.9, pqcrypto-kyber 0.8.1, pqcrypto-traits 0.3.5, protobuf-parse 3.5.0 +## cesu8 1.1.0, half 2.4.1, pqcrypto-internals 0.2.5, pqcrypto-kyber 0.7.9, pqcrypto-kyber 0.8.1, pqcrypto-traits 0.3.5 ``` MIT License @@ -10597,7 +10596,7 @@ THE SOFTWARE. ``` -## strsim 0.10.0, strsim 0.11.1 +## strsim 0.10.0 ``` The MIT License (MIT) @@ -10735,7 +10734,7 @@ SOFTWARE. ``` -## clap_builder 4.5.11, clap_derive 4.5.11, clap_lex 0.7.2 +## clap_builder 4.4.18, clap_derive 4.4.7, clap_lex 0.6.0 ``` The MIT License (MIT) @@ -11018,7 +11017,7 @@ SOFTWARE. ``` -## version_check 0.9.5 +## version_check 0.9.4 ``` The MIT License (MIT) @@ -11096,7 +11095,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` -## boring-sys 4.6.0, ring 0.17.8 +## boring-sys 4.9.0, ring 0.17.8 ``` /* ==================================================================== diff --git a/package-lock.json b/package-lock.json index c71a11aadf..a8c2ebd7f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@react-aria/utils": "3.16.0", "@react-spring/web": "9.5.5", "@signalapp/better-sqlite3": "8.7.1", - "@signalapp/libsignal-client": "0.54.0", + "@signalapp/libsignal-client": "0.55.0", "@signalapp/ringrtc": "2.46.0", "@signalapp/windows-dummy-keystroke": "1.0.0", "@types/fabric": "4.5.3", @@ -7223,9 +7223,9 @@ } }, "node_modules/@signalapp/libsignal-client": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@signalapp/libsignal-client/-/libsignal-client-0.54.0.tgz", - "integrity": "sha512-bkdw3r39UU0W0UaFAJJVhLC5AEiMkgCgtdtabI64c1KuT7oeIx4XvegfqtBwF7vKj29ZHz4lODCh3tFyqbrNLw==", + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@signalapp/libsignal-client/-/libsignal-client-0.55.0.tgz", + "integrity": "sha512-SwsBLUlHsUU6ae6cWsvok2maaRqIoACi0LgW0uGtTrNDeSUdja3j2Dnt/M5InQ+LaU5DQg8xMyUq8KKRp2RmpQ==", "hasInstallScript": true, "dependencies": { "node-gyp-build": "^4.2.3", diff --git a/package.json b/package.json index 72d9ceb181..7a5301d50c 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "@react-aria/utils": "3.16.0", "@react-spring/web": "9.5.5", "@signalapp/better-sqlite3": "8.7.1", - "@signalapp/libsignal-client": "0.54.0", + "@signalapp/libsignal-client": "0.55.0", "@signalapp/ringrtc": "2.46.0", "@signalapp/windows-dummy-keystroke": "1.0.0", "@types/fabric": "4.5.3", diff --git a/ts/test-electron/backup/non_bubble_test.ts b/ts/test-electron/backup/non_bubble_test.ts index 35a676ecce..f20f5c6cf9 100644 --- a/ts/test-electron/backup/non_bubble_test.ts +++ b/ts/test-electron/backup/non_bubble_test.ts @@ -10,6 +10,7 @@ import { getRandomBytes } from '../../Crypto'; import * as Bytes from '../../Bytes'; import { SignalService as Proto, Backups } from '../../protobuf'; import { DataWriter } from '../../sql/Client'; +import { APPLICATION_OCTET_STREAM } from '../../types/MIME'; import { generateAci } from '../../types/ServiceId'; import { PaymentEventKind } from '../../types/Payment'; import { ContactFormType } from '../../types/EmbeddedContact'; @@ -372,6 +373,11 @@ describe('backup/non-bubble messages', () => { packId: Bytes.toHex(getRandomBytes(16)), stickerId: 1, packKey: Bytes.toBase64(getRandomBytes(32)), + data: { + contentType: APPLICATION_OCTET_STREAM, + error: true, + size: 0, + }, }, reactions: [ { diff --git a/ts/textsecure/SocketManager.ts b/ts/textsecure/SocketManager.ts index 717a52dcca..b03089048e 100644 --- a/ts/textsecure/SocketManager.ts +++ b/ts/textsecure/SocketManager.ts @@ -591,14 +591,12 @@ export class SocketManager extends EventListener { : TransportOption.Original; } - private connectLibsignalUnauthenticated(): AbortableProcess { - return connectUnauthenticatedLibsignal({ - libsignalNet: this.libsignalNet, - name: UNAUTHENTICATED_CHANNEL_NAME, - }); - } - private async getUnauthenticatedResource(): Promise { + // awaiting on `this.getProxyAgent()` needs to happen here + // so that there are no calls to `await` between checking + // the value of `this.unauthenticated` and assigning it later in this function + const proxyAgent = await this.getProxyAgent(); + if (this.unauthenticated) { return this.unauthenticated.getResult(); } @@ -613,8 +611,6 @@ export class SocketManager extends EventListener { log.info('SocketManager: connecting unauthenticated socket'); - const proxyAgent = await this.getProxyAgent(); - const transportOption = this.transportOption(proxyAgent); log.info( `SocketManager: connecting unauthenticated socket, transport option [${transportOption}]` @@ -623,7 +619,10 @@ export class SocketManager extends EventListener { let process: AbortableProcess; if (transportOption === TransportOption.Libsignal) { - process = this.connectLibsignalUnauthenticated(); + process = connectUnauthenticatedLibsignal({ + libsignalNet: this.libsignalNet, + name: UNAUTHENTICATED_CHANNEL_NAME, + }); } else { process = this.connectResource({ name: UNAUTHENTICATED_CHANNEL_NAME, diff --git a/ts/textsecure/WebsocketResources.ts b/ts/textsecure/WebsocketResources.ts index d648eaef80..8b5dc5dc6e 100644 --- a/ts/textsecure/WebsocketResources.ts +++ b/ts/textsecure/WebsocketResources.ts @@ -38,7 +38,11 @@ import type { ChatServiceDebugInfo } from '@signalapp/libsignal-client/Native'; import type { Net } from '@signalapp/libsignal-client'; import { Buffer } from 'node:buffer'; -import type { ChatServerMessageAck } from '@signalapp/libsignal-client/dist/net'; +import type { + ChatServerMessageAck, + ChatServiceListener, + ConnectionEventsListener, +} from '@signalapp/libsignal-client/dist/net'; import type { EventHandler } from './EventTarget'; import EventTarget from './EventTarget'; @@ -88,7 +92,6 @@ const AggregatedStatsSchema = z.object({ connectionFailures: z.number(), requestsCompared: z.number(), ipVersionMismatches: z.number(), - unexpectedReconnects: z.number(), healthcheckFailures: z.number(), healthcheckBadStatus: z.number(), lastToastTimestamp: z.number(), @@ -133,7 +136,6 @@ export namespace AggregatedStats { connectionFailures: a.connectionFailures + b.connectionFailures, 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), }; @@ -144,7 +146,6 @@ export namespace AggregatedStats { requestsCompared: 0, connectionFailures: 0, ipVersionMismatches: 0, - unexpectedReconnects: 0, healthcheckFailures: 0, healthcheckBadStatus: 0, lastToastTimestamp: 0, @@ -156,12 +157,11 @@ export namespace AggregatedStats { if (timeSinceLastToast < durations.DAY || stats.requestsCompared < 1000) { return false; } - return ( + const totalFailuresSinceLastToast = stats.healthcheckBadStatus + - stats.healthcheckFailures + - stats.connectionFailures > - 20 || stats.unexpectedReconnects > 50 - ); + stats.healthcheckFailures + + stats.connectionFailures; + return totalFailuresSinceLastToast > 20; } export function localStorageKey(name: string): string { @@ -330,6 +330,10 @@ export interface IWebSocketResource extends IResource { localPort(): number | undefined; } +type LibsignalWebSocketResourceHolder = { + resource: LibsignalWebSocketResource | undefined; +}; + export function connectUnauthenticatedLibsignal({ libsignalNet, name, @@ -337,7 +341,24 @@ export function connectUnauthenticatedLibsignal({ libsignalNet: Net.Net; name: string; }): AbortableProcess { - return connectLibsignal(libsignalNet.newUnauthenticatedChatService(), name); + const logId = `LibsignalWebSocketResource(${name})`; + const listener: LibsignalWebSocketResourceHolder & ConnectionEventsListener = + { + resource: undefined, + onConnectionInterrupted(): void { + if (!this.resource) { + logDisconnectedListenerWarn(logId, 'onConnectionInterrupted'); + return; + } + this.resource.onConnectionInterrupted(); + this.resource = undefined; + }, + }; + return connectLibsignal( + libsignalNet.newUnauthenticatedChatService(listener), + listener, + logId + ); } export function connectAuthenticatedLibsignal({ @@ -353,12 +374,15 @@ export function connectAuthenticatedLibsignal({ handler: (request: IncomingWebSocketRequest) => void; receiveStories: boolean; }): AbortableProcess { - const listener = { + const logId = `LibsignalWebSocketResource(${name})`; + const listener: LibsignalWebSocketResourceHolder & ChatServiceListener = { + resource: undefined, onIncomingMessage( envelope: Buffer, timestamp: number, ack: ChatServerMessageAck ): void { + // Handle incoming messages even if we've disconnected. const request = new IncomingWebSocketRequestLibsignal( ServerRequestType.ApiMessage, envelope, @@ -368,6 +392,10 @@ export function connectAuthenticatedLibsignal({ handler(request); }, onQueueEmpty(): void { + if (!this.resource) { + logDisconnectedListenerWarn(logId, 'onQueueEmpty'); + return; + } const request = new IncomingWebSocketRequestLibsignal( ServerRequestType.ApiEmptyQueue, undefined, @@ -377,7 +405,12 @@ export function connectAuthenticatedLibsignal({ handler(request); }, onConnectionInterrupted(): void { - log.warn(`LibsignalWebSocketResource(${name}): connection interrupted`); + if (!this.resource) { + logDisconnectedListenerWarn(logId, 'onConnectionInterrupted'); + return; + } + this.resource.onConnectionInterrupted(); + this.resource = undefined; }, }; return connectLibsignal( @@ -387,33 +420,40 @@ export function connectAuthenticatedLibsignal({ receiveStories, listener ), - name + listener, + logId ); } +function logDisconnectedListenerWarn(logId: string, method: string): void { + log.warn(`${logId} received ${method}, but listener already disconnected`); +} + function connectLibsignal( chatService: Net.ChatService, - name: string + resourceHolder: LibsignalWebSocketResourceHolder, + logId: string ): AbortableProcess { const connectAsync = async () => { try { const debugInfo = await chatService.connect(); - log.info(`LibsignalWebSocketResource(${name}) connected`, debugInfo); - return new LibsignalWebSocketResource( + log.info(`${logId} connected`, debugInfo); + const resource = new LibsignalWebSocketResource( chatService, - IpVersion.fromDebugInfoCode(debugInfo.ipType) + IpVersion.fromDebugInfoCode(debugInfo.ipType), + logId ); + // eslint-disable-next-line no-param-reassign + resourceHolder.resource = resource; + return resource; } catch (error) { // Handle any errors that occur during connection - log.error( - `LibsignalWebSocketResource(${name}) connection failed`, - Errors.toLogFormat(error) - ); + log.error(`${logId} connection failed`, Errors.toLogFormat(error)); throw error; } }; return new AbortableProcess( - `LibsignalWebSocketResource.connect(${name})`, + `${logId}.connect`, { abort() { // if interrupted, trying to disconnect @@ -428,9 +468,12 @@ export class LibsignalWebSocketResource extends EventTarget implements IWebSocketResource { + closed = false; + constructor( private readonly chatService: Net.ChatService, - private readonly socketIpVersion: IpVersion | undefined + private readonly socketIpVersion: IpVersion | undefined, + private readonly logId: string ) { super(); } @@ -452,12 +495,39 @@ export class LibsignalWebSocketResource return super.addEventListener(name, handler); } - public close(_code?: number, _reason?: string): void { + public close(code = 3000, reason?: string): void { + if (this.closed) { + log.info(`${this.logId}.close: Already closed! ${code}/${reason}`); + return; + } drop(this.chatService.disconnect()); + + // 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 + // process up. + Timers.setTimeout( + () => this.onConnectionInterrupted(), + 5 * durations.SECOND + ); } public shutdown(): void { - drop(this.chatService.disconnect()); + this.close(3000, 'Shutdown'); + } + + onConnectionInterrupted(): void { + if (this.closed) { + log.warn( + `${this.logId}.onConnectionInterrupted called after resource is closed` + ); + return; + } + this.closed = true; + log.warn(`${this.logId}: connection closed`); + // TODO: DESKTOP-7519. `reason` should be eventually resolved from the + // disconnect reason error object coming from libsignal. + const reason = undefined; + this.dispatchEvent(new CloseEvent(3000, reason || 'normal')); } public forceKeepAlive(): void { @@ -627,12 +697,11 @@ export class WebSocketResourceWithShadowing implements IWebSocketResource { return; } try { - const [healthCheckResult, debugInfo] = - await this.shadowing.sendRequestGetDebugInfo({ - verb: 'GET', - path: '/v1/keepalive', - timeout: KEEPALIVE_TIMEOUT_MS, - }); + const healthCheckResult = await this.shadowing.sendRequest({ + verb: 'GET', + path: '/v1/keepalive', + timeout: KEEPALIVE_TIMEOUT_MS, + }); this.stats.requestsCompared += 1; if (!isSuccessfulStatusCode(healthCheckResult.status)) { this.stats.healthcheckBadStatus += 1; @@ -640,7 +709,6 @@ export class WebSocketResourceWithShadowing implements IWebSocketResource { `${this.logId}: keepalive via libsignal responded with status [${healthCheckResult.status}]` ); } - this.stats.unexpectedReconnects = debugInfo.reconnectCount; } catch (error) { this.stats.healthcheckFailures += 1; log.warn( diff --git a/ts/windows/main/phase1-ipc.ts b/ts/windows/main/phase1-ipc.ts index 45577c6721..ff64a98933 100644 --- a/ts/windows/main/phase1-ipc.ts +++ b/ts/windows/main/phase1-ipc.ts @@ -180,7 +180,6 @@ type NetworkStatistics = { unauthorizedRequestsCompared?: string; unauthorizedHealthcheckFailures?: string; unauthorizedHealthcheckBadStatus?: string; - unauthorizedUnexpectedReconnects?: string; unauthorizedIpVersionMismatches?: string; }; @@ -220,9 +219,6 @@ ipc.on('additional-log-data-request', async event => { unauthorizedHealthcheckBadStatus: formatCountForLogging( unauthorizedStats.healthcheckBadStatus ), - unauthorizedUnexpectedReconnects: formatCountForLogging( - unauthorizedStats.unexpectedReconnects - ), unauthorizedIpVersionMismatches: formatCountForLogging( unauthorizedStats.ipVersionMismatches ),