mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-02-14 23:18:54 +00:00
Switch provisioning to libsignal
This commit is contained in:
@@ -77,7 +77,6 @@ const NODE_PACKAGES = new Set([
|
||||
'proxy-agent',
|
||||
'read-last-lines',
|
||||
'split2',
|
||||
'websocket',
|
||||
'write-file-atomic',
|
||||
|
||||
// Dev dependencies
|
||||
|
||||
@@ -5873,185 +5873,6 @@ Signal Desktop makes use of the following open source projects.
|
||||
|
||||
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.
|
||||
|
||||
## websocket
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
## write-file-atomic
|
||||
|
||||
Copyright (c) 2015, Rebecca Turner
|
||||
|
||||
12
package.json
12
package.json
@@ -133,7 +133,7 @@
|
||||
"@react-aria/utils": "3.25.3",
|
||||
"@react-spring/web": "10.0.3",
|
||||
"@react-types/shared": "3.27.0",
|
||||
"@signalapp/libsignal-client": "0.86.12",
|
||||
"@signalapp/libsignal-client": "0.86.16",
|
||||
"@signalapp/minimask": "1.0.1",
|
||||
"@signalapp/mute-state-change": "workspace:1.0.0",
|
||||
"@signalapp/quill-cjs": "2.1.2",
|
||||
@@ -220,7 +220,6 @@
|
||||
"url": "0.11.4",
|
||||
"urlpattern-polyfill": "10.0.0",
|
||||
"uuid": "11.0.2",
|
||||
"websocket": "1.0.34",
|
||||
"write-file-atomic": "6.0.0",
|
||||
"zod": "3.23.8"
|
||||
},
|
||||
@@ -292,7 +291,6 @@
|
||||
"@types/sinon": "17.0.3",
|
||||
"@types/split2": "4.2.3",
|
||||
"@types/uuid": "10.0.0",
|
||||
"@types/websocket": "1.0.0",
|
||||
"@types/write-file-atomic": "4.0.3",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||
@@ -394,8 +392,6 @@
|
||||
"@vitest/expect@2.0.5": "patches/@vitest+expect+2.0.5.patch",
|
||||
"got@11.8.5": "patches/got+11.8.5.patch",
|
||||
"growing-file@0.1.3": "patches/growing-file+0.1.3.patch",
|
||||
"websocket@1.0.34": "patches/websocket+1.0.34.patch",
|
||||
"@types/websocket@1.0.0": "patches/@types+websocket+1.0.0.patch",
|
||||
"node-fetch@2.6.7": "patches/node-fetch+2.6.7.patch",
|
||||
"zod@3.23.8": "patches/zod+3.23.8.patch",
|
||||
"app-builder-lib": "patches/app-builder-lib.patch",
|
||||
@@ -413,11 +409,9 @@
|
||||
"@signalapp/windows-ucv",
|
||||
"@swc/core",
|
||||
"@tailwindcss/oxide",
|
||||
"bufferutil",
|
||||
"electron",
|
||||
"esbuild",
|
||||
"fs-xattr",
|
||||
"utf-8-validate"
|
||||
"fs-xattr"
|
||||
],
|
||||
"ignoredBuiltDependencies": [
|
||||
"core-js",
|
||||
@@ -664,8 +658,6 @@
|
||||
"!**/*.{o,hprof,orig,pyc,pyo,rbc,c,h,m}",
|
||||
"!**/._*",
|
||||
"!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,__pycache__,thumbs.db,.gitignore,.gitattributes,.flowconfig,.yarn-metadata.json,.idea,appveyor.yml,.travis.yml,circle.yml,npm-debug.log,.nyc_output,yarn.lock,.yarn-integrity}",
|
||||
"node_modules/websocket/build/Release/*.node",
|
||||
"!node_modules/websocket/builderror.log",
|
||||
"node_modules/socks/build/*.js",
|
||||
"node_modules/socks/build/common/*.js",
|
||||
"node_modules/socks/build/client/*.js",
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
diff --git a/index.d.ts b/index.d.ts
|
||||
index eb23f3c479e6d368e1563de7531a8459566d5c56..146769d33ce61f036371c05743cd31fb44d25d49 100644
|
||||
--- a/index.d.ts
|
||||
+++ b/index.d.ts
|
||||
@@ -650,9 +650,11 @@ export class client extends events.EventEmitter {
|
||||
on(event: 'connect', cb: (connection: connection) => void): this;
|
||||
on(event: 'connectFailed', cb: (err: Error) => void): this;
|
||||
on(event: 'httpResponse', cb: (response: http.IncomingMessage, client: client) => void): this;
|
||||
+ on(event: 'upgradeResponse', cb: (response: http.IncomingMessage) => void): this;
|
||||
addListener(event: 'connect', cb: (connection: connection) => void): this;
|
||||
addListener(event: 'connectFailed', cb: (err: Error) => void): this;
|
||||
addListener(event: 'httpResponse', cb: (response: http.IncomingMessage, client: client) => void): this;
|
||||
+ addListener(event: 'upgradeResponse', cb: (response: http.IncomingMessage) => void): this;
|
||||
}
|
||||
|
||||
export interface IRouterRequest extends events.EventEmitter {
|
||||
@@ -1,63 +0,0 @@
|
||||
diff --git a/lib/WebSocketClient.js b/lib/WebSocketClient.js
|
||||
index fb7572b382de289f7fa3ff35a9647f118f3bd4eb..0000a94607520b4b7ac71f5eee4ed297df8d1bf7 100644
|
||||
--- a/lib/WebSocketClient.js
|
||||
+++ b/lib/WebSocketClient.js
|
||||
@@ -258,6 +258,7 @@ WebSocketClient.prototype.connect = function(requestUrl, protocols, origin, head
|
||||
self.socket = socket;
|
||||
self.response = response;
|
||||
self.firstDataChunk = head;
|
||||
+ self.emit('upgradeResponse', response);
|
||||
self.validateHandshake();
|
||||
});
|
||||
req.on('error', handleRequestError);
|
||||
diff --git a/lib/WebSocketConnection.js b/lib/WebSocketConnection.js
|
||||
index 219de631dabe578e64574732fc95353a1c1047fa..93d380004b5bc58ba9895fb5a760709389641071 100644
|
||||
--- a/lib/WebSocketConnection.js
|
||||
+++ b/lib/WebSocketConnection.js
|
||||
@@ -271,7 +271,7 @@ WebSocketConnection.prototype.handleSocketData = function(data) {
|
||||
this.processReceivedData();
|
||||
};
|
||||
|
||||
-WebSocketConnection.prototype.processReceivedData = function() {
|
||||
+WebSocketConnection.prototype.processReceivedData = function(isSync = false) {
|
||||
this._debug('processReceivedData');
|
||||
// If we're not connected, we should ignore any data remaining on the buffer.
|
||||
if (!this.connected) { return; }
|
||||
@@ -320,7 +320,11 @@ WebSocketConnection.prototype.processReceivedData = function() {
|
||||
process.nextTick(function() { self.emit('frame', frame); });
|
||||
}
|
||||
|
||||
- process.nextTick(function() { self.processFrame(frame); });
|
||||
+ if (isSync) {
|
||||
+ self.processFrame(frame);
|
||||
+ } else {
|
||||
+ process.nextTick(function() { self.processFrame(frame); });
|
||||
+ }
|
||||
|
||||
this.currentFrame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
|
||||
|
||||
@@ -329,7 +333,11 @@ WebSocketConnection.prototype.processReceivedData = function() {
|
||||
// processed. We use setImmediate here instead of process.nextTick to
|
||||
// explicitly indicate that we wish for other I/O to be handled first.
|
||||
if (this.bufferList.length > 0) {
|
||||
- setImmediateImpl(this.receivedDataHandler);
|
||||
+ if (isSync) {
|
||||
+ this.receivedDataHandler();
|
||||
+ } else {
|
||||
+ setImmediateImpl(this.receivedDataHandler);
|
||||
+ }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -353,6 +361,11 @@ WebSocketConnection.prototype.handleSocketError = function(error) {
|
||||
};
|
||||
|
||||
WebSocketConnection.prototype.handleSocketEnd = function() {
|
||||
+ // We might have socket data scheduled for a next tick, process it now.
|
||||
+ if (this.bufferList.length > 0) {
|
||||
+ this.receivedDataHandler(true);
|
||||
+ }
|
||||
+
|
||||
this._debug('handleSocketEnd: received socket end. state = %s', this.state);
|
||||
this.receivedEnd = true;
|
||||
if (this.state === STATE_CLOSED) {
|
||||
53
pnpm-lock.yaml
generated
53
pnpm-lock.yaml
generated
@@ -24,9 +24,6 @@ patchedDependencies:
|
||||
'@types/node-fetch@2.6.12':
|
||||
hash: bc43fb8cfed85fb4f7917b5bd3b47644ae15a000c26a6fe23078b2f5217efaf0
|
||||
path: patches/@types+node-fetch+2.6.12.patch
|
||||
'@types/websocket@1.0.0':
|
||||
hash: 53fdd65a6b35f379eb8f5807d315f00c66e9bfe9741f4859a0fa21026bdb30fa
|
||||
path: patches/@types+websocket+1.0.0.patch
|
||||
'@vitest/expect@2.0.5':
|
||||
hash: e8a96f71e52bf903c9f1eadba4740489a0beb48da33db52354adca484fe1f495
|
||||
path: patches/@vitest+expect+2.0.5.patch
|
||||
@@ -66,9 +63,6 @@ patchedDependencies:
|
||||
qrcode-generator@1.4.4:
|
||||
hash: 1f10c592d849ed4cfc9f81301196d39857b79240997ef5772138218cb3717e80
|
||||
path: patches/qrcode-generator+1.4.4.patch
|
||||
websocket@1.0.34:
|
||||
hash: b8d361a6a73e44000bb51102dea0d841c22d2bb455dd6c54de566d0e0bd86355
|
||||
path: patches/websocket+1.0.34.patch
|
||||
zod@3.23.8:
|
||||
hash: 239818e5d88990616205c8cdc1de1660bf5e18b157d00c4a5f726dde6094af4d
|
||||
path: patches/zod+3.23.8.patch
|
||||
@@ -129,8 +123,8 @@ importers:
|
||||
specifier: 3.27.0
|
||||
version: 3.27.0(react@18.3.1)
|
||||
'@signalapp/libsignal-client':
|
||||
specifier: 0.86.12
|
||||
version: 0.86.12
|
||||
specifier: 0.86.16
|
||||
version: 0.86.16
|
||||
'@signalapp/minimask':
|
||||
specifier: 1.0.1
|
||||
version: 1.0.1
|
||||
@@ -389,9 +383,6 @@ importers:
|
||||
uuid:
|
||||
specifier: 11.0.2
|
||||
version: 11.0.2
|
||||
websocket:
|
||||
specifier: 1.0.34
|
||||
version: 1.0.34(patch_hash=b8d361a6a73e44000bb51102dea0d841c22d2bb455dd6c54de566d0e0bd86355)
|
||||
write-file-atomic:
|
||||
specifier: 6.0.0
|
||||
version: 6.0.0
|
||||
@@ -600,9 +591,6 @@ importers:
|
||||
'@types/uuid':
|
||||
specifier: 10.0.0
|
||||
version: 10.0.0
|
||||
'@types/websocket':
|
||||
specifier: 1.0.0
|
||||
version: 1.0.0(patch_hash=53fdd65a6b35f379eb8f5807d315f00c66e9bfe9741f4859a0fa21026bdb30fa)
|
||||
'@types/write-file-atomic':
|
||||
specifier: 4.0.3
|
||||
version: 4.0.3
|
||||
@@ -3495,8 +3483,8 @@ packages:
|
||||
'@signalapp/libsignal-client@0.76.7':
|
||||
resolution: {integrity: sha512-iGWTlFkko7IKlm96Iy91Wz5sIN089nj02ifOk6BWtLzeVi0kFaNj+jK26Sl1JRXy/VfXevcYtiOivOg43BPqpg==}
|
||||
|
||||
'@signalapp/libsignal-client@0.86.12':
|
||||
resolution: {integrity: sha512-GJYT0uSfDv6hsu4uqFlUK/pS43/mCJ86rRODS8RNDdQ1XioGyFE0JT4etHF27f5n+xNkpd4XKYhiquZfxazOgA==}
|
||||
'@signalapp/libsignal-client@0.86.16':
|
||||
resolution: {integrity: sha512-oQqslL2GM+1Mr9uhSqQjKtBcxdP5ngOG+L17K7ohD01uD5IHwgzc3hH1W2pt0D8VN2cIsBhemNqRgtNgVR3prA==}
|
||||
|
||||
'@signalapp/minimask@1.0.1':
|
||||
resolution: {integrity: sha512-QAwo0joA60urTNbW9RIz6vLKQjy+jdVtH7cvY0wD9PVooD46MAjE40MLssp4xUJrph91n2XvtJ3pbEUDrmT2AA==}
|
||||
@@ -4260,9 +4248,6 @@ packages:
|
||||
'@types/wait-on@5.3.4':
|
||||
resolution: {integrity: sha512-EBsPjFMrFlMbbUFf9D1Fp+PAB2TwmUn7a3YtHyD9RLuTIk1jDd8SxXVAoez2Ciy+8Jsceo2MYEYZzJ/DvorOKw==}
|
||||
|
||||
'@types/websocket@1.0.0':
|
||||
resolution: {integrity: sha512-MLr8hDM8y7vvdAdnoDEP5LotRoYJj7wgT6mWzCUQH/gHqzS4qcnOT/K4dhC0WimWIUiA3Arj9QAJGGKNRiRZKA==}
|
||||
|
||||
'@types/write-file-atomic@4.0.3':
|
||||
resolution: {integrity: sha512-qdo+vZRchyJIHNeuI1nrpsLw+hnkgqP/8mlaN6Wle/NKhydHmUN9l4p3ZE8yP90AJNJW4uB8HQhedb4f1vNayQ==}
|
||||
|
||||
@@ -10711,10 +10696,6 @@ packages:
|
||||
resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
|
||||
websocket@1.0.34:
|
||||
resolution: {integrity: sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
whatwg-encoding@2.0.0:
|
||||
resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -10867,11 +10848,6 @@ packages:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
yaeti@0.0.6:
|
||||
resolution: {integrity: sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==}
|
||||
engines: {node: '>=0.10.32'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
|
||||
yallist@2.1.2:
|
||||
resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==}
|
||||
|
||||
@@ -14288,7 +14264,7 @@ snapshots:
|
||||
type-fest: 4.26.1
|
||||
uuid: 11.0.2
|
||||
|
||||
'@signalapp/libsignal-client@0.86.12':
|
||||
'@signalapp/libsignal-client@0.86.16':
|
||||
dependencies:
|
||||
node-gyp-build: 4.8.4
|
||||
type-fest: 4.26.1
|
||||
@@ -15226,10 +15202,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 20.17.6
|
||||
|
||||
'@types/websocket@1.0.0(patch_hash=53fdd65a6b35f379eb8f5807d315f00c66e9bfe9741f4859a0fa21026bdb30fa)':
|
||||
dependencies:
|
||||
'@types/node': 20.17.6
|
||||
|
||||
'@types/write-file-atomic@4.0.3':
|
||||
dependencies:
|
||||
'@types/node': 20.17.6
|
||||
@@ -16083,6 +16055,7 @@ snapshots:
|
||||
bufferutil@4.0.9:
|
||||
dependencies:
|
||||
node-gyp-build: 4.8.4
|
||||
optional: true
|
||||
|
||||
builder-util-runtime@9.3.2:
|
||||
dependencies:
|
||||
@@ -22785,6 +22758,7 @@ snapshots:
|
||||
utf-8-validate@5.0.10:
|
||||
dependencies:
|
||||
node-gyp-build: 4.8.4
|
||||
optional: true
|
||||
|
||||
utf8-byte-length@1.0.5: {}
|
||||
|
||||
@@ -23028,17 +23002,6 @@ snapshots:
|
||||
|
||||
websocket-extensions@0.1.4: {}
|
||||
|
||||
websocket@1.0.34(patch_hash=b8d361a6a73e44000bb51102dea0d841c22d2bb455dd6c54de566d0e0bd86355):
|
||||
dependencies:
|
||||
bufferutil: 4.0.9
|
||||
debug: 2.6.9
|
||||
es5-ext: 0.10.64
|
||||
typedarray-to-buffer: 3.1.5
|
||||
utf-8-validate: 5.0.10
|
||||
yaeti: 0.0.6
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
whatwg-encoding@2.0.0:
|
||||
dependencies:
|
||||
iconv-lite: 0.6.3
|
||||
@@ -23206,8 +23169,6 @@ snapshots:
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
yaeti@0.0.6: {}
|
||||
|
||||
yallist@2.1.2: {}
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
@@ -73,15 +73,12 @@ const bundleDefaults = {
|
||||
'fsevents',
|
||||
'mac-screen-capture-permissions',
|
||||
'sass',
|
||||
'bufferutil',
|
||||
'utf-8-validate',
|
||||
|
||||
// Things that don't bundle well
|
||||
'got',
|
||||
'node-fetch',
|
||||
'pino',
|
||||
'proxy-agent',
|
||||
'websocket',
|
||||
|
||||
// Large libraries (3.7mb total)
|
||||
// See: https://esbuild.github.io/api/#analyze
|
||||
|
||||
@@ -84,9 +84,7 @@ const ScalarKeys = [
|
||||
// These keys should always match those in Net.REMOTE_CONFIG_KEYS, prefixed by
|
||||
// `desktop.libsignalNet`
|
||||
const KnownDesktopLibsignalNetKeys = [
|
||||
'desktop.libsignalNet.chatPermessageDeflate.beta',
|
||||
'desktop.libsignalNet.chatPermessageDeflate.prod',
|
||||
'desktop.libsignalNet.chatPermessageDeflate',
|
||||
'desktop.libsignalNet.chatRequestConnectionCheckTimeoutMillis.beta',
|
||||
'desktop.libsignalNet.chatRequestConnectionCheckTimeoutMillis',
|
||||
'desktop.libsignalNet.disableNagleAlgorithm.beta',
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
type EnvelopeType as ProvisionEnvelopeType,
|
||||
} from '../../textsecure/Provisioner.preload.js';
|
||||
import { accountManager } from '../../textsecure/AccountManager.preload.js';
|
||||
import { getProvisioningResource } from '../../textsecure/WebAPI.preload.js';
|
||||
import { getProvisioningConnection } from '../../textsecure/WebAPI.preload.js';
|
||||
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions.std.js';
|
||||
import { useBoundActions } from '../../hooks/useBoundActions.std.js';
|
||||
import { createLogger } from '../../logging/log.std.js';
|
||||
@@ -179,7 +179,7 @@ function startInstaller(): ThunkAction<
|
||||
if (!provisioner) {
|
||||
provisioner = new Provisioner({
|
||||
server: {
|
||||
getProvisioningResource,
|
||||
getProvisioningConnection,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// Copyright 2015 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
|
||||
import { assert } from 'chai';
|
||||
import Long from 'long';
|
||||
|
||||
import MessageReceiver from '../textsecure/MessageReceiver.preload.js';
|
||||
import { IncomingWebSocketRequestLegacy } from '../textsecure/WebsocketResources.preload.js';
|
||||
import {
|
||||
IncomingWebSocketRequest,
|
||||
ServerRequestType,
|
||||
} from '../textsecure/WebsocketResources.preload.js';
|
||||
import type { DecryptionErrorEvent } from '../textsecure/messageReceiverEvents.std.js';
|
||||
import { generateAci } from '../types/ServiceId.std.js';
|
||||
import type { AciString } from '../types/ServiceId.std.js';
|
||||
@@ -58,15 +59,15 @@ describe('MessageReceiver', () => {
|
||||
}).finish();
|
||||
|
||||
messageReceiver.handleRequest(
|
||||
new IncomingWebSocketRequestLegacy(
|
||||
new IncomingWebSocketRequest(
|
||||
ServerRequestType.ApiMessage,
|
||||
body,
|
||||
Date.now(),
|
||||
{
|
||||
id: Long.fromNumber(1),
|
||||
verb: 'PUT',
|
||||
path: '/api/v1/message',
|
||||
body,
|
||||
headers: [],
|
||||
},
|
||||
(_: Buffer): void => {}
|
||||
async send() {
|
||||
// no-op
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -1,250 +0,0 @@
|
||||
// Copyright 2015 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable
|
||||
no-new,
|
||||
@typescript-eslint/no-empty-function,
|
||||
@typescript-eslint/no-explicit-any
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import EventEmitter from 'node:events';
|
||||
import type { connection as WebSocket } from 'websocket';
|
||||
import Long from 'long';
|
||||
|
||||
import { dropNull } from '../util/dropNull.std.js';
|
||||
import { SignalService as Proto } from '../protobuf/index.std.js';
|
||||
|
||||
import WebSocketResource, {
|
||||
ServerRequestType,
|
||||
} from '../textsecure/WebsocketResources.preload.js';
|
||||
|
||||
describe('WebSocket-Resource', () => {
|
||||
class FakeSocket extends EventEmitter {
|
||||
public sendBytes(_: Uint8Array) {}
|
||||
public socket = {
|
||||
localPort: 5678,
|
||||
};
|
||||
public close() {}
|
||||
}
|
||||
|
||||
const NOW = Date.now();
|
||||
|
||||
beforeEach(function (this: Mocha.Context) {
|
||||
this.sandbox = sinon.createSandbox();
|
||||
this.clock = this.sandbox.useFakeTimers({
|
||||
now: NOW,
|
||||
});
|
||||
this.sandbox
|
||||
.stub(window.SignalContext.timers, 'setTimeout')
|
||||
.callsFake(setTimeout);
|
||||
this.sandbox
|
||||
.stub(window.SignalContext.timers, 'clearTimeout')
|
||||
.callsFake(clearTimeout);
|
||||
});
|
||||
|
||||
afterEach(function (this: Mocha.Context) {
|
||||
this.sandbox.restore();
|
||||
});
|
||||
|
||||
describe('requests and responses', () => {
|
||||
it('receives requests and sends responses', done => {
|
||||
// mock socket
|
||||
const requestId = new Long(0xdeadbeef, 0x7fffffff);
|
||||
const socket = new FakeSocket();
|
||||
|
||||
sinon.stub(socket, 'sendBytes').callsFake((data: Uint8Array) => {
|
||||
const message = Proto.WebSocketMessage.decode(data);
|
||||
assert.strictEqual(message.type, Proto.WebSocketMessage.Type.RESPONSE);
|
||||
assert.strictEqual(message.response?.message, 'OK');
|
||||
assert.strictEqual(message.response?.status, 200);
|
||||
const id = message.response?.id;
|
||||
|
||||
if (Long.isLong(id)) {
|
||||
assert(id.equals(requestId));
|
||||
} else {
|
||||
assert(false, `id should be Long, got ${id}`);
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
// actual test
|
||||
new WebSocketResource(socket as WebSocket, {
|
||||
name: 'test',
|
||||
handleRequest(request: any) {
|
||||
assert.strictEqual(request.requestType, ServerRequestType.ApiMessage);
|
||||
assert.deepEqual(request.body, new Uint8Array([1, 2, 3]));
|
||||
request.respond(200, 'OK');
|
||||
},
|
||||
});
|
||||
|
||||
// mock socket request
|
||||
socket.emit('message', {
|
||||
type: 'binary',
|
||||
binaryData: Proto.WebSocketMessage.encode({
|
||||
type: Proto.WebSocketMessage.Type.REQUEST,
|
||||
request: {
|
||||
id: requestId,
|
||||
verb: 'PUT',
|
||||
path: ServerRequestType.ApiMessage.toString(),
|
||||
body: new Uint8Array([1, 2, 3]),
|
||||
},
|
||||
}).finish(),
|
||||
});
|
||||
});
|
||||
|
||||
it('sends requests and receives responses', async () => {
|
||||
// mock socket and request handler
|
||||
let requestId: Long | undefined;
|
||||
const socket = new FakeSocket();
|
||||
|
||||
sinon.stub(socket, 'sendBytes').callsFake((data: Uint8Array) => {
|
||||
const message = Proto.WebSocketMessage.decode(data);
|
||||
assert.strictEqual(message.type, Proto.WebSocketMessage.Type.REQUEST);
|
||||
assert.strictEqual(message.request?.verb, 'PUT');
|
||||
assert.strictEqual(message.request?.path, '/some/path');
|
||||
assert.deepEqual(message.request?.body, new Uint8Array([1, 2, 3]));
|
||||
requestId = dropNull(message.request?.id);
|
||||
});
|
||||
|
||||
// actual test
|
||||
const resource = new WebSocketResource(socket as WebSocket, {
|
||||
name: 'test',
|
||||
});
|
||||
const promise = resource.sendRequest({
|
||||
verb: 'PUT',
|
||||
path: '/some/path',
|
||||
body: new Uint8Array([1, 2, 3]),
|
||||
});
|
||||
|
||||
// mock socket response
|
||||
socket.emit('message', {
|
||||
type: 'binary',
|
||||
binaryData: Proto.WebSocketMessage.encode({
|
||||
type: Proto.WebSocketMessage.Type.RESPONSE,
|
||||
response: { id: requestId, message: 'OK', status: 200 },
|
||||
}).finish(),
|
||||
});
|
||||
|
||||
const response = await promise;
|
||||
assert.strictEqual(response.statusText, 'OK');
|
||||
assert.strictEqual(response.status, 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('close', () => {
|
||||
it('closes the connection', done => {
|
||||
const socket = new FakeSocket();
|
||||
|
||||
sinon.stub(socket, 'close').callsFake(() => done());
|
||||
|
||||
const resource = new WebSocketResource(socket as WebSocket, {
|
||||
name: 'test',
|
||||
});
|
||||
resource.close();
|
||||
});
|
||||
|
||||
it('force closes the connection', function (this: Mocha.Context, done) {
|
||||
const socket = new FakeSocket();
|
||||
|
||||
const resource = new WebSocketResource(socket as WebSocket, {
|
||||
name: 'test',
|
||||
});
|
||||
resource.close();
|
||||
|
||||
resource.addEventListener('close', () => done());
|
||||
|
||||
// Wait 5 seconds to forcefully close the connection
|
||||
this.clock.next();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a keepalive config', () => {
|
||||
it('sends keepalives once a minute', function (this: Mocha.Context, done) {
|
||||
const socket = new FakeSocket();
|
||||
|
||||
sinon.stub(socket, 'sendBytes').callsFake(data => {
|
||||
const message = Proto.WebSocketMessage.decode(data);
|
||||
assert.strictEqual(message.type, Proto.WebSocketMessage.Type.REQUEST);
|
||||
assert.strictEqual(message.request?.verb, 'GET');
|
||||
assert.strictEqual(message.request?.path, '/v1/keepalive');
|
||||
done();
|
||||
});
|
||||
|
||||
new WebSocketResource(socket as WebSocket, {
|
||||
name: 'test',
|
||||
keepalive: { path: '/v1/keepalive' },
|
||||
});
|
||||
|
||||
this.clock.next();
|
||||
});
|
||||
|
||||
it('optionally disconnects if no response', function (this: Mocha.Context, done) {
|
||||
const socket = new FakeSocket();
|
||||
|
||||
sinon.stub(socket, 'close').callsFake(() => done());
|
||||
|
||||
new WebSocketResource(socket as WebSocket, {
|
||||
name: 'test',
|
||||
keepalive: { path: '/' },
|
||||
});
|
||||
|
||||
// One to trigger send
|
||||
this.clock.next();
|
||||
|
||||
// Another to trigger send timeout
|
||||
this.clock.next();
|
||||
});
|
||||
|
||||
it('optionally disconnects if suspended', function (this: Mocha.Context, done) {
|
||||
const socket = new FakeSocket();
|
||||
|
||||
sinon.stub(socket, 'close').callsFake(() => done());
|
||||
|
||||
new WebSocketResource(socket as WebSocket, {
|
||||
name: 'test',
|
||||
keepalive: { path: '/' },
|
||||
});
|
||||
|
||||
// Just skip one hour immediately
|
||||
this.clock.setSystemTime(NOW + 3600 * 1000);
|
||||
this.clock.next();
|
||||
});
|
||||
|
||||
it('allows resetting the keepalive timer', function (this: Mocha.Context, done) {
|
||||
const startTime = Date.now();
|
||||
|
||||
const socket = new FakeSocket();
|
||||
|
||||
sinon.stub(socket, 'sendBytes').callsFake(data => {
|
||||
const message = Proto.WebSocketMessage.decode(data);
|
||||
assert.strictEqual(message.type, Proto.WebSocketMessage.Type.REQUEST);
|
||||
assert.strictEqual(message.request?.verb, 'GET');
|
||||
assert.strictEqual(message.request?.path, '/');
|
||||
assert.strictEqual(
|
||||
Date.now(),
|
||||
startTime + 30000 + 5000,
|
||||
'keepalive time should be 35s'
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
const resource = new WebSocketResource(socket as WebSocket, {
|
||||
name: 'test',
|
||||
keepalive: { path: '/' },
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
resource.keepalive?.reset();
|
||||
}, 5000);
|
||||
|
||||
// Trigger setTimeout above
|
||||
this.clock.next();
|
||||
|
||||
// Trigger sendBytes
|
||||
this.clock.next();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import pTimeout, { TimeoutError as PTimeoutError } from 'p-timeout';
|
||||
import type { LibSignalError } from '@signalapp/libsignal-client';
|
||||
import type { ProvisioningConnection } from '@signalapp/libsignal-client/dist/net/Chat.js';
|
||||
|
||||
import { createLogger } from '../logging/log.std.js';
|
||||
import * as Errors from '../types/errors.std.js';
|
||||
@@ -25,13 +27,8 @@ import {
|
||||
import ProvisioningCipher, {
|
||||
type ProvisionDecryptResult,
|
||||
} from './ProvisioningCipher.node.js';
|
||||
import {
|
||||
type IWebSocketResource,
|
||||
type IncomingWebSocketRequest,
|
||||
ServerRequestType,
|
||||
} from './WebsocketResources.preload.js';
|
||||
import { ConnectTimeoutError } from './Errors.std.js';
|
||||
import type { getProvisioningResource } from './WebAPI.preload.js';
|
||||
import type { getProvisioningConnection } from './WebAPI.preload.js';
|
||||
|
||||
const log = createLogger('Provisioner');
|
||||
|
||||
@@ -45,7 +42,7 @@ export enum EventKind {
|
||||
}
|
||||
|
||||
export type ProvisionerOptionsType = Readonly<{
|
||||
server: { getProvisioningResource: typeof getProvisioningResource };
|
||||
server: { getProvisioningConnection: typeof getProvisioningConnection };
|
||||
}>;
|
||||
|
||||
export type EnvelopeType = ProvisionDecryptResult;
|
||||
@@ -106,10 +103,12 @@ const QR_CODE_TIMEOUTS = [10 * SECOND, 20 * SECOND, 30 * SECOND, 60 * SECOND];
|
||||
|
||||
export class Provisioner {
|
||||
readonly #subscribers = new Set<SubscriberType>();
|
||||
readonly #server: { getProvisioningResource: typeof getProvisioningResource };
|
||||
readonly #server: {
|
||||
getProvisioningConnection: typeof getProvisioningConnection;
|
||||
};
|
||||
readonly #retryBackOff = new BackOff(FIBONACCI_TIMEOUTS);
|
||||
|
||||
#sockets: Array<IWebSocketResource> = [];
|
||||
#sockets: Array<ProvisioningConnection> = [];
|
||||
#abortController: AbortController | undefined;
|
||||
#attemptCount = 0;
|
||||
#isRunning = false;
|
||||
@@ -326,60 +325,43 @@ export class Provisioner {
|
||||
|
||||
const timeoutAt = Date.now() + timeout;
|
||||
|
||||
const resource = await this.#server.getProvisioningResource(
|
||||
const connection = await this.#server.getProvisioningConnection(
|
||||
{
|
||||
handleRequest: (request: IncomingWebSocketRequest) => {
|
||||
const { requestType, body } = request;
|
||||
if (!body) {
|
||||
log.warn('connect: no request body');
|
||||
request.respond(400, 'Missing body');
|
||||
onReceivedAddress: (address, ack) => {
|
||||
if (state !== SocketState.WaitingForUuid) {
|
||||
log.error('onReceivedAddress: duplicate uuid');
|
||||
drop(connection.disconnect());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (requestType === ServerRequestType.ProvisioningAddress) {
|
||||
strictAssert(
|
||||
state === SocketState.WaitingForUuid,
|
||||
'Provisioner.connect: duplicate uuid'
|
||||
);
|
||||
|
||||
const proto = Proto.ProvisioningAddress.decode(body);
|
||||
strictAssert(
|
||||
proto.address,
|
||||
'Provisioner.connect: expected a UUID'
|
||||
);
|
||||
|
||||
state = SocketState.WaitingForEnvelope;
|
||||
uuidPromise.resolve(proto.address);
|
||||
request.respond(200, 'OK');
|
||||
} else if (requestType === ServerRequestType.ProvisioningMessage) {
|
||||
strictAssert(
|
||||
state === SocketState.WaitingForEnvelope,
|
||||
'Provisioner.connect: duplicate envelope or not ready'
|
||||
);
|
||||
|
||||
const ciphertext = Proto.ProvisionEnvelope.decode(body);
|
||||
const envelope = cipher.decrypt(ciphertext);
|
||||
|
||||
state = SocketState.Done;
|
||||
this.#notify({
|
||||
kind: EventKind.Envelope,
|
||||
envelope,
|
||||
isLinkAndSync:
|
||||
isLinkAndSyncEnabled() &&
|
||||
Bytes.isNotEmpty(envelope.ephemeralBackupKey),
|
||||
});
|
||||
} else {
|
||||
log.warn('connect: unsupported request type', requestType);
|
||||
request.respond(404, 'Unsupported');
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('connect: error', Errors.toLogFormat(error));
|
||||
resource.close();
|
||||
}
|
||||
state = SocketState.WaitingForEnvelope;
|
||||
uuidPromise.resolve(address);
|
||||
ack.send(200);
|
||||
},
|
||||
handleDisconnect() {
|
||||
// No-op
|
||||
onReceivedEnvelope: (body, ack) => {
|
||||
if (state !== SocketState.WaitingForEnvelope) {
|
||||
log.error('onReceivedEnvelope: duplicate envelope or not ready');
|
||||
drop(connection.disconnect());
|
||||
return;
|
||||
}
|
||||
|
||||
const ciphertext = Proto.ProvisionEnvelope.decode(body);
|
||||
const envelope = cipher.decrypt(ciphertext);
|
||||
|
||||
state = SocketState.Done;
|
||||
this.#notify({
|
||||
kind: EventKind.Envelope,
|
||||
envelope,
|
||||
isLinkAndSync:
|
||||
isLinkAndSyncEnabled() &&
|
||||
Bytes.isNotEmpty(envelope.ephemeralBackupKey),
|
||||
});
|
||||
ack.send(200);
|
||||
},
|
||||
|
||||
onConnectionInterrupted: (cause: LibSignalError | null) => {
|
||||
signal.removeEventListener('abort', onAbort);
|
||||
this.#handleClose(connection, state, cause);
|
||||
},
|
||||
},
|
||||
timeout
|
||||
@@ -392,16 +374,11 @@ export class Provisioner {
|
||||
// Setup listeners on the socket
|
||||
|
||||
const onAbort = () => {
|
||||
resource.close();
|
||||
drop(connection.disconnect());
|
||||
uuidPromise.reject(new Error('aborted'));
|
||||
};
|
||||
signal.addEventListener('abort', onAbort);
|
||||
|
||||
resource.addEventListener('close', ({ code, reason }) => {
|
||||
signal.removeEventListener('abort', onAbort);
|
||||
this.#handleClose(resource, state, code, reason);
|
||||
});
|
||||
|
||||
// But only register it once we get the uuid from server back.
|
||||
|
||||
const uuid = await pTimeout(
|
||||
@@ -420,28 +397,28 @@ export class Provisioner {
|
||||
|
||||
this.#notify({ kind: EventKind.URL, url });
|
||||
|
||||
this.#sockets.push(resource);
|
||||
this.#sockets.push(connection);
|
||||
|
||||
while (this.#sockets.length > MAX_OPEN_SOCKETS) {
|
||||
log.info('closing extra socket');
|
||||
this.#sockets.shift()?.close();
|
||||
drop(this.#sockets.shift()?.disconnect());
|
||||
}
|
||||
}
|
||||
|
||||
#handleClose(
|
||||
resource: IWebSocketResource,
|
||||
connection: ProvisioningConnection,
|
||||
state: SocketState,
|
||||
code: number,
|
||||
reason: string
|
||||
cause: LibSignalError | null
|
||||
): void {
|
||||
const index = this.#sockets.indexOf(resource);
|
||||
const reason = cause && Errors.toLogFormat(cause);
|
||||
const index = this.#sockets.indexOf(connection);
|
||||
if (index === -1) {
|
||||
log.info(`ignoring socket closed, code=${code}, reason=${reason}`);
|
||||
log.info(`ignoring socket closed, reason=${reason}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const logId = `Provisioner.#handleClose(${index})`;
|
||||
log.info(`${logId}: closed, code=${code}, reason=${reason}`);
|
||||
const logId = `Provisioner.#handleClose(${index}): reason=${reason}`;
|
||||
log.info(`${logId}: closed`);
|
||||
|
||||
// Is URL from the socket displayed as a QR code?
|
||||
const isActive = index === this.#sockets.length - 1;
|
||||
@@ -460,9 +437,7 @@ export class Provisioner {
|
||||
state === SocketState.WaitingForUuid
|
||||
? EventKind.ConnectError
|
||||
: EventKind.EnvelopeError,
|
||||
error: new Error(
|
||||
`Socket ${index} closed, code=${code}, reason=${reason}`
|
||||
),
|
||||
error: new Error(`Socket ${index} closed, reason=${reason}`),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,14 @@ import {
|
||||
import URL from 'node:url';
|
||||
import type { RequestInit, Response } from 'node-fetch';
|
||||
import { Headers } from 'node-fetch';
|
||||
import type { connection as WebSocket } from 'websocket';
|
||||
import qs from 'node:querystring';
|
||||
import EventListener from 'node:events';
|
||||
import type { IncomingMessage } from 'node:http';
|
||||
import { setTimeout as sleep } from 'node:timers/promises';
|
||||
|
||||
import type { UnauthenticatedChatConnection } from '@signalapp/libsignal-client/dist/net/Chat.js';
|
||||
import type {
|
||||
UnauthenticatedChatConnection,
|
||||
ProvisioningConnection,
|
||||
ProvisioningConnectionListener,
|
||||
} from '@signalapp/libsignal-client/dist/net/Chat.js';
|
||||
|
||||
import { strictAssert } from '../util/assert.std.js';
|
||||
import { explodePromise } from '../util/explodePromise.std.js';
|
||||
@@ -26,8 +27,6 @@ import {
|
||||
} from '../util/BackOff.std.js';
|
||||
import * as durations from '../util/durations/index.std.js';
|
||||
import { drop } from '../util/drop.std.js';
|
||||
import type { ProxyAgent } from '../util/createProxyAgent.node.js';
|
||||
import { createProxyAgent } from '../util/createProxyAgent.node.js';
|
||||
import { type SocketInfo, SocketStatus } from '../types/SocketStatus.std.js';
|
||||
import { HTTPError } from '../types/HTTPError.std.js';
|
||||
import * as Errors from '../types/errors.std.js';
|
||||
@@ -39,17 +38,14 @@ import type {
|
||||
ChatKind,
|
||||
IChatConnection,
|
||||
IncomingWebSocketRequest,
|
||||
IWebSocketResource,
|
||||
WebSocketResourceOptions,
|
||||
} from './WebsocketResources.preload.js';
|
||||
import WebSocketResource, {
|
||||
connectAuthenticatedLibsignal,
|
||||
connectUnauthenticatedLibsignal,
|
||||
import {
|
||||
connectAuthenticated,
|
||||
connectUnauthenticated,
|
||||
ServerRequestType,
|
||||
} from './WebsocketResources.preload.js';
|
||||
import { ConnectTimeoutError } from './Errors.std.js';
|
||||
import type { IRequestHandler, WebAPICredentials } from './Types.d.ts';
|
||||
import { connect as connectWebSocket } from './WebSocket.preload.js';
|
||||
import type { ServerAlert } from '../types/ServerAlert.std.js';
|
||||
import { getUserLanguages } from '../util/userLanguages.std.js';
|
||||
|
||||
@@ -66,13 +62,6 @@ export const AUTHENTICATED_CHANNEL_NAME = 'authenticated';
|
||||
|
||||
export const NORMAL_DISCONNECT_CODE = 3000;
|
||||
|
||||
export type SocketManagerOptions = Readonly<{
|
||||
url: string;
|
||||
certificateAuthority: string;
|
||||
version: string;
|
||||
proxyUrl?: string;
|
||||
}>;
|
||||
|
||||
type SocketStatusUpdate = { status: SocketStatus };
|
||||
|
||||
export type SocketStatuses = Record<
|
||||
@@ -84,9 +73,9 @@ export type SocketExpirationReason = 'remote' | 'build';
|
||||
|
||||
// This class manages two websocket resources:
|
||||
//
|
||||
// - Authenticated IWebSocketResource which uses supplied WebAPICredentials and
|
||||
// - Authenticated IChatConnection which uses supplied WebAPICredentials and
|
||||
// automatically reconnects on closed socket (using back off)
|
||||
// - Unauthenticated IWebSocketResource that is created on the first outgoing
|
||||
// - Unauthenticated IChatConnection that is created on the first outgoing
|
||||
// unauthenticated request and is periodically rotated (5 minutes since first
|
||||
// activity on the socket).
|
||||
//
|
||||
@@ -95,7 +84,7 @@ export type SocketExpirationReason = 'remote' | 'build';
|
||||
// least one such request handler becomes available.
|
||||
//
|
||||
// Incoming requests on unauthenticated resource are not currently supported.
|
||||
// IWebSocketResource is responsible for their immediate termination.
|
||||
// IChatConnection is responsible for their immediate termination.
|
||||
export class SocketManager extends EventListener {
|
||||
#backOff = new BackOff(FIBONACCI_TIMEOUTS, {
|
||||
jitter: JITTER,
|
||||
@@ -105,7 +94,6 @@ export class SocketManager extends EventListener {
|
||||
#unauthenticated?: AbortableProcess<IChatConnection<'unauth'>>;
|
||||
#unauthenticatedExpirationTimer?: NodeJS.Timeout;
|
||||
#credentials?: WebAPICredentials;
|
||||
#lazyProxyAgent?: Promise<ProxyAgent>;
|
||||
#authenticatedStatus: SocketInfo = {
|
||||
status: SocketStatus.CLOSED,
|
||||
};
|
||||
@@ -121,10 +109,7 @@ export class SocketManager extends EventListener {
|
||||
#reconnectController: AbortController | undefined;
|
||||
#envelopeCount = 0;
|
||||
|
||||
constructor(
|
||||
private readonly libsignalNet: Net.Net,
|
||||
private readonly options: SocketManagerOptions
|
||||
) {
|
||||
constructor(private readonly libsignalNet: Net.Net) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -202,7 +187,7 @@ export class SocketManager extends EventListener {
|
||||
window.SignalContext.getResolvedMessagesLocale()
|
||||
);
|
||||
|
||||
const process = connectAuthenticatedLibsignal({
|
||||
const process = connectAuthenticated({
|
||||
libsignalNet: this.libsignalNet,
|
||||
name: AUTHENTICATED_CHANNEL_NAME,
|
||||
credentials: this.#credentials,
|
||||
@@ -367,7 +352,7 @@ export class SocketManager extends EventListener {
|
||||
}
|
||||
|
||||
// Either returns currently connecting/active authenticated
|
||||
// IWebSocketResource or connects a fresh one.
|
||||
// IChatConnection or connects a fresh one.
|
||||
public async getAuthenticatedResource(): Promise<IChatConnection<'auth'>> {
|
||||
if (!this.#authenticated) {
|
||||
strictAssert(this.#credentials !== undefined, 'Missing credentials');
|
||||
@@ -378,11 +363,11 @@ export class SocketManager extends EventListener {
|
||||
return this.#authenticated.getResult();
|
||||
}
|
||||
|
||||
// Creates new IWebSocketResource for AccountManager's provisioning
|
||||
public async getProvisioningResource(
|
||||
handler: IRequestHandler,
|
||||
timeout?: number
|
||||
): Promise<IWebSocketResource> {
|
||||
// Creates new ProvisioningConnection for AccountManager's provisioning
|
||||
public async getProvisioningConnection(
|
||||
listener: ProvisioningConnectionListener,
|
||||
timeout: number
|
||||
): Promise<ProvisioningConnection> {
|
||||
if (this.#expirationReason != null) {
|
||||
throw new Error(
|
||||
`${this.#expirationReason} expired, ` +
|
||||
@@ -390,48 +375,22 @@ export class SocketManager extends EventListener {
|
||||
);
|
||||
}
|
||||
|
||||
return this.#connectResource({
|
||||
name: 'provisioning',
|
||||
path: '/v1/websocket/provisioning/',
|
||||
proxyAgent: await this.#getProxyAgent(),
|
||||
resourceOptions: {
|
||||
name: 'provisioning',
|
||||
handleRequest: (req: IncomingWebSocketRequest): void => {
|
||||
handler.handleRequest(req);
|
||||
},
|
||||
keepalive: { path: '/v1/keepalive/provisioning' },
|
||||
},
|
||||
extraHeaders: {
|
||||
'x-signal-websocket-timeout': 'true',
|
||||
},
|
||||
timeout,
|
||||
}).getResult();
|
||||
const abortController = new AbortController();
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, timeout);
|
||||
|
||||
try {
|
||||
return await this.libsignalNet.connectProvisioning(listener, {
|
||||
abortSignal: abortController.signal,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
// Creates new WebSocket for Art Creator provisioning
|
||||
public async connectExternalSocket({
|
||||
url,
|
||||
extraHeaders,
|
||||
}: {
|
||||
url: string;
|
||||
extraHeaders?: Record<string, string>;
|
||||
}): Promise<WebSocket> {
|
||||
const proxyAgent = await this.#getProxyAgent();
|
||||
|
||||
return connectWebSocket({
|
||||
name: 'art-creator-provisioning',
|
||||
url,
|
||||
version: this.options.version,
|
||||
proxyAgent,
|
||||
extraHeaders,
|
||||
|
||||
createResource(socket: WebSocket): WebSocket {
|
||||
return socket;
|
||||
},
|
||||
}).getResult();
|
||||
}
|
||||
|
||||
public async getUnauthenticatedLibsignalApi(): Promise<UnauthenticatedChatConnection> {
|
||||
public async getUnauthenticatedApi(): Promise<UnauthenticatedChatConnection> {
|
||||
const resource = await this.#getUnauthenticatedResource();
|
||||
return resource.libsignalWebsocket;
|
||||
}
|
||||
@@ -652,7 +611,7 @@ export class SocketManager extends EventListener {
|
||||
);
|
||||
|
||||
const process: AbortableProcess<IChatConnection<'unauth'>> =
|
||||
connectUnauthenticatedLibsignal({
|
||||
connectUnauthenticated({
|
||||
libsignalNet: this.libsignalNet,
|
||||
name: UNAUTHENTICATED_CHANNEL_NAME,
|
||||
userLanguages,
|
||||
@@ -728,58 +687,6 @@ export class SocketManager extends EventListener {
|
||||
return this.#unauthenticated.getResult();
|
||||
}
|
||||
|
||||
#connectResource({
|
||||
name,
|
||||
path,
|
||||
proxyAgent,
|
||||
resourceOptions,
|
||||
query = {},
|
||||
extraHeaders = {},
|
||||
onUpgradeResponse,
|
||||
timeout,
|
||||
}: {
|
||||
name: string;
|
||||
path: string;
|
||||
proxyAgent: ProxyAgent | undefined;
|
||||
resourceOptions: WebSocketResourceOptions;
|
||||
query?: Record<string, string>;
|
||||
extraHeaders?: Record<string, string>;
|
||||
onUpgradeResponse?: (response: IncomingMessage) => void;
|
||||
timeout?: number;
|
||||
}): AbortableProcess<IWebSocketResource> {
|
||||
const queryWithDefaults = {
|
||||
agent: 'OWD',
|
||||
version: this.options.version,
|
||||
...query,
|
||||
};
|
||||
|
||||
const url = `${this.options.url}${path}?${qs.encode(queryWithDefaults)}`;
|
||||
const { version } = this.options;
|
||||
|
||||
const start = performance.now();
|
||||
const webSocketResourceConnection = connectWebSocket({
|
||||
name,
|
||||
url,
|
||||
version,
|
||||
certificateAuthority: this.options.certificateAuthority,
|
||||
proxyAgent,
|
||||
timeout,
|
||||
|
||||
extraHeaders,
|
||||
onUpgradeResponse,
|
||||
|
||||
createResource(socket: WebSocket): WebSocketResource {
|
||||
const duration = (performance.now() - start).toFixed(1);
|
||||
log.info(
|
||||
`WebSocketResource(${resourceOptions.name}) connected in ${duration}ms`
|
||||
);
|
||||
return new WebSocketResource(socket, resourceOptions);
|
||||
},
|
||||
});
|
||||
|
||||
return webSocketResourceConnection;
|
||||
}
|
||||
|
||||
async #checkResource<Chat extends ChatKind>(
|
||||
process?: AbortableProcess<IChatConnection<Chat>>
|
||||
): Promise<void> {
|
||||
@@ -928,14 +835,6 @@ export class SocketManager extends EventListener {
|
||||
);
|
||||
}
|
||||
|
||||
async #getProxyAgent(): Promise<ProxyAgent | undefined> {
|
||||
if (this.options.proxyUrl && !this.#lazyProxyAgent) {
|
||||
// Cache the promise so that we don't import concurrently.
|
||||
this.#lazyProxyAgent = createProxyAgent(this.options.proxyUrl);
|
||||
}
|
||||
return this.#lazyProxyAgent;
|
||||
}
|
||||
|
||||
// EventEmitter types
|
||||
|
||||
public override on(type: 'authError', callback: () => void): this;
|
||||
|
||||
@@ -23,6 +23,10 @@ import type {
|
||||
Pni,
|
||||
} from '@signalapp/libsignal-client';
|
||||
import { AccountAttributes } from '@signalapp/libsignal-client/dist/net.js';
|
||||
import type {
|
||||
ProvisioningConnection,
|
||||
ProvisioningConnectionListener,
|
||||
} from '@signalapp/libsignal-client/dist/net.js';
|
||||
import { GroupSendFullToken } from '@signalapp/libsignal-client/zkgroup.js';
|
||||
import type {
|
||||
Request as KTRequest,
|
||||
@@ -93,7 +97,6 @@ import { createLogger } from '../logging/log.std.js';
|
||||
import { maybeParseUrl, urlPathFromComponents } from '../util/url.std.js';
|
||||
import { HOUR, MINUTE, SECOND } from '../util/durations/index.std.js';
|
||||
import { safeParseNumber } from '../util/numbers.std.js';
|
||||
import type { IWebSocketResource } from './WebsocketResources.preload.js';
|
||||
import { getLibsignalNet } from './preconnect.preload.js';
|
||||
import type { GroupSendToken } from '../types/GroupSendEndorsements.std.js';
|
||||
import {
|
||||
@@ -1738,12 +1741,7 @@ const PARSE_RANGE_HEADER = /\/(\d+)$/;
|
||||
const PARSE_GROUP_LOG_RANGE_HEADER =
|
||||
/^versions\s+(\d{1,10})-(\d{1,10})\/(\d{1,10})/;
|
||||
|
||||
const socketManager = new SocketManager(libsignalNet, {
|
||||
url: chatServiceUrl,
|
||||
certificateAuthority,
|
||||
version,
|
||||
proxyUrl,
|
||||
});
|
||||
const socketManager = new SocketManager(libsignalNet);
|
||||
|
||||
socketManager.on('statusChange', () => {
|
||||
window.Whisper.events.emit('socketStatusChange');
|
||||
@@ -2478,7 +2476,7 @@ export async function getAccountForUsername({
|
||||
hash,
|
||||
}: GetAccountForUsernameOptionsType): Promise<GetAccountForUsernameResultType> {
|
||||
const aci = await _retry(async () => {
|
||||
const chat = await socketManager.getUnauthenticatedLibsignalApi();
|
||||
const chat = await socketManager.getUnauthenticatedApi();
|
||||
return chat.lookUpUsernameHash({ hash });
|
||||
});
|
||||
|
||||
@@ -2490,7 +2488,7 @@ export async function keyTransparencySearch(
|
||||
abortSignal?: AbortSignal
|
||||
): Promise<void> {
|
||||
return _retry(async () => {
|
||||
const chat = await socketManager.getUnauthenticatedLibsignalApi();
|
||||
const chat = await socketManager.getUnauthenticatedApi();
|
||||
if (abortSignal?.aborted) {
|
||||
throw new Error('Aborted');
|
||||
}
|
||||
@@ -2506,7 +2504,7 @@ export async function keyTransparencyMonitor(
|
||||
abortSignal?: AbortSignal
|
||||
): Promise<void> {
|
||||
return _retry(async () => {
|
||||
const chat = await socketManager.getUnauthenticatedLibsignalApi();
|
||||
const chat = await socketManager.getUnauthenticatedApi();
|
||||
if (abortSignal?.aborted) {
|
||||
throw new Error('Aborted');
|
||||
}
|
||||
@@ -2731,7 +2729,7 @@ export async function resolveUsernameLink({
|
||||
uuid,
|
||||
}: ResolveUsernameByLinkOptionsType): Promise<ResolveUsernameLinkResultType> {
|
||||
return _retry(async () => {
|
||||
const chat = await socketManager.getUnauthenticatedLibsignalApi();
|
||||
const chat = await socketManager.getUnauthenticatedApi();
|
||||
return chat.lookUpUsernameLink({ uuid, entropy });
|
||||
});
|
||||
}
|
||||
@@ -3679,7 +3677,7 @@ export async function sendMulti(
|
||||
}
|
||||
|
||||
const result = await _retry(async () => {
|
||||
const chat = await socketManager.getUnauthenticatedLibsignalApi();
|
||||
const chat = await socketManager.getUnauthenticatedApi();
|
||||
return chat.sendMultiRecipientMessage({
|
||||
payload,
|
||||
timestamp,
|
||||
@@ -4819,11 +4817,11 @@ export async function getHasSubscription(
|
||||
return data.subscription.active;
|
||||
}
|
||||
|
||||
export function getProvisioningResource(
|
||||
handler: IRequestHandler,
|
||||
timeout?: number
|
||||
): Promise<IWebSocketResource> {
|
||||
return socketManager.getProvisioningResource(handler, timeout);
|
||||
export function getProvisioningConnection(
|
||||
listener: ProvisioningConnectionListener,
|
||||
timeout: number
|
||||
): Promise<ProvisioningConnection> {
|
||||
return socketManager.getProvisioningConnection(listener, timeout);
|
||||
}
|
||||
|
||||
export async function cdsLookup({
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import ws from 'websocket';
|
||||
import type { connection as WebSocket } from 'websocket';
|
||||
import type { IncomingMessage } from 'node:http';
|
||||
|
||||
import { AbortableProcess } from '../util/AbortableProcess.std.js';
|
||||
import { strictAssert } from '../util/assert.std.js';
|
||||
import { explodePromise } from '../util/explodePromise.std.js';
|
||||
import { getUserAgent } from '../util/getUserAgent.node.js';
|
||||
import * as durations from '../util/durations/index.std.js';
|
||||
import type { ProxyAgent } from '../util/createProxyAgent.node.js';
|
||||
import { createHTTPSAgent } from '../util/createHTTPSAgent.node.js';
|
||||
import { HTTPError } from '../types/HTTPError.std.js';
|
||||
import { createLogger } from '../logging/log.std.js';
|
||||
import * as Timers from '../Timers.preload.js';
|
||||
import { ConnectTimeoutError } from './Errors.std.js';
|
||||
import { handleStatusCode, translateError } from './Utils.dom.js';
|
||||
|
||||
const { client: WebSocketClient } = ws;
|
||||
|
||||
const log = createLogger('WebSocket');
|
||||
|
||||
const TEN_SECONDS = 10 * durations.SECOND;
|
||||
const WEBSOCKET_CONNECT_TIMEOUT = TEN_SECONDS;
|
||||
const KEEPALIVE_INTERVAL_MS = TEN_SECONDS;
|
||||
|
||||
export type IResource = {
|
||||
close(code: number, reason: string): void;
|
||||
};
|
||||
|
||||
export type ConnectOptionsType<Resource extends IResource> = Readonly<{
|
||||
name: string;
|
||||
url: string;
|
||||
certificateAuthority?: string;
|
||||
version: string;
|
||||
proxyAgent?: ProxyAgent;
|
||||
timeout?: number;
|
||||
extraHeaders?: Record<string, string>;
|
||||
onUpgradeResponse?: (response: IncomingMessage) => void;
|
||||
|
||||
createResource(socket: WebSocket): Resource;
|
||||
}>;
|
||||
|
||||
export function connect<Resource extends IResource>({
|
||||
name,
|
||||
url,
|
||||
certificateAuthority,
|
||||
version,
|
||||
proxyAgent,
|
||||
extraHeaders = {},
|
||||
timeout = WEBSOCKET_CONNECT_TIMEOUT,
|
||||
onUpgradeResponse,
|
||||
createResource,
|
||||
}: ConnectOptionsType<Resource>): AbortableProcess<Resource> {
|
||||
const fixedScheme = url
|
||||
.replace('https://', 'wss://')
|
||||
.replace('http://', 'ws://');
|
||||
|
||||
const headers = {
|
||||
...extraHeaders,
|
||||
'User-Agent': getUserAgent(version),
|
||||
};
|
||||
const client = new WebSocketClient({
|
||||
tlsOptions: {
|
||||
ca: certificateAuthority,
|
||||
agent: proxyAgent ?? createHTTPSAgent(),
|
||||
},
|
||||
maxReceivedFrameSize: 0x210000,
|
||||
});
|
||||
|
||||
client.connect(fixedScheme, undefined, undefined, headers);
|
||||
|
||||
const { stack } = new Error();
|
||||
|
||||
const { promise, resolve, reject } = explodePromise<Resource>();
|
||||
|
||||
const timer = Timers.setTimeout(() => {
|
||||
reject(new ConnectTimeoutError('Connection timed out'));
|
||||
|
||||
client.abort();
|
||||
}, timeout);
|
||||
|
||||
let resource: Resource | undefined;
|
||||
client.on('connect', socket => {
|
||||
Timers.clearTimeout(timer);
|
||||
|
||||
socket.socket.setKeepAlive(true, KEEPALIVE_INTERVAL_MS);
|
||||
|
||||
resource = createResource(socket);
|
||||
resolve(resource);
|
||||
});
|
||||
|
||||
client.on('upgradeResponse', response => {
|
||||
onUpgradeResponse?.(response);
|
||||
});
|
||||
|
||||
client.on('httpResponse', async response => {
|
||||
Timers.clearTimeout(timer);
|
||||
|
||||
const statusCode = response.statusCode || -1;
|
||||
await handleStatusCode(statusCode);
|
||||
|
||||
const error = new HTTPError('connectResource: invalid websocket response', {
|
||||
code: statusCode || -1,
|
||||
headers: {},
|
||||
stack,
|
||||
});
|
||||
|
||||
const translatedError = translateError(error);
|
||||
strictAssert(
|
||||
translatedError,
|
||||
'`httpResponse` event cannot be emitted with 200 status code'
|
||||
);
|
||||
|
||||
reject(translatedError);
|
||||
});
|
||||
|
||||
client.on('connectFailed', originalErr => {
|
||||
Timers.clearTimeout(timer);
|
||||
|
||||
const err = new HTTPError('connectResource: connectFailed', {
|
||||
code: -1,
|
||||
headers: {},
|
||||
stack,
|
||||
cause: originalErr,
|
||||
});
|
||||
reject(err);
|
||||
});
|
||||
|
||||
return new AbortableProcess<Resource>(
|
||||
`WebSocket.connect(${name})`,
|
||||
{
|
||||
abort() {
|
||||
if (resource) {
|
||||
log.warn(`closing socket ${name}`);
|
||||
resource.close(3000, 'aborted');
|
||||
} else {
|
||||
log.warn(`aborting connection ${name}`);
|
||||
Timers.clearTimeout(timer);
|
||||
client.abort();
|
||||
}
|
||||
},
|
||||
},
|
||||
promise
|
||||
);
|
||||
}
|
||||
@@ -26,16 +26,12 @@
|
||||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
/* eslint-disable @typescript-eslint/brace-style */
|
||||
|
||||
import type { connection as WebSocket, IMessage } from 'websocket';
|
||||
import Long from 'long';
|
||||
import pTimeout from 'p-timeout';
|
||||
import { Response } from 'node-fetch';
|
||||
import net from 'node:net';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { LibSignalError, Net } from '@signalapp/libsignal-client';
|
||||
import { ErrorCode } from '@signalapp/libsignal-client';
|
||||
import { Buffer } from 'node:buffer';
|
||||
import type {
|
||||
AuthenticatedChatConnection,
|
||||
ChatServerMessageAck,
|
||||
@@ -47,15 +43,11 @@ import type { EventHandler } from './EventTarget.std.js';
|
||||
import EventTarget from './EventTarget.std.js';
|
||||
|
||||
import * as durations from '../util/durations/index.std.js';
|
||||
import { dropNull } from '../util/dropNull.std.js';
|
||||
import { drop } from '../util/drop.std.js';
|
||||
import { isOlderThan } from '../util/timestamp.std.js';
|
||||
import { strictAssert } from '../util/assert.std.js';
|
||||
import * as Errors from '../types/errors.std.js';
|
||||
import { SignalService as Proto } from '../protobuf/index.std.js';
|
||||
import { createLogger } from '../logging/log.std.js';
|
||||
import * as Timers from '../Timers.preload.js';
|
||||
import type { IResource } from './WebSocket.preload.js';
|
||||
|
||||
import { AbortableProcess } from '../util/AbortableProcess.std.js';
|
||||
import type { WebAPICredentials } from './Types.d.ts';
|
||||
@@ -66,10 +58,6 @@ import type { ServerAlert } from '../types/ServerAlert.std.js';
|
||||
|
||||
const log = createLogger('WebsocketResources');
|
||||
|
||||
const THIRTY_SECONDS = 30 * durations.SECOND;
|
||||
|
||||
const MAX_MESSAGE_SIZE = 512 * 1024;
|
||||
|
||||
const AGGREGATED_STATS_KEY = 'websocketStats';
|
||||
|
||||
export enum IpVersion {
|
||||
@@ -166,20 +154,12 @@ export enum ServerRequestType {
|
||||
Unknown = 'unknown',
|
||||
}
|
||||
|
||||
export type IncomingWebSocketRequest = {
|
||||
readonly requestType: ServerRequestType;
|
||||
readonly body: Uint8Array | undefined;
|
||||
readonly timestamp: number | undefined;
|
||||
|
||||
respond(status: number, message: string): void;
|
||||
};
|
||||
|
||||
export class IncomingWebSocketRequestLibsignal implements IncomingWebSocketRequest {
|
||||
export class IncomingWebSocketRequest {
|
||||
constructor(
|
||||
readonly requestType: ServerRequestType,
|
||||
readonly body: Uint8Array | undefined,
|
||||
readonly timestamp: number | undefined,
|
||||
private readonly ack: ChatServerMessageAck | undefined
|
||||
private readonly ack: Pick<ChatServerMessageAck, 'send'> | undefined
|
||||
) {}
|
||||
|
||||
respond(status: number, _message: string): void {
|
||||
@@ -187,68 +167,6 @@ export class IncomingWebSocketRequestLibsignal implements IncomingWebSocketReque
|
||||
}
|
||||
}
|
||||
|
||||
export class IncomingWebSocketRequestLegacy implements IncomingWebSocketRequest {
|
||||
readonly #id: Long;
|
||||
|
||||
public readonly requestType: ServerRequestType;
|
||||
|
||||
public readonly body: Uint8Array | undefined;
|
||||
|
||||
public readonly timestamp: number | undefined;
|
||||
|
||||
constructor(
|
||||
request: Proto.IWebSocketRequestMessage,
|
||||
private readonly sendBytes: (bytes: Buffer) => void
|
||||
) {
|
||||
strictAssert(request.id, 'request without id');
|
||||
strictAssert(request.verb, 'request without verb');
|
||||
strictAssert(request.path, 'request without path');
|
||||
|
||||
this.#id = request.id;
|
||||
this.requestType = resolveType(request.path, request.verb);
|
||||
this.body = dropNull(request.body);
|
||||
this.timestamp = resolveTimestamp(request.headers || []);
|
||||
}
|
||||
|
||||
public respond(status: number, message: string): void {
|
||||
const bytes = Proto.WebSocketMessage.encode({
|
||||
type: Proto.WebSocketMessage.Type.RESPONSE,
|
||||
response: { id: this.#id, message, status },
|
||||
}).finish();
|
||||
|
||||
this.sendBytes(Buffer.from(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
function resolveType(path: string, verb: string): ServerRequestType {
|
||||
if (path === ServerRequestType.ApiMessage) {
|
||||
return ServerRequestType.ApiMessage;
|
||||
}
|
||||
if (path === ServerRequestType.ApiEmptyQueue && verb === 'PUT') {
|
||||
return ServerRequestType.ApiEmptyQueue;
|
||||
}
|
||||
if (path === ServerRequestType.ProvisioningAddress && verb === 'PUT') {
|
||||
return ServerRequestType.ProvisioningAddress;
|
||||
}
|
||||
if (path === ServerRequestType.ProvisioningMessage && verb === 'PUT') {
|
||||
return ServerRequestType.ProvisioningMessage;
|
||||
}
|
||||
return ServerRequestType.Unknown;
|
||||
}
|
||||
|
||||
function resolveTimestamp(headers: ReadonlyArray<string>): number | undefined {
|
||||
// The 'X-Signal-Timestamp' is usually the last item, so start there.
|
||||
let it = headers.length;
|
||||
// eslint-disable-next-line no-plusplus
|
||||
while (--it >= 0) {
|
||||
const match = headers[it].match(/^X-Signal-Timestamp:\s*(\d+)\s*$/i);
|
||||
if (match && match.length === 2) {
|
||||
return Number(match[1]);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export type SendRequestOptions = Readonly<{
|
||||
verb: string;
|
||||
path: string;
|
||||
@@ -281,12 +199,12 @@ export class CloseEvent extends Event {
|
||||
|
||||
export type ChatKind = 'auth' | 'unauth';
|
||||
|
||||
type LibsignalChatConnection<Kind extends ChatKind> = Kind extends 'auth'
|
||||
type ChatConnection<Kind extends ChatKind> = Kind extends 'auth'
|
||||
? AuthenticatedChatConnection
|
||||
: UnauthenticatedChatConnection;
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
export interface IWebSocketResource extends IResource {
|
||||
export interface IWebSocketResource {
|
||||
sendRequest(options: SendRequestOptions): Promise<Response>;
|
||||
|
||||
addEventListener(name: 'close', handler: (ev: CloseEvent) => void): void;
|
||||
@@ -301,16 +219,16 @@ export interface IWebSocketResource extends IResource {
|
||||
}
|
||||
|
||||
export type IChatConnection<Chat extends ChatKind> = IWebSocketResource & {
|
||||
get libsignalWebsocket(): LibsignalChatConnection<Chat>;
|
||||
get libsignalWebsocket(): ChatConnection<Chat>;
|
||||
};
|
||||
|
||||
type LibsignalWebSocketResourceHolder<Chat extends ChatKind> = {
|
||||
resource: LibsignalWebSocketResource<Chat> | undefined;
|
||||
type WebSocketResourceHandler<Chat extends ChatKind> = {
|
||||
resource: WebSocketResource<Chat> | undefined;
|
||||
};
|
||||
|
||||
const UNEXPECTED_DISCONNECT_CODE = 3001;
|
||||
|
||||
export function connectUnauthenticatedLibsignal({
|
||||
export function connectUnauthenticated({
|
||||
libsignalNet,
|
||||
name,
|
||||
userLanguages,
|
||||
@@ -320,9 +238,9 @@ export function connectUnauthenticatedLibsignal({
|
||||
name: string;
|
||||
userLanguages: ReadonlyArray<string>;
|
||||
keepalive: KeepAliveOptionsType;
|
||||
}): AbortableProcess<LibsignalWebSocketResource<'unauth'>> {
|
||||
const logId = `LibsignalWebSocketResource(${name})`;
|
||||
const listener: LibsignalWebSocketResourceHolder<'unauth'> &
|
||||
}): AbortableProcess<WebSocketResource<'unauth'>> {
|
||||
const logId = `WebSocketResource(${name})`;
|
||||
const listener: WebSocketResourceHandler<'unauth'> &
|
||||
ConnectionEventsListener = {
|
||||
resource: undefined,
|
||||
onConnectionInterrupted(cause: LibSignalError | null): void {
|
||||
@@ -334,7 +252,7 @@ export function connectUnauthenticatedLibsignal({
|
||||
this.resource = undefined;
|
||||
},
|
||||
};
|
||||
return connectLibsignal(
|
||||
return connect(
|
||||
abortSignal =>
|
||||
libsignalNet.connectUnauthenticatedChat(listener, {
|
||||
abortSignal,
|
||||
@@ -346,7 +264,7 @@ export function connectUnauthenticatedLibsignal({
|
||||
);
|
||||
}
|
||||
|
||||
export function connectAuthenticatedLibsignal({
|
||||
export function connectAuthenticated({
|
||||
libsignalNet,
|
||||
name,
|
||||
credentials,
|
||||
@@ -364,10 +282,9 @@ export function connectAuthenticatedLibsignal({
|
||||
receiveStories: boolean;
|
||||
userLanguages: ReadonlyArray<string>;
|
||||
keepalive: KeepAliveOptionsType;
|
||||
}): AbortableProcess<LibsignalWebSocketResource<'auth'>> {
|
||||
const logId = `LibsignalWebSocketResource(${name})`;
|
||||
const listener: LibsignalWebSocketResourceHolder<'auth'> &
|
||||
ChatServiceListener = {
|
||||
}): AbortableProcess<WebSocketResource<'auth'>> {
|
||||
const logId = `WebSocketResource(${name})`;
|
||||
const listener: WebSocketResourceHandler<'auth'> & ChatServiceListener = {
|
||||
resource: undefined,
|
||||
onIncomingMessage(
|
||||
envelope: Uint8Array,
|
||||
@@ -375,7 +292,7 @@ export function connectAuthenticatedLibsignal({
|
||||
ack: ChatServerMessageAck
|
||||
): void {
|
||||
// Handle incoming messages even if we've disconnected.
|
||||
const request = new IncomingWebSocketRequestLibsignal(
|
||||
const request = new IncomingWebSocketRequest(
|
||||
ServerRequestType.ApiMessage,
|
||||
envelope,
|
||||
timestamp,
|
||||
@@ -388,7 +305,7 @@ export function connectAuthenticatedLibsignal({
|
||||
logDisconnectedListenerWarn(logId, 'onQueueEmpty');
|
||||
return;
|
||||
}
|
||||
const request = new IncomingWebSocketRequestLibsignal(
|
||||
const request = new IncomingWebSocketRequest(
|
||||
ServerRequestType.ApiEmptyQueue,
|
||||
undefined,
|
||||
undefined,
|
||||
@@ -408,7 +325,7 @@ export function connectAuthenticatedLibsignal({
|
||||
onReceivedAlerts(alerts.map(parseServerAlertsFromHeader).flat());
|
||||
},
|
||||
};
|
||||
return connectLibsignal(
|
||||
return connect(
|
||||
(abortSignal: AbortSignal) =>
|
||||
libsignalNet.connectAuthenticatedChat(
|
||||
credentials.username,
|
||||
@@ -427,21 +344,19 @@ function logDisconnectedListenerWarn(logId: string, method: string): void {
|
||||
log.warn(`${logId} received ${method}, but listener already disconnected`);
|
||||
}
|
||||
|
||||
function connectLibsignal<Chat extends ChatKind>(
|
||||
makeConnection: (
|
||||
abortSignal: AbortSignal
|
||||
) => Promise<LibsignalChatConnection<Chat>>,
|
||||
resourceHolder: LibsignalWebSocketResourceHolder<Chat>,
|
||||
function connect<Chat extends ChatKind>(
|
||||
makeConnection: (abortSignal: AbortSignal) => Promise<ChatConnection<Chat>>,
|
||||
resourceHolder: WebSocketResourceHandler<Chat>,
|
||||
logId: string,
|
||||
keepalive: KeepAliveOptionsType
|
||||
): AbortableProcess<LibsignalWebSocketResource<Chat>> {
|
||||
): AbortableProcess<WebSocketResource<Chat>> {
|
||||
const abortController = new AbortController();
|
||||
const connectAsync = async () => {
|
||||
try {
|
||||
const service = await makeConnection(abortController.signal);
|
||||
log.info(`${logId} connected`);
|
||||
const connectionInfo = service.connectionInfo();
|
||||
const resource = new LibsignalWebSocketResource(
|
||||
const resource = new WebSocketResource(
|
||||
service,
|
||||
IpVersion[connectionInfo.ipVersion],
|
||||
connectionInfo.localPort,
|
||||
@@ -461,7 +376,7 @@ function connectLibsignal<Chat extends ChatKind>(
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
return new AbortableProcess<LibsignalWebSocketResource<Chat>>(
|
||||
return new AbortableProcess<WebSocketResource<Chat>>(
|
||||
`${logId}.connect`,
|
||||
{
|
||||
abort() {
|
||||
@@ -477,7 +392,7 @@ function connectLibsignal<Chat extends ChatKind>(
|
||||
);
|
||||
}
|
||||
|
||||
export class LibsignalWebSocketResource<Chat extends ChatKind>
|
||||
export class WebSocketResource<Chat extends ChatKind>
|
||||
extends EventTarget
|
||||
implements IChatConnection<Chat>
|
||||
{
|
||||
@@ -494,7 +409,7 @@ export class LibsignalWebSocketResource<Chat extends ChatKind>
|
||||
#keepalive: KeepAlive;
|
||||
|
||||
constructor(
|
||||
private readonly chatService: LibsignalChatConnection<Chat>,
|
||||
private readonly chatService: ChatConnection<Chat>,
|
||||
private readonly socketIpVersion: IpVersion,
|
||||
private readonly localPortNumber: number,
|
||||
private readonly logId: string,
|
||||
@@ -583,7 +498,7 @@ export class LibsignalWebSocketResource<Chat extends ChatKind>
|
||||
return response;
|
||||
}
|
||||
|
||||
get libsignalWebsocket(): LibsignalChatConnection<Chat> {
|
||||
get libsignalWebsocket(): ChatConnection<Chat> {
|
||||
return this.chatService;
|
||||
}
|
||||
|
||||
@@ -605,338 +520,6 @@ export class LibsignalWebSocketResource<Chat extends ChatKind>
|
||||
}
|
||||
}
|
||||
|
||||
export default class WebSocketResource
|
||||
extends EventTarget
|
||||
implements IWebSocketResource
|
||||
{
|
||||
#outgoingId = Long.fromNumber(1, true);
|
||||
#closed = false;
|
||||
|
||||
readonly #outgoingMap = new Map<
|
||||
string,
|
||||
(result: SendRequestResult) => void
|
||||
>();
|
||||
|
||||
readonly #boundOnMessage: (message: IMessage) => void;
|
||||
#activeRequests = new Set<IncomingWebSocketRequest | string>();
|
||||
#shuttingDown = false;
|
||||
#shutdownTimer?: Timers.Timeout;
|
||||
readonly #logId: string;
|
||||
readonly #localSocketPort: number | undefined;
|
||||
readonly #socketIpVersion: IpVersion | undefined;
|
||||
|
||||
// Public for tests
|
||||
public readonly keepalive?: KeepAlive;
|
||||
|
||||
constructor(
|
||||
private readonly socket: WebSocket,
|
||||
private readonly options: WebSocketResourceOptions
|
||||
) {
|
||||
super();
|
||||
|
||||
this.#logId = `WebSocketResource(${options.name})`;
|
||||
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);
|
||||
|
||||
socket.on('message', this.#boundOnMessage);
|
||||
|
||||
if (options.keepalive) {
|
||||
const keepalive = new KeepAlive(
|
||||
this,
|
||||
options.name,
|
||||
options.keepalive ?? {}
|
||||
);
|
||||
this.keepalive = keepalive;
|
||||
|
||||
keepalive.reset();
|
||||
socket.on('close', () => this.keepalive?.stop());
|
||||
socket.on('error', (error: Error) => {
|
||||
log.warn(`${this.#logId}: WebSocket error`, Errors.toLogFormat(error));
|
||||
});
|
||||
}
|
||||
|
||||
socket.on('close', (code, reason) => {
|
||||
this.#closed = true;
|
||||
|
||||
log.warn(`${this.#logId}: Socket closed`);
|
||||
this.dispatchEvent(new CloseEvent(code, reason || 'normal'));
|
||||
});
|
||||
|
||||
this.addEventListener('close', () => this.#onClose());
|
||||
}
|
||||
|
||||
public ipVersion(): IpVersion | undefined {
|
||||
return this.#socketIpVersion;
|
||||
}
|
||||
|
||||
public localPort(): number | undefined {
|
||||
return this.#localSocketPort;
|
||||
}
|
||||
|
||||
public override addEventListener(
|
||||
name: 'close',
|
||||
handler: (ev: CloseEvent) => void
|
||||
): void;
|
||||
|
||||
public override addEventListener(name: string, handler: EventHandler): void {
|
||||
return super.addEventListener(name, handler);
|
||||
}
|
||||
|
||||
public async sendRequest(options: SendRequestOptions): Promise<Response> {
|
||||
const id = this.#outgoingId;
|
||||
const idString = id.toString();
|
||||
strictAssert(
|
||||
!this.#outgoingMap.has(idString),
|
||||
'Duplicate outgoing request'
|
||||
);
|
||||
|
||||
// Note that this automatically wraps
|
||||
this.#outgoingId = this.#outgoingId.add(1);
|
||||
|
||||
const bytes = Proto.WebSocketMessage.encode({
|
||||
type: Proto.WebSocketMessage.Type.REQUEST,
|
||||
request: {
|
||||
verb: options.verb,
|
||||
path: options.path,
|
||||
body: options.body,
|
||||
headers: options.headers
|
||||
? options.headers
|
||||
.map(([key, value]) => {
|
||||
return `${key}:${value}`;
|
||||
})
|
||||
.slice()
|
||||
: undefined,
|
||||
id,
|
||||
},
|
||||
}).finish();
|
||||
strictAssert(
|
||||
bytes.length <= MAX_MESSAGE_SIZE,
|
||||
'WebSocket request byte size exceeded'
|
||||
);
|
||||
|
||||
strictAssert(!this.#shuttingDown, 'Cannot send request, shutting down');
|
||||
this.#addActive(idString);
|
||||
const promise = new Promise<SendRequestResult>((resolve, reject) => {
|
||||
let timer = options.timeout
|
||||
? Timers.setTimeout(() => {
|
||||
this.#removeActive(idString);
|
||||
this.close(UNEXPECTED_DISCONNECT_CODE, 'Request timed out');
|
||||
reject(new Error(`Request timed out; id: [${idString}]`));
|
||||
}, options.timeout)
|
||||
: undefined;
|
||||
|
||||
this.#outgoingMap.set(idString, result => {
|
||||
if (timer !== undefined) {
|
||||
Timers.clearTimeout(timer);
|
||||
timer = undefined;
|
||||
}
|
||||
|
||||
this.keepalive?.reset();
|
||||
this.#removeActive(idString);
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
|
||||
this.socket.sendBytes(Buffer.from(bytes));
|
||||
|
||||
const requestResult = await promise;
|
||||
return WebSocketResource.intoResponse(requestResult);
|
||||
}
|
||||
|
||||
public forceKeepAlive(timeout?: number): void {
|
||||
if (!this.keepalive) {
|
||||
return;
|
||||
}
|
||||
drop(this.keepalive.send(timeout));
|
||||
}
|
||||
|
||||
public close(code = NORMAL_DISCONNECT_CODE, reason?: string): void {
|
||||
if (this.#closed) {
|
||||
log.info(`${this.#logId}.close: Already closed! ${code}/${reason}`);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(`${this.#logId}.close(${code})`);
|
||||
if (this.keepalive) {
|
||||
this.keepalive.stop();
|
||||
}
|
||||
|
||||
this.socket.close(code, reason);
|
||||
|
||||
this.socket.removeListener('message', this.#boundOnMessage);
|
||||
|
||||
// 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(() => {
|
||||
if (this.#closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.warn(`${this.#logId}.close: Dispatching our own socket close event`);
|
||||
this.dispatchEvent(new CloseEvent(code, reason || 'normal'));
|
||||
}, 5 * durations.SECOND);
|
||||
}
|
||||
|
||||
public shutdown(): void {
|
||||
if (this.#closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#activeRequests.size === 0) {
|
||||
log.info(`${this.#logId}.shutdown: no active requests, closing`);
|
||||
this.close(NORMAL_DISCONNECT_CODE, 'Shutdown');
|
||||
return;
|
||||
}
|
||||
|
||||
this.#shuttingDown = true;
|
||||
|
||||
log.info(`${this.#logId}.shutdown: shutting down`);
|
||||
this.#shutdownTimer = Timers.setTimeout(() => {
|
||||
if (this.#closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.warn(`${this.#logId}.shutdown: Failed to shutdown gracefully`);
|
||||
this.close(NORMAL_DISCONNECT_CODE, 'Shutdown');
|
||||
}, THIRTY_SECONDS);
|
||||
}
|
||||
|
||||
#onMessage({ type, binaryData }: IMessage): void {
|
||||
if (type !== 'binary' || !binaryData) {
|
||||
throw new Error(`Unsupported websocket message type: ${type}`);
|
||||
}
|
||||
|
||||
const message = Proto.WebSocketMessage.decode(binaryData);
|
||||
if (
|
||||
message.type === Proto.WebSocketMessage.Type.REQUEST &&
|
||||
message.request
|
||||
) {
|
||||
const handleRequest =
|
||||
this.options.handleRequest ||
|
||||
(request => request.respond(404, 'Not found'));
|
||||
|
||||
const incomingRequest = new IncomingWebSocketRequestLegacy(
|
||||
message.request,
|
||||
(bytes: Buffer): void => {
|
||||
this.#removeActive(incomingRequest);
|
||||
|
||||
strictAssert(
|
||||
bytes.length <= MAX_MESSAGE_SIZE,
|
||||
'WebSocket response byte size exceeded'
|
||||
);
|
||||
this.socket.sendBytes(bytes);
|
||||
}
|
||||
);
|
||||
|
||||
if (this.#shuttingDown) {
|
||||
incomingRequest.respond(-1, 'Shutting down');
|
||||
return;
|
||||
}
|
||||
|
||||
this.#addActive(incomingRequest);
|
||||
handleRequest(incomingRequest);
|
||||
} else if (
|
||||
message.type === Proto.WebSocketMessage.Type.RESPONSE &&
|
||||
message.response
|
||||
) {
|
||||
const { response } = message;
|
||||
strictAssert(response.id, 'response without id');
|
||||
|
||||
const responseIdString = response.id.toString();
|
||||
const resolve = this.#outgoingMap.get(responseIdString);
|
||||
this.#outgoingMap.delete(responseIdString);
|
||||
|
||||
if (!resolve) {
|
||||
throw new Error(`Received response for unknown request ${response.id}`);
|
||||
}
|
||||
|
||||
resolve({
|
||||
status: response.status ?? -1,
|
||||
message: response.message ?? '',
|
||||
response: dropNull(response.body),
|
||||
headers: response.headers ?? [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#onClose(): void {
|
||||
const outgoing = new Map(this.#outgoingMap);
|
||||
this.#outgoingMap.clear();
|
||||
|
||||
for (const resolve of outgoing.values()) {
|
||||
resolve({
|
||||
status: -1,
|
||||
message: 'Connection closed',
|
||||
response: undefined,
|
||||
headers: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#addActive(request: IncomingWebSocketRequest | string): void {
|
||||
this.#activeRequests.add(request);
|
||||
}
|
||||
|
||||
#removeActive(request: IncomingWebSocketRequest | string): void {
|
||||
if (!this.#activeRequests.has(request)) {
|
||||
log.warn(`${this.#logId}.removeActive: removing unknown request`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.#activeRequests.delete(request);
|
||||
if (this.#activeRequests.size !== 0) {
|
||||
return;
|
||||
}
|
||||
if (!this.#shuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#shutdownTimer) {
|
||||
Timers.clearTimeout(this.#shutdownTimer);
|
||||
this.#shutdownTimer = undefined;
|
||||
}
|
||||
|
||||
log.info(`${this.#logId}.removeActive: shutdown complete`);
|
||||
this.close(NORMAL_DISCONNECT_CODE, '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 = {
|
||||
path?: string;
|
||||
};
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { Cds2Client } from '@signalapp/libsignal-client';
|
||||
|
||||
import { strictAssert } from '../../util/assert.std.js';
|
||||
import { SignalService as Proto } from '../../protobuf/index.std.js';
|
||||
import { CDSSocketBase, CDSSocketState } from './CDSSocketBase.node.js';
|
||||
import type { CDSSocketBaseOptionsType } from './CDSSocketBase.node.js';
|
||||
|
||||
export type CDSISocketOptionsType = Readonly<{
|
||||
mrenclave: Uint8Array;
|
||||
}> &
|
||||
CDSSocketBaseOptionsType;
|
||||
|
||||
export class CDSISocket extends CDSSocketBase<CDSISocketOptionsType> {
|
||||
#privCdsClient: Cds2Client | undefined;
|
||||
|
||||
public override async handshake(): Promise<void> {
|
||||
strictAssert(
|
||||
this.state === CDSSocketState.Open,
|
||||
'CDSI handshake called twice'
|
||||
);
|
||||
this.state = CDSSocketState.Handshake;
|
||||
|
||||
{
|
||||
const { done, value: attestationMessage } =
|
||||
await this.socketIterator.next();
|
||||
strictAssert(!done, 'CDSI socket closed before handshake');
|
||||
|
||||
const earliestValidTimestamp = new Date();
|
||||
|
||||
strictAssert(
|
||||
this.#privCdsClient === undefined,
|
||||
'CDSI handshake called twice'
|
||||
);
|
||||
this.#privCdsClient = Cds2Client.new(
|
||||
this.options.mrenclave,
|
||||
attestationMessage,
|
||||
earliestValidTimestamp
|
||||
);
|
||||
}
|
||||
|
||||
this.socket.sendBytes(Buffer.from(this.#cdsClient.initialRequest()));
|
||||
|
||||
{
|
||||
const { done, value: message } = await this.socketIterator.next();
|
||||
strictAssert(!done, 'CDSI socket expected handshake data');
|
||||
|
||||
this.#cdsClient.completeHandshake(message);
|
||||
}
|
||||
|
||||
this.state = CDSSocketState.Established;
|
||||
}
|
||||
|
||||
protected override async sendRequest(
|
||||
_version: number,
|
||||
request: Uint8Array
|
||||
): Promise<void> {
|
||||
this.socket.sendBytes(
|
||||
Buffer.from(this.#cdsClient.establishedSend(request))
|
||||
);
|
||||
|
||||
const { done, value: ciphertext } = await this.socketIterator.next();
|
||||
strictAssert(!done, 'CDSISocket.sendRequest(): expected token message');
|
||||
|
||||
const message = await this.decryptResponse(ciphertext);
|
||||
|
||||
this.logger.info('CDSISocket.sendRequest(): processing token message');
|
||||
|
||||
const { token } = Proto.CDSClientResponse.decode(message);
|
||||
strictAssert(token, 'CDSISocket.sendRequest(): expected token');
|
||||
|
||||
this.socket.sendBytes(
|
||||
Buffer.from(
|
||||
this.#cdsClient.establishedSend(
|
||||
Proto.CDSClientRequest.encode({
|
||||
tokenAck: true,
|
||||
}).finish()
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected override async decryptResponse(
|
||||
ciphertext: Uint8Array
|
||||
): Promise<Uint8Array> {
|
||||
return this.#cdsClient.establishedRecv(ciphertext);
|
||||
}
|
||||
|
||||
//
|
||||
// Private
|
||||
//
|
||||
|
||||
get #cdsClient(): Cds2Client {
|
||||
strictAssert(this.#privCdsClient, 'CDSISocket did not start handshake');
|
||||
return this.#privCdsClient;
|
||||
}
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { Readable } from 'node:stream';
|
||||
import lodash from 'lodash';
|
||||
import type { connection as WebSocket } from 'websocket';
|
||||
import Long from 'long';
|
||||
|
||||
import type { LoggerType } from '../../types/Logging.std.js';
|
||||
import { strictAssert } from '../../util/assert.std.js';
|
||||
import { isUntaggedPniString, toTaggedPni } from '../../types/ServiceId.std.js';
|
||||
import { isAciString } from '../../util/isAciString.std.js';
|
||||
import * as Bytes from '../../Bytes.std.js';
|
||||
import { UUID_BYTE_SIZE } from '../../types/Crypto.std.js';
|
||||
import { uuidToBytes, bytesToUuid } from '../../util/uuidToBytes.std.js';
|
||||
import { SignalService as Proto } from '../../protobuf/index.std.js';
|
||||
import type {
|
||||
CDSRequestOptionsType,
|
||||
CDSResponseEntryType,
|
||||
CDSResponseType,
|
||||
} from './Types.d.ts';
|
||||
import { RateLimitedError } from './RateLimitedError.std.js';
|
||||
|
||||
const { noop } = lodash;
|
||||
|
||||
export type CDSSocketBaseOptionsType = Readonly<{
|
||||
logger: LoggerType;
|
||||
socket: WebSocket;
|
||||
}>;
|
||||
|
||||
export enum CDSSocketState {
|
||||
Open = 'Open',
|
||||
Handshake = 'Handshake',
|
||||
Established = 'Established',
|
||||
Closed = 'Closed',
|
||||
}
|
||||
|
||||
const MAX_E164_COUNT = 5000;
|
||||
const E164_BYTE_SIZE = 8;
|
||||
const TRIPLE_BYTE_SIZE = UUID_BYTE_SIZE * 2 + E164_BYTE_SIZE;
|
||||
|
||||
export abstract class CDSSocketBase<
|
||||
Options extends CDSSocketBaseOptionsType = CDSSocketBaseOptionsType,
|
||||
> extends EventEmitter {
|
||||
protected state = CDSSocketState.Open;
|
||||
|
||||
protected readonly socket: WebSocket;
|
||||
|
||||
protected readonly logger: LoggerType;
|
||||
|
||||
protected readonly socketIterator: AsyncIterator<Uint8Array>;
|
||||
|
||||
constructor(protected readonly options: Options) {
|
||||
super();
|
||||
|
||||
// For easier access
|
||||
this.logger = options.logger;
|
||||
this.socket = options.socket;
|
||||
|
||||
this.socketIterator = this.#iterateSocket();
|
||||
}
|
||||
|
||||
public async close(code: number, reason: string): Promise<void> {
|
||||
return this.socket.close(code, reason);
|
||||
}
|
||||
|
||||
public async request({
|
||||
e164s,
|
||||
acisAndAccessKeys,
|
||||
returnAcisWithoutUaks = false,
|
||||
}: CDSRequestOptionsType): Promise<CDSResponseType> {
|
||||
const log = this.logger;
|
||||
|
||||
strictAssert(
|
||||
e164s.length < MAX_E164_COUNT,
|
||||
'CDSSocket does not support paging. Use this for one-off requests'
|
||||
);
|
||||
|
||||
strictAssert(
|
||||
this.state === CDSSocketState.Established,
|
||||
'CDS Connection not established'
|
||||
);
|
||||
|
||||
const version = 2;
|
||||
|
||||
const aciUakPairs = acisAndAccessKeys.map(({ aci, accessKey }) =>
|
||||
Bytes.concatenate([uuidToBytes(aci), Bytes.fromBase64(accessKey)])
|
||||
);
|
||||
|
||||
const request = Proto.CDSClientRequest.encode({
|
||||
newE164s: Bytes.concatenate(
|
||||
e164s.map(e164 => {
|
||||
// Long.fromString handles numbers with or without a leading '+'
|
||||
return new Uint8Array(Long.fromString(e164).toBytesBE());
|
||||
})
|
||||
),
|
||||
aciUakPairs: Bytes.concatenate(aciUakPairs),
|
||||
returnAcisWithoutUaks,
|
||||
}).finish();
|
||||
|
||||
log.info(`CDSSocket.request(): sending version=${version} request`);
|
||||
await this.sendRequest(version, request);
|
||||
|
||||
const resultMap: Map<string, CDSResponseEntryType> = new Map();
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { done, value: ciphertext } = await this.socketIterator.next();
|
||||
if (done) {
|
||||
this.state = CDSSocketState.Closed;
|
||||
break;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const message = await this.decryptResponse(ciphertext);
|
||||
|
||||
log.info('CDSSocket.request(): processing response message');
|
||||
|
||||
const response = Proto.CDSClientResponse.decode(message);
|
||||
|
||||
decodeSingleResponse(resultMap, response);
|
||||
}
|
||||
|
||||
log.info('CDSSocket.request(): done');
|
||||
return { debugPermitsUsed: 0, entries: resultMap };
|
||||
}
|
||||
|
||||
// Abstract methods
|
||||
|
||||
public abstract handshake(): Promise<void>;
|
||||
|
||||
protected abstract sendRequest(
|
||||
version: number,
|
||||
data: Uint8Array
|
||||
): Promise<void>;
|
||||
|
||||
protected abstract decryptResponse(
|
||||
ciphertext: Uint8Array
|
||||
): Promise<Uint8Array>;
|
||||
|
||||
// EventEmitter types
|
||||
|
||||
public override on(
|
||||
type: 'close',
|
||||
callback: (code: number, reason?: string) => void
|
||||
): this;
|
||||
public override on(type: 'error', callback: (error: Error) => void): this;
|
||||
|
||||
public override on(
|
||||
type: string | symbol,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
listener: (...args: Array<any>) => void
|
||||
): this {
|
||||
return super.on(type, listener);
|
||||
}
|
||||
|
||||
public override emit(type: 'close', code: number, reason?: string): boolean;
|
||||
public override emit(type: 'error', error: Error): boolean;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public override emit(type: string | symbol, ...args: Array<any>): boolean {
|
||||
return super.emit(type, ...args);
|
||||
}
|
||||
|
||||
//
|
||||
// Private
|
||||
//
|
||||
|
||||
#iterateSocket(): AsyncIterator<Uint8Array> {
|
||||
const stream = new Readable({ read: noop, objectMode: true });
|
||||
|
||||
this.socket.on('message', ({ type, binaryData }) => {
|
||||
strictAssert(type === 'binary', 'Invalid CDS socket packet');
|
||||
strictAssert(binaryData, 'Invalid CDS socket packet');
|
||||
|
||||
stream.push(binaryData);
|
||||
});
|
||||
|
||||
this.socket.on('close', (code, reason) => {
|
||||
if (code === 1000) {
|
||||
stream.push(null);
|
||||
} else if (code === 4008) {
|
||||
try {
|
||||
const payload = JSON.parse(reason);
|
||||
|
||||
stream.destroy(new RateLimitedError(payload));
|
||||
} catch (error) {
|
||||
stream.destroy(
|
||||
new Error(
|
||||
`Socket closed with code ${code} and reason ${reason}, ` +
|
||||
'but rate limiting response cannot be parsed'
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
stream.destroy(
|
||||
new Error(`Socket closed with code ${code} and reason ${reason}`)
|
||||
);
|
||||
}
|
||||
});
|
||||
this.socket.on('error', (error: Error) => stream.destroy(error));
|
||||
|
||||
return stream[Symbol.asyncIterator]();
|
||||
}
|
||||
}
|
||||
|
||||
function decodeSingleResponse(
|
||||
resultMap: Map<string, CDSResponseEntryType>,
|
||||
response: Proto.CDSClientResponse
|
||||
): void {
|
||||
if (!response.e164PniAciTriples) {
|
||||
return;
|
||||
}
|
||||
for (
|
||||
let i = 0;
|
||||
i < response.e164PniAciTriples.length;
|
||||
i += TRIPLE_BYTE_SIZE
|
||||
) {
|
||||
const tripleBytes = response.e164PniAciTriples.subarray(
|
||||
i,
|
||||
i + TRIPLE_BYTE_SIZE
|
||||
);
|
||||
strictAssert(
|
||||
tripleBytes.length === TRIPLE_BYTE_SIZE,
|
||||
'Invalid size of CDS response triple'
|
||||
);
|
||||
|
||||
let offset = 0;
|
||||
const e164Bytes = tripleBytes.subarray(offset, offset + E164_BYTE_SIZE);
|
||||
offset += E164_BYTE_SIZE;
|
||||
|
||||
const pniBytes = tripleBytes.subarray(offset, offset + UUID_BYTE_SIZE);
|
||||
offset += UUID_BYTE_SIZE;
|
||||
|
||||
const aciBytes = tripleBytes.subarray(offset, offset + UUID_BYTE_SIZE);
|
||||
offset += UUID_BYTE_SIZE;
|
||||
|
||||
const e164Long = Long.fromBytesBE(Array.from(e164Bytes));
|
||||
if (e164Long.isZero()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const e164 = `+${e164Long.toString()}`;
|
||||
const pni = bytesToUuid(pniBytes);
|
||||
const aci = bytesToUuid(aciBytes);
|
||||
strictAssert(
|
||||
aci === undefined || isAciString(aci),
|
||||
'CDSI response has invalid ACI'
|
||||
);
|
||||
strictAssert(
|
||||
pni === undefined || isUntaggedPniString(pni),
|
||||
'CDSI response has invalid PNI'
|
||||
);
|
||||
|
||||
resultMap.set(e164, {
|
||||
pni: pni === undefined ? undefined : toTaggedPni(pni),
|
||||
aci,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user