More protobufjs use
This commit is contained in:
parent
1fa0e6c8c1
commit
299fe2af36
20 changed files with 646 additions and 447 deletions
|
@ -1687,6 +1687,210 @@ Signal Desktop makes use of the following open source projects.
|
|||
licenses; we recommend you read them, as their terms may differ from the
|
||||
terms above.
|
||||
|
||||
## long
|
||||
|
||||
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
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
## lru-cache
|
||||
|
||||
The ISC License
|
||||
|
|
|
@ -104,6 +104,7 @@
|
|||
"js-yaml": "3.13.1",
|
||||
"linkify-it": "2.2.0",
|
||||
"lodash": "4.17.21",
|
||||
"long": "4.0.0",
|
||||
"lru-cache": "6.0.0",
|
||||
"mac-screen-capture-permissions": "2.0.0",
|
||||
"memoizee": "0.4.14",
|
||||
|
|
|
@ -11,6 +11,7 @@ const { noop, uniqBy } = require('lodash');
|
|||
const pMap = require('p-map');
|
||||
const client = require('@signalapp/signal-client');
|
||||
const { deriveStickerPackKey } = require('../ts/Crypto');
|
||||
const { SignalService: Proto } = require('../ts/protobuf');
|
||||
const {
|
||||
getEnvironment,
|
||||
setEnvironment,
|
||||
|
@ -219,24 +220,24 @@ window.encryptAndUpload = async (
|
|||
'imageData'
|
||||
);
|
||||
|
||||
const manifestProto = new window.textsecure.protobuf.StickerPack();
|
||||
const manifestProto = new Proto.StickerPack();
|
||||
manifestProto.title = manifest.title;
|
||||
manifestProto.author = manifest.author;
|
||||
manifestProto.stickers = stickers.map(({ emoji }, id) => {
|
||||
const s = new window.textsecure.protobuf.StickerPack.Sticker();
|
||||
const s = new Proto.StickerPack.Sticker();
|
||||
s.id = id;
|
||||
s.emoji = emoji;
|
||||
|
||||
return s;
|
||||
});
|
||||
const coverSticker = new window.textsecure.protobuf.StickerPack.Sticker();
|
||||
const coverSticker = new Proto.StickerPack.Sticker();
|
||||
coverSticker.id =
|
||||
uniqueStickers.length === stickers.length ? 0 : uniqueStickers.length - 1;
|
||||
coverSticker.emoji = '';
|
||||
manifestProto.cover = coverSticker;
|
||||
|
||||
const encryptedManifest = await encrypt(
|
||||
manifestProto.toArrayBuffer(),
|
||||
Proto.StickerPack.encode(manifestProto).finish(),
|
||||
encryptionKey,
|
||||
iv
|
||||
);
|
||||
|
|
|
@ -59,6 +59,7 @@ import {
|
|||
SystemTraySetting,
|
||||
parseSystemTraySetting,
|
||||
} from './types/SystemTraySetting';
|
||||
import { SignalService as Proto } from './protobuf';
|
||||
|
||||
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
||||
|
||||
|
@ -119,15 +120,12 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
const reconnectBackOff = new BackOff(FIBONACCI_TIMEOUTS);
|
||||
|
||||
window.textsecure.protobuf.onLoad(() => {
|
||||
window.storage.onready(() => {
|
||||
senderCertificateService.initialize({
|
||||
WebAPI: window.WebAPI,
|
||||
navigator,
|
||||
onlineEventTarget: window,
|
||||
SenderCertificate: window.textsecure.protobuf.SenderCertificate,
|
||||
storage: window.storage,
|
||||
});
|
||||
window.storage.onready(() => {
|
||||
senderCertificateService.initialize({
|
||||
WebAPI: window.WebAPI,
|
||||
navigator,
|
||||
onlineEventTarget: window,
|
||||
storage: window.storage,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -2876,7 +2874,7 @@ export async function startApp(): Promise<void> {
|
|||
destinationUuid: data.sourceUuid,
|
||||
});
|
||||
|
||||
const { PROFILE_KEY_UPDATE } = window.textsecure.protobuf.DataMessage.Flags;
|
||||
const { PROFILE_KEY_UPDATE } = Proto.DataMessage.Flags;
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const isProfileUpdate = Boolean(data.message.flags & PROFILE_KEY_UPDATE);
|
||||
if (isProfileUpdate) {
|
||||
|
@ -3198,7 +3196,7 @@ export async function startApp(): Promise<void> {
|
|||
sourceUuid: window.textsecure.storage.user.getUuid(),
|
||||
});
|
||||
|
||||
const { PROFILE_KEY_UPDATE } = window.textsecure.protobuf.DataMessage.Flags;
|
||||
const { PROFILE_KEY_UPDATE } = Proto.DataMessage.Flags;
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const isProfileUpdate = Boolean(data.message.flags & PROFILE_KEY_UPDATE);
|
||||
if (isProfileUpdate) {
|
||||
|
@ -3514,9 +3512,7 @@ export async function startApp(): Promise<void> {
|
|||
);
|
||||
|
||||
try {
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
const result = await window.textsecure.messaging.sendSenderKeyDistributionMessage(
|
||||
{
|
||||
|
@ -3739,9 +3735,7 @@ export async function startApp(): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
// 3. Determine how to represent this to the user. Three different options.
|
||||
|
||||
|
@ -3844,8 +3838,7 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
const { eventType } = ev;
|
||||
|
||||
const FETCH_LATEST_ENUM =
|
||||
window.textsecure.protobuf.SyncMessage.FetchLatest.Type;
|
||||
const FETCH_LATEST_ENUM = Proto.SyncMessage.FetchLatest.Type;
|
||||
|
||||
switch (eventType) {
|
||||
case FETCH_LATEST_ENUM.LOCAL_PROFILE:
|
||||
|
@ -4008,13 +4001,13 @@ export async function startApp(): Promise<void> {
|
|||
}
|
||||
|
||||
switch (ev.verified.state) {
|
||||
case window.textsecure.protobuf.Verified.State.DEFAULT:
|
||||
case Proto.Verified.State.DEFAULT:
|
||||
state = 'DEFAULT';
|
||||
break;
|
||||
case window.textsecure.protobuf.Verified.State.VERIFIED:
|
||||
case Proto.Verified.State.VERIFIED:
|
||||
state = 'VERIFIED';
|
||||
break;
|
||||
case window.textsecure.protobuf.Verified.State.UNVERIFIED:
|
||||
case Proto.Verified.State.UNVERIFIED:
|
||||
state = 'UNVERIFIED';
|
||||
break;
|
||||
default:
|
||||
|
|
|
@ -98,7 +98,7 @@ export async function joinViaLink(hash: string): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
const ACCESS_ENUM = window.textsecure.protobuf.AccessControl.AccessRequired;
|
||||
const ACCESS_ENUM = Proto.AccessControl.AccessRequired;
|
||||
if (
|
||||
result.addFromInviteLink !== ACCESS_ENUM.ADMINISTRATOR &&
|
||||
result.addFromInviteLink !== ACCESS_ENUM.ANY
|
||||
|
|
|
@ -724,7 +724,7 @@ export class ConversationModel extends window.Backbone
|
|||
);
|
||||
}
|
||||
|
||||
const MEMBER_ROLES = window.textsecure.protobuf.Member.Role;
|
||||
const MEMBER_ROLES = Proto.Member.Role;
|
||||
|
||||
const role = this.isAdmin(conversationId)
|
||||
? MEMBER_ROLES.DEFAULT
|
||||
|
@ -1183,9 +1183,7 @@ export class ConversationModel extends window.Backbone
|
|||
}
|
||||
);
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
const sendOptions = await getSendOptions(this.attributes);
|
||||
if (isDirectConversation(this.attributes)) {
|
||||
|
@ -1612,8 +1610,7 @@ export class ConversationModel extends window.Backbone
|
|||
{ fromSync = false, viaStorageServiceSync = false } = {}
|
||||
): Promise<void> {
|
||||
try {
|
||||
const messageRequestEnum =
|
||||
window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type;
|
||||
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
||||
const isLocalAction = !fromSync && !viaStorageServiceSync;
|
||||
const ourConversationId = window.ConversationController.getOurConversationId();
|
||||
|
||||
|
@ -1793,8 +1790,7 @@ export class ConversationModel extends window.Backbone
|
|||
}
|
||||
}
|
||||
|
||||
const messageRequestEnum =
|
||||
window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type;
|
||||
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
||||
|
||||
// Ensure active_at is set, because this is an event that justifies putting the group
|
||||
// in the left pane.
|
||||
|
@ -2901,7 +2897,7 @@ export class ConversationModel extends window.Backbone
|
|||
return false;
|
||||
}
|
||||
|
||||
const MEMBER_ROLES = window.textsecure.protobuf.Member.Role;
|
||||
const MEMBER_ROLES = Proto.Member.Role;
|
||||
|
||||
return member.role === MEMBER_ROLES.ADMINISTRATOR;
|
||||
}
|
||||
|
@ -2916,8 +2912,7 @@ export class ConversationModel extends window.Backbone
|
|||
|
||||
const members = this.get('membersV2') || [];
|
||||
return members.map(member => ({
|
||||
isAdmin:
|
||||
member.role === window.textsecure.protobuf.Member.Role.ADMINISTRATOR,
|
||||
isAdmin: member.role === Proto.Member.Role.ADMINISTRATOR,
|
||||
conversationId: member.conversationId,
|
||||
}));
|
||||
}
|
||||
|
@ -3213,9 +3208,7 @@ export class ConversationModel extends window.Backbone
|
|||
profileKey = await ourProfileKeyService.get();
|
||||
}
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
if (isDirectConversation(this.attributes)) {
|
||||
return window.textsecure.messaging.sendMessageToIdentifier({
|
||||
|
@ -3363,9 +3356,7 @@ export class ConversationModel extends window.Backbone
|
|||
}
|
||||
|
||||
const options = await getSendOptions(this.attributes);
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
const promise = (() => {
|
||||
if (isDirectConversation(this.attributes)) {
|
||||
|
@ -3621,9 +3612,7 @@ export class ConversationModel extends window.Backbone
|
|||
|
||||
const conversationType = this.get('type');
|
||||
const options = await getSendOptions(this.attributes);
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
let promise;
|
||||
if (conversationType === Message.GROUP) {
|
||||
|
@ -3845,7 +3834,7 @@ export class ConversationModel extends window.Backbone
|
|||
value
|
||||
);
|
||||
|
||||
const ACCESS_ENUM = window.textsecure.protobuf.AccessControl.AccessRequired;
|
||||
const ACCESS_ENUM = Proto.AccessControl.AccessRequired;
|
||||
const addFromInviteLink = value
|
||||
? ACCESS_ENUM.ANY
|
||||
: ACCESS_ENUM.UNSATISFIABLE;
|
||||
|
@ -3889,7 +3878,7 @@ export class ConversationModel extends window.Backbone
|
|||
return;
|
||||
}
|
||||
|
||||
const ACCESS_ENUM = window.textsecure.protobuf.AccessControl.AccessRequired;
|
||||
const ACCESS_ENUM = Proto.AccessControl.AccessRequired;
|
||||
|
||||
const addFromInviteLink = value
|
||||
? ACCESS_ENUM.ADMINISTRATOR
|
||||
|
@ -3927,7 +3916,7 @@ export class ConversationModel extends window.Backbone
|
|||
),
|
||||
});
|
||||
|
||||
const ACCESS_ENUM = window.textsecure.protobuf.AccessControl.AccessRequired;
|
||||
const ACCESS_ENUM = Proto.AccessControl.AccessRequired;
|
||||
this.set({
|
||||
accessControl: {
|
||||
addFromInviteLink:
|
||||
|
@ -3952,7 +3941,7 @@ export class ConversationModel extends window.Backbone
|
|||
),
|
||||
});
|
||||
|
||||
const ACCESS_ENUM = window.textsecure.protobuf.AccessControl.AccessRequired;
|
||||
const ACCESS_ENUM = Proto.AccessControl.AccessRequired;
|
||||
this.set({
|
||||
accessControl: {
|
||||
addFromInviteLink:
|
||||
|
@ -4030,8 +4019,7 @@ export class ConversationModel extends window.Backbone
|
|||
sent_at: timestamp,
|
||||
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||
received_at_ms: timestamp,
|
||||
flags:
|
||||
window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||
flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||
expirationTimerUpdate: {
|
||||
expireTimer,
|
||||
source,
|
||||
|
@ -4068,8 +4056,7 @@ export class ConversationModel extends window.Backbone
|
|||
let promise;
|
||||
|
||||
if (isMe(this.attributes)) {
|
||||
const flags =
|
||||
window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
|
||||
const flags = Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
|
||||
const dataMessage = await window.textsecure.messaging.getDataMessage({
|
||||
attachments: [],
|
||||
// body
|
||||
|
@ -4166,7 +4153,7 @@ export class ConversationModel extends window.Backbone
|
|||
destination: this.get('e164'),
|
||||
destinationUuid: this.get('uuid'),
|
||||
recipients: this.getRecipients(),
|
||||
flags: window.textsecure.protobuf.DataMessage.Flags.END_SESSION,
|
||||
flags: Proto.DataMessage.Flags.END_SESSION,
|
||||
// TODO: DESKTOP-722
|
||||
} as unknown) as MessageAttributesType);
|
||||
|
||||
|
@ -4887,8 +4874,7 @@ export class ConversationModel extends window.Backbone
|
|||
return true;
|
||||
}
|
||||
|
||||
const accessControlEnum =
|
||||
window.textsecure.protobuf.AccessControl.AccessRequired;
|
||||
const accessControlEnum = Proto.AccessControl.AccessRequired;
|
||||
const accessControl = this.get('accessControl');
|
||||
const canAnyoneChangeTimer =
|
||||
accessControl &&
|
||||
|
@ -4913,7 +4899,7 @@ export class ConversationModel extends window.Backbone
|
|||
return (
|
||||
this.areWeAdmin() ||
|
||||
this.get('accessControl')?.attributes ===
|
||||
window.textsecure.protobuf.AccessControl.AccessRequired.MEMBER
|
||||
Proto.AccessControl.AccessRequired.MEMBER
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -4922,7 +4908,7 @@ export class ConversationModel extends window.Backbone
|
|||
return false;
|
||||
}
|
||||
|
||||
const memberEnum = window.textsecure.protobuf.Member.Role;
|
||||
const memberEnum = Proto.Member.Role;
|
||||
const members = this.get('membersV2') || [];
|
||||
const myId = window.ConversationController.getOurConversationId();
|
||||
const me = members.find(item => item.conversationId === myId);
|
||||
|
|
|
@ -78,6 +78,7 @@ import { ReadSyncs } from '../messageModifiers/ReadSyncs';
|
|||
import { ViewSyncs } from '../messageModifiers/ViewSyncs';
|
||||
import * as AttachmentDownloads from '../messageModifiers/AttachmentDownloads';
|
||||
import * as LinkPreview from '../types/LinkPreview';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
/* eslint-disable more/no-then */
|
||||
|
@ -175,10 +176,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
);
|
||||
}
|
||||
|
||||
this.CURRENT_PROTOCOL_VERSION =
|
||||
window.textsecure.protobuf.DataMessage.ProtocolVersion.CURRENT;
|
||||
this.INITIAL_PROTOCOL_VERSION =
|
||||
window.textsecure.protobuf.DataMessage.ProtocolVersion.INITIAL;
|
||||
this.CURRENT_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.CURRENT;
|
||||
this.INITIAL_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.INITIAL;
|
||||
this.OUR_NUMBER = window.textsecure.storage.user.getNumber();
|
||||
this.OUR_UUID = window.textsecure.storage.user.getUuid();
|
||||
|
||||
|
@ -439,11 +438,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
}
|
||||
|
||||
if (isGroupV2Change(attributes)) {
|
||||
const { protobuf } = window.textsecure;
|
||||
const change = this.get('groupV2Change');
|
||||
|
||||
const lines = window.Signal.GroupChange.renderChange(change, {
|
||||
AccessControlEnum: protobuf.AccessControl.AccessRequired,
|
||||
AccessControlEnum: Proto.AccessControl.AccessRequired,
|
||||
i18n: window.i18n,
|
||||
ourConversationId: window.ConversationController.getOurConversationId(),
|
||||
renderContact: (conversationId: string) => {
|
||||
|
@ -459,7 +457,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
_i18n: unknown,
|
||||
placeholders: Array<string>
|
||||
) => window.i18n(key, placeholders),
|
||||
RoleEnum: protobuf.Member.Role,
|
||||
RoleEnum: Proto.Member.Role,
|
||||
});
|
||||
|
||||
return { text: lines.join(' ') };
|
||||
|
@ -1285,9 +1283,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
let promise;
|
||||
const options = await getSendOptions(conversation.attributes);
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
if (isDirectConversation(conversation.attributes)) {
|
||||
const [identifier] = recipients;
|
||||
|
@ -1427,9 +1423,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
return this.sendSyncMessageOnly(dataMessage);
|
||||
}
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
const parentConversation = this.getConversation();
|
||||
const groupId = parentConversation?.get('groupId');
|
||||
const {
|
||||
|
@ -1440,7 +1434,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
groupId && isGroupV1(parentConversation?.attributes)
|
||||
? {
|
||||
id: groupId,
|
||||
type: window.textsecure.protobuf.GroupContext.Type.DELIVER,
|
||||
type: Proto.GroupContext.Type.DELIVER,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
|
@ -2385,7 +2379,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
const sourceUuid = message.get('sourceUuid');
|
||||
const type = message.get('type');
|
||||
const conversationId = message.get('conversationId');
|
||||
const GROUP_TYPES = window.textsecure.protobuf.GroupContext.Type;
|
||||
const GROUP_TYPES = Proto.GroupContext.Type;
|
||||
|
||||
const fromContact = this.getContact();
|
||||
if (fromContact) {
|
||||
|
@ -2568,8 +2562,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
const hasGroupV2Prop = Boolean(initialMessage.groupV2);
|
||||
const isV1GroupUpdate =
|
||||
initialMessage.group &&
|
||||
initialMessage.group.type !==
|
||||
window.textsecure.protobuf.GroupContext.Type.DELIVER;
|
||||
initialMessage.group.type !== Proto.GroupContext.Type.DELIVER;
|
||||
|
||||
// Drop an incoming GroupV2 message if we or the sender are not part of the group
|
||||
// after applying the message's associated group changes.
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
// Copyright 2018-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as protobuf from 'protobufjs/minimal';
|
||||
import Long from 'long';
|
||||
|
||||
import { signalservice as SignalService } from './compiled';
|
||||
|
||||
protobuf.util.Long = Long;
|
||||
protobuf.configure();
|
||||
|
||||
export { SignalService };
|
||||
|
|
|
@ -72,6 +72,7 @@ import {
|
|||
} from '../calling/constants';
|
||||
import { notify } from './notify';
|
||||
import { getSendOptions } from '../util/getSendOptions';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
|
||||
const RINGRTC_HTTP_METHOD_TO_OUR_HTTP_METHOD: Map<
|
||||
HttpMethod,
|
||||
|
@ -800,9 +801,7 @@ export class CallingClass {
|
|||
const timestamp = Date.now();
|
||||
|
||||
// We "fire and forget" because sending this message is non-essential.
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
wrapWithSyncMessageSend({
|
||||
conversation,
|
||||
logId: `sendToGroup/groupCallUpdate/${conversationId}-${eraId}`,
|
||||
|
|
|
@ -6,14 +6,18 @@ import {
|
|||
serializedCertificateSchema,
|
||||
SerializedCertificateType,
|
||||
} from '../textsecure/OutgoingMessage';
|
||||
import { SenderCertificateClass } from '../textsecure';
|
||||
import { base64ToArrayBuffer } from '../Crypto';
|
||||
import * as Bytes from '../Bytes';
|
||||
import { typedArrayToArrayBuffer } from '../Crypto';
|
||||
import { assert } from '../util/assert';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { normalizeNumber } from '../util/normalizeNumber';
|
||||
import { waitForOnline } from '../util/waitForOnline';
|
||||
import * as log from '../logging/log';
|
||||
import { connectToServerWithStoredCredentials } from '../util/connectToServerWithStoredCredentials';
|
||||
import { StorageInterface } from '../types/Storage.d';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
|
||||
import SenderCertificate = Proto.SenderCertificate;
|
||||
|
||||
function isWellFormed(data: unknown): data is SerializedCertificateType {
|
||||
return serializedCertificateSchema.safeParse(data).success;
|
||||
|
@ -26,8 +30,6 @@ const CLOCK_SKEW_THRESHOLD = 15 * 60 * 1000;
|
|||
export class SenderCertificateService {
|
||||
private WebAPI?: typeof window.WebAPI;
|
||||
|
||||
private SenderCertificate?: typeof SenderCertificateClass;
|
||||
|
||||
private fetchPromises: Map<
|
||||
SenderCertificateMode,
|
||||
Promise<undefined | SerializedCertificateType>
|
||||
|
@ -40,7 +42,6 @@ export class SenderCertificateService {
|
|||
private storage?: StorageInterface;
|
||||
|
||||
initialize({
|
||||
SenderCertificate,
|
||||
WebAPI,
|
||||
navigator,
|
||||
onlineEventTarget,
|
||||
|
@ -49,12 +50,10 @@ export class SenderCertificateService {
|
|||
WebAPI: typeof window.WebAPI;
|
||||
navigator: Readonly<{ onLine: boolean }>;
|
||||
onlineEventTarget: EventTarget;
|
||||
SenderCertificate: typeof SenderCertificateClass;
|
||||
storage: StorageInterface;
|
||||
}): void {
|
||||
log.info('Sender certificate service initialized');
|
||||
|
||||
this.SenderCertificate = SenderCertificate;
|
||||
this.WebAPI = WebAPI;
|
||||
this.navigator = navigator;
|
||||
this.onlineEventTarget = onlineEventTarget;
|
||||
|
@ -134,9 +133,9 @@ export class SenderCertificateService {
|
|||
private async fetchAndSaveCertificate(
|
||||
mode: SenderCertificateMode
|
||||
): Promise<undefined | SerializedCertificateType> {
|
||||
const { SenderCertificate, storage, navigator, onlineEventTarget } = this;
|
||||
const { storage, navigator, onlineEventTarget } = this;
|
||||
assert(
|
||||
SenderCertificate && storage && navigator && onlineEventTarget,
|
||||
storage && navigator && onlineEventTarget,
|
||||
'Sender certificate service method was called before it was initialized'
|
||||
);
|
||||
|
||||
|
@ -160,12 +159,12 @@ export class SenderCertificateService {
|
|||
);
|
||||
return undefined;
|
||||
}
|
||||
const certificate = base64ToArrayBuffer(certificateString);
|
||||
const certificate = Bytes.fromBase64(certificateString);
|
||||
const decodedContainer = SenderCertificate.decode(certificate);
|
||||
const decodedCert = decodedContainer.certificate
|
||||
? SenderCertificate.Certificate.decode(decodedContainer.certificate)
|
||||
: undefined;
|
||||
const expires = decodedCert?.expires?.toNumber();
|
||||
const expires = normalizeNumber(decodedCert?.expires);
|
||||
|
||||
if (!isExpirationValid(expires)) {
|
||||
log.warn(
|
||||
|
@ -178,7 +177,7 @@ export class SenderCertificateService {
|
|||
|
||||
const serializedCertificate = {
|
||||
expires: expires - CLOCK_SKEW_THRESHOLD,
|
||||
serialized: certificate,
|
||||
serialized: typedArrayToArrayBuffer(certificate),
|
||||
};
|
||||
|
||||
await storage.put(modeToStorageKey(mode), serializedCertificate);
|
||||
|
|
21
ts/test-both/util/normalizeNumber_test.ts
Normal file
21
ts/test-both/util/normalizeNumber_test.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import Long from 'long';
|
||||
|
||||
import { normalizeNumber } from '../../util/normalizeNumber';
|
||||
|
||||
describe('normalizeNumber', () => {
|
||||
it('returns undefined when input is undefined', () => {
|
||||
assert.isUndefined(normalizeNumber(undefined));
|
||||
});
|
||||
|
||||
it('returns number when input is number', () => {
|
||||
assert.strictEqual(normalizeNumber(123), 123);
|
||||
});
|
||||
|
||||
it('returns number when input is Long', () => {
|
||||
assert.strictEqual(normalizeNumber(new Long(123)), 123);
|
||||
});
|
||||
});
|
|
@ -11,8 +11,10 @@ import { assert } from 'chai';
|
|||
import * as sinon from 'sinon';
|
||||
import EventEmitter from 'events';
|
||||
import { connection as WebSocket } from 'websocket';
|
||||
import Long from 'long';
|
||||
|
||||
import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto';
|
||||
import { dropNull } from '../util/dropNull';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
|
||||
import WebSocketResource from '../textsecure/WebsocketResources';
|
||||
|
||||
|
@ -23,23 +25,38 @@ describe('WebSocket-Resource', () => {
|
|||
public close() {}
|
||||
}
|
||||
|
||||
const NOW = Date.now();
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
this.sandbox = sinon.createSandbox();
|
||||
this.clock = this.sandbox.useFakeTimers({
|
||||
now: NOW,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function afterEach() {
|
||||
this.sandbox.restore();
|
||||
});
|
||||
|
||||
describe('requests and responses', () => {
|
||||
it('receives requests and sends responses', done => {
|
||||
// mock socket
|
||||
const requestId = '1';
|
||||
const requestId = new Long(0xdeadbeef, 0x7fffffff);
|
||||
const socket = new FakeSocket();
|
||||
|
||||
sinon.stub(socket, 'sendBytes').callsFake((data: Uint8Array) => {
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
toArrayBuffer(data)
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE
|
||||
);
|
||||
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);
|
||||
assert.strictEqual(message.response?.id.toString(), requestId);
|
||||
const id = message.response?.id;
|
||||
|
||||
if (id instanceof Long) {
|
||||
assert(id.equals(requestId));
|
||||
} else {
|
||||
assert(false, `id should be Long, got ${id}`);
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
|
@ -48,14 +65,7 @@ describe('WebSocket-Resource', () => {
|
|||
handleRequest(request: any) {
|
||||
assert.strictEqual(request.verb, 'PUT');
|
||||
assert.strictEqual(request.path, '/some/path');
|
||||
assert.ok(
|
||||
window.Signal.Crypto.constantTimeEqual(
|
||||
request.body.toArrayBuffer(),
|
||||
window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([1, 2, 3])
|
||||
)
|
||||
)
|
||||
);
|
||||
assert.deepEqual(request.body, new Uint8Array([1, 2, 3]));
|
||||
request.respond(200, 'OK');
|
||||
},
|
||||
});
|
||||
|
@ -63,48 +73,30 @@ describe('WebSocket-Resource', () => {
|
|||
// mock socket request
|
||||
socket.emit('message', {
|
||||
type: 'binary',
|
||||
binaryData: new Uint8Array(
|
||||
new window.textsecure.protobuf.WebSocketMessage({
|
||||
type: window.textsecure.protobuf.WebSocketMessage.Type.REQUEST,
|
||||
request: {
|
||||
id: requestId,
|
||||
verb: 'PUT',
|
||||
path: '/some/path',
|
||||
body: window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([1, 2, 3])
|
||||
),
|
||||
},
|
||||
})
|
||||
.encode()
|
||||
.toArrayBuffer()
|
||||
),
|
||||
binaryData: Proto.WebSocketMessage.encode({
|
||||
type: Proto.WebSocketMessage.Type.REQUEST,
|
||||
request: {
|
||||
id: requestId,
|
||||
verb: 'PUT',
|
||||
path: '/some/path',
|
||||
body: new Uint8Array([1, 2, 3]),
|
||||
},
|
||||
}).finish(),
|
||||
});
|
||||
});
|
||||
|
||||
it('sends requests and receives responses', done => {
|
||||
// mock socket and request handler
|
||||
let requestId: Long | undefined;
|
||||
let requestId: number | Long | undefined;
|
||||
const socket = new FakeSocket();
|
||||
|
||||
sinon.stub(socket, 'sendBytes').callsFake((data: Uint8Array) => {
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
toArrayBuffer(data)
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
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.ok(
|
||||
window.Signal.Crypto.constantTimeEqual(
|
||||
message.request?.body.toArrayBuffer(),
|
||||
window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([1, 2, 3])
|
||||
)
|
||||
)
|
||||
);
|
||||
requestId = message.request?.id;
|
||||
assert.deepEqual(message.request?.body, new Uint8Array([1, 2, 3]));
|
||||
requestId = dropNull(message.request?.id);
|
||||
});
|
||||
|
||||
// actual test
|
||||
|
@ -112,9 +104,7 @@ describe('WebSocket-Resource', () => {
|
|||
resource.sendRequest({
|
||||
verb: 'PUT',
|
||||
path: '/some/path',
|
||||
body: window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([1, 2, 3])
|
||||
),
|
||||
body: new Uint8Array([1, 2, 3]),
|
||||
error: done,
|
||||
success(message: string, status: number) {
|
||||
assert.strictEqual(message, 'OK');
|
||||
|
@ -126,14 +116,10 @@ describe('WebSocket-Resource', () => {
|
|||
// mock socket response
|
||||
socket.emit('message', {
|
||||
type: 'binary',
|
||||
binaryData: new Uint8Array(
|
||||
new window.textsecure.protobuf.WebSocketMessage({
|
||||
type: window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
|
||||
response: { id: requestId, message: 'OK', status: 200 },
|
||||
})
|
||||
.encode()
|
||||
.toArrayBuffer()
|
||||
),
|
||||
binaryData: Proto.WebSocketMessage.encode({
|
||||
type: Proto.WebSocketMessage.Type.RESPONSE,
|
||||
response: { id: requestId, message: 'OK', status: 200 },
|
||||
}).finish(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -147,33 +133,27 @@ describe('WebSocket-Resource', () => {
|
|||
const resource = new WebSocketResource(socket as WebSocket);
|
||||
resource.close();
|
||||
});
|
||||
|
||||
it('force closes the connection', function test(done) {
|
||||
const socket = new FakeSocket();
|
||||
|
||||
const resource = new WebSocketResource(socket as WebSocket);
|
||||
resource.close();
|
||||
|
||||
resource.addEventListener('close', () => done());
|
||||
|
||||
// Wait 5 seconds to forcefully close the connection
|
||||
this.clock.next();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a keepalive config', () => {
|
||||
const NOW = Date.now();
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
this.sandbox = sinon.createSandbox();
|
||||
this.clock = this.sandbox.useFakeTimers({
|
||||
now: NOW,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function afterEach() {
|
||||
this.sandbox.restore();
|
||||
});
|
||||
|
||||
it('sends keepalives once a minute', function test(done) {
|
||||
const socket = new FakeSocket();
|
||||
|
||||
sinon.stub(socket, 'sendBytes').callsFake(data => {
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
toArrayBuffer(data)
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
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();
|
||||
|
@ -190,13 +170,8 @@ describe('WebSocket-Resource', () => {
|
|||
const socket = new FakeSocket();
|
||||
|
||||
sinon.stub(socket, 'sendBytes').callsFake(data => {
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
toArrayBuffer(data)
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
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, '/');
|
||||
done();
|
||||
|
@ -245,13 +220,8 @@ describe('WebSocket-Resource', () => {
|
|||
const socket = new FakeSocket();
|
||||
|
||||
sinon.stub(socket, 'sendBytes').callsFake(data => {
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
toArrayBuffer(data)
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
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(
|
||||
|
|
|
@ -4,33 +4,32 @@
|
|||
// We allow `any`s because it's arduous to set up "real" WebAPIs and storages.
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { assert } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { arrayBufferToBase64 } from '../../Crypto';
|
||||
import { SenderCertificateClass } from '../../textsecure';
|
||||
import * as Bytes from '../../Bytes';
|
||||
import { typedArrayToArrayBuffer } from '../../Crypto';
|
||||
import { SenderCertificateMode } from '../../textsecure/OutgoingMessage';
|
||||
import { SignalService as Proto } from '../../protobuf';
|
||||
|
||||
import { SenderCertificateService } from '../../services/senderCertificate';
|
||||
|
||||
import SenderCertificate = Proto.SenderCertificate;
|
||||
|
||||
describe('SenderCertificateService', () => {
|
||||
const FIFTEEN_MINUTES = 15 * 60 * 1000;
|
||||
|
||||
let fakeValidCertificate: SenderCertificateClass;
|
||||
let fakeValidCertificate: SenderCertificate;
|
||||
let fakeValidCertificateExpiry: number;
|
||||
let fakeServer: any;
|
||||
let fakeWebApi: typeof window.WebAPI;
|
||||
let fakeNavigator: { onLine: boolean };
|
||||
let fakeWindow: EventTarget;
|
||||
let fakeStorage: any;
|
||||
let SenderCertificate: typeof SenderCertificateClass;
|
||||
|
||||
function initializeTestService(): SenderCertificateService {
|
||||
const result = new SenderCertificateService();
|
||||
result.initialize({
|
||||
SenderCertificate,
|
||||
WebAPI: fakeWebApi,
|
||||
navigator: fakeNavigator,
|
||||
onlineEventTarget: fakeWindow,
|
||||
|
@ -39,27 +38,6 @@ describe('SenderCertificateService', () => {
|
|||
return result;
|
||||
}
|
||||
|
||||
before(done => {
|
||||
const protoPath = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'protos',
|
||||
'UnidentifiedDelivery.proto'
|
||||
);
|
||||
fs.readFile(protoPath, 'utf8', (err, proto) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
return;
|
||||
}
|
||||
({ SenderCertificate } = global.window.dcodeIO.ProtoBuf.loadProto(
|
||||
proto
|
||||
).build('signalservice'));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fakeValidCertificate = new SenderCertificate();
|
||||
fakeValidCertificateExpiry = Date.now() + 604800000;
|
||||
|
@ -67,11 +45,15 @@ describe('SenderCertificateService', () => {
|
|||
certificate.expires = global.window.dcodeIO.Long.fromNumber(
|
||||
fakeValidCertificateExpiry
|
||||
);
|
||||
fakeValidCertificate.certificate = certificate.toArrayBuffer();
|
||||
fakeValidCertificate.certificate = SenderCertificate.Certificate.encode(
|
||||
certificate
|
||||
).finish();
|
||||
|
||||
fakeServer = {
|
||||
getSenderCertificate: sinon.stub().resolves({
|
||||
certificate: arrayBufferToBase64(fakeValidCertificate.toArrayBuffer()),
|
||||
certificate: Bytes.toBase64(
|
||||
SenderCertificate.encode(fakeValidCertificate).finish()
|
||||
),
|
||||
}),
|
||||
};
|
||||
fakeWebApi = { connect: sinon.stub().returns(fakeServer) };
|
||||
|
@ -133,12 +115,16 @@ describe('SenderCertificateService', () => {
|
|||
|
||||
assert.deepEqual(await service.get(SenderCertificateMode.WithE164), {
|
||||
expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES,
|
||||
serialized: fakeValidCertificate.toArrayBuffer(),
|
||||
serialized: typedArrayToArrayBuffer(
|
||||
SenderCertificate.encode(fakeValidCertificate).finish()
|
||||
),
|
||||
});
|
||||
|
||||
sinon.assert.calledWithMatch(fakeStorage.put, 'senderCertificate', {
|
||||
expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES,
|
||||
serialized: fakeValidCertificate.toArrayBuffer(),
|
||||
serialized: typedArrayToArrayBuffer(
|
||||
SenderCertificate.encode(fakeValidCertificate).finish()
|
||||
),
|
||||
});
|
||||
|
||||
sinon.assert.calledWith(fakeServer.getSenderCertificate, false);
|
||||
|
@ -149,12 +135,16 @@ describe('SenderCertificateService', () => {
|
|||
|
||||
assert.deepEqual(await service.get(SenderCertificateMode.WithoutE164), {
|
||||
expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES,
|
||||
serialized: fakeValidCertificate.toArrayBuffer(),
|
||||
serialized: typedArrayToArrayBuffer(
|
||||
SenderCertificate.encode(fakeValidCertificate).finish()
|
||||
),
|
||||
});
|
||||
|
||||
sinon.assert.calledWithMatch(fakeStorage.put, 'senderCertificateNoE164', {
|
||||
expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES,
|
||||
serialized: fakeValidCertificate.toArrayBuffer(),
|
||||
serialized: typedArrayToArrayBuffer(
|
||||
SenderCertificate.encode(fakeValidCertificate).finish()
|
||||
),
|
||||
});
|
||||
|
||||
sinon.assert.calledWith(fakeServer.getSenderCertificate, true);
|
||||
|
@ -228,9 +218,13 @@ describe('SenderCertificateService', () => {
|
|||
certificate.expires = global.window.dcodeIO.Long.fromNumber(
|
||||
Date.now() - 1000
|
||||
);
|
||||
expiredCertificate.certificate = certificate.toArrayBuffer();
|
||||
expiredCertificate.certificate = SenderCertificate.Certificate.encode(
|
||||
certificate
|
||||
).finish();
|
||||
fakeServer.getSenderCertificate.resolves({
|
||||
certificate: arrayBufferToBase64(expiredCertificate.toArrayBuffer()),
|
||||
certificate: Bytes.toBase64(
|
||||
SenderCertificate.encode(expiredCertificate).finish()
|
||||
),
|
||||
});
|
||||
|
||||
assert.isUndefined(await service.get(SenderCertificateMode.WithE164));
|
||||
|
|
|
@ -10,17 +10,18 @@ import PQueue from 'p-queue';
|
|||
|
||||
import EventTarget from './EventTarget';
|
||||
import { WebAPIType } from './WebAPI';
|
||||
import MessageReceiver from './MessageReceiver';
|
||||
import { KeyPairType, CompatSignedPreKeyType } from './Types.d';
|
||||
import utils from './Helpers';
|
||||
import ProvisioningCipher from './ProvisioningCipher';
|
||||
import WebSocketResource, {
|
||||
IncomingWebSocketRequest,
|
||||
} from './WebsocketResources';
|
||||
import * as Bytes from '../Bytes';
|
||||
import {
|
||||
deriveAccessKey,
|
||||
generateRegistrationId,
|
||||
getRandomBytes,
|
||||
typedArrayToArrayBuffer,
|
||||
} from '../Crypto';
|
||||
import {
|
||||
generateKeyPair,
|
||||
|
@ -29,13 +30,18 @@ import {
|
|||
} from '../Curve';
|
||||
import { isMoreRecentThan, isOlderThan } from '../util/timestamp';
|
||||
import { ourProfileKeyService } from '../services/ourProfileKey';
|
||||
import { assert } from '../util/assert';
|
||||
import { getProvisioningUrl } from '../util/getProvisioningUrl';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
|
||||
const ARCHIVE_AGE = 30 * 24 * 60 * 60 * 1000;
|
||||
const PREKEY_ROTATION_AGE = 24 * 60 * 60 * 1000;
|
||||
const PROFILE_KEY_LENGTH = 32;
|
||||
const SIGNED_KEY_GEN_BATCH_SIZE = 100;
|
||||
|
||||
// TODO: remove once we move away from ArrayBuffers
|
||||
const FIXMEU8 = Uint8Array;
|
||||
|
||||
function getIdentifier(id: string | undefined) {
|
||||
if (!id || !id.length) {
|
||||
return id;
|
||||
|
@ -100,13 +106,13 @@ export default class AccountManager extends EventTarget {
|
|||
identityKey.pubKey
|
||||
);
|
||||
|
||||
const proto = new window.textsecure.protobuf.DeviceName();
|
||||
proto.ephemeralPublic = encrypted.ephemeralPublic;
|
||||
proto.syntheticIv = encrypted.syntheticIv;
|
||||
proto.ciphertext = encrypted.ciphertext;
|
||||
const proto = new Proto.DeviceName();
|
||||
proto.ephemeralPublic = new FIXMEU8(encrypted.ephemeralPublic);
|
||||
proto.syntheticIv = new FIXMEU8(encrypted.syntheticIv);
|
||||
proto.ciphertext = new FIXMEU8(encrypted.ciphertext);
|
||||
|
||||
const arrayBuffer = proto.encode().toArrayBuffer();
|
||||
return MessageReceiver.arrayBufferToStringBase64(arrayBuffer);
|
||||
const bytes = Proto.DeviceName.encode(proto).finish();
|
||||
return Bytes.toBase64(bytes);
|
||||
}
|
||||
|
||||
async decryptDeviceName(base64: string) {
|
||||
|
@ -115,12 +121,16 @@ export default class AccountManager extends EventTarget {
|
|||
throw new Error('decryptDeviceName: No identity key pair!');
|
||||
}
|
||||
|
||||
const arrayBuffer = MessageReceiver.stringToArrayBufferBase64(base64);
|
||||
const proto = window.textsecure.protobuf.DeviceName.decode(arrayBuffer);
|
||||
const bytes = Bytes.fromBase64(base64);
|
||||
const proto = Proto.DeviceName.decode(bytes);
|
||||
assert(
|
||||
proto.ephemeralPublic && proto.syntheticIv && proto.ciphertext,
|
||||
'Missing required fields in DeviceName'
|
||||
);
|
||||
const encrypted = {
|
||||
ephemeralPublic: proto.ephemeralPublic.toArrayBuffer(),
|
||||
syntheticIv: proto.syntheticIv.toArrayBuffer(),
|
||||
ciphertext: proto.ciphertext.toArrayBuffer(),
|
||||
ephemeralPublic: typedArrayToArrayBuffer(proto.ephemeralPublic),
|
||||
syntheticIv: typedArrayToArrayBuffer(proto.syntheticIv),
|
||||
ciphertext: typedArrayToArrayBuffer(proto.ciphertext),
|
||||
};
|
||||
|
||||
const name = await window.Signal.Crypto.decryptDeviceName(
|
||||
|
@ -223,9 +233,7 @@ export default class AccountManager extends EventTarget {
|
|||
request.verb === 'PUT' &&
|
||||
request.body
|
||||
) {
|
||||
const proto = window.textsecure.protobuf.ProvisioningUuid.decode(
|
||||
request.body
|
||||
);
|
||||
const proto = Proto.ProvisioningUuid.decode(request.body);
|
||||
const { uuid } = proto;
|
||||
if (!uuid) {
|
||||
throw new Error('registerSecondDevice: expected a UUID');
|
||||
|
@ -243,10 +251,7 @@ export default class AccountManager extends EventTarget {
|
|||
request.verb === 'PUT' &&
|
||||
request.body
|
||||
) {
|
||||
const envelope = window.textsecure.protobuf.ProvisionEnvelope.decode(
|
||||
request.body,
|
||||
'binary'
|
||||
);
|
||||
const envelope = Proto.ProvisionEnvelope.decode(request.body);
|
||||
request.respond(200, 'OK');
|
||||
gotProvisionEnvelope = true;
|
||||
wsr.close();
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable no-bitwise */
|
||||
/* eslint-disable more/no-then */
|
||||
import { ByteBufferClass } from '../window.d';
|
||||
import {
|
||||
decryptAes256CbcPkcsPadding,
|
||||
encryptAes256CbcPkcsPadding,
|
||||
|
@ -145,11 +144,9 @@ async function verifyDigest(
|
|||
const Crypto = {
|
||||
// Decrypts message into a raw string
|
||||
async decryptWebsocketMessage(
|
||||
message: ByteBufferClass,
|
||||
decodedMessage: ArrayBuffer,
|
||||
signalingKey: ArrayBuffer
|
||||
): Promise<ArrayBuffer> {
|
||||
const decodedMessage = message.toArrayBuffer();
|
||||
|
||||
if (signalingKey.byteLength !== 52) {
|
||||
throw new Error('Got invalid length signalingKey');
|
||||
}
|
||||
|
|
|
@ -521,11 +521,11 @@ class MessageReceiverInner extends EventTarget {
|
|||
|
||||
if (headers.includes('X-Signal-Key: true')) {
|
||||
plaintext = await Crypto.decryptWebsocketMessage(
|
||||
request.body,
|
||||
typedArrayToArrayBuffer(request.body),
|
||||
this.signalingKey
|
||||
);
|
||||
} else {
|
||||
plaintext = request.body.toArrayBuffer();
|
||||
plaintext = typedArrayToArrayBuffer(request.body);
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -583,7 +583,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
}
|
||||
|
||||
calculateMessageAge(
|
||||
headers: Array<string>,
|
||||
headers: ReadonlyArray<string>,
|
||||
serverTimestamp?: number
|
||||
): number {
|
||||
let messageAgeSec = 0; // Default to 0 in case of unreliable parameters.
|
||||
|
|
|
@ -5,14 +5,19 @@
|
|||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { KeyPairType } from './Types.d';
|
||||
import { ProvisionEnvelopeClass } from '../textsecure.d';
|
||||
import {
|
||||
decryptAes256CbcPkcsPadding,
|
||||
deriveSecrets,
|
||||
bytesFromString,
|
||||
verifyHmacSha256,
|
||||
typedArrayToArrayBuffer,
|
||||
} from '../Crypto';
|
||||
import { calculateAgreement, createKeyPair, generateKeyPair } from '../Curve';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
import { assert } from '../util/assert';
|
||||
|
||||
// TODO: remove once we move away from ArrayBuffers
|
||||
const FIXMEU8 = Uint8Array;
|
||||
|
||||
type ProvisionDecryptResult = {
|
||||
identityKeyPair: KeyPairType;
|
||||
|
@ -28,10 +33,14 @@ class ProvisioningCipherInner {
|
|||
keyPair?: KeyPairType;
|
||||
|
||||
async decrypt(
|
||||
provisionEnvelope: ProvisionEnvelopeClass
|
||||
provisionEnvelope: Proto.ProvisionEnvelope
|
||||
): Promise<ProvisionDecryptResult> {
|
||||
const masterEphemeral = provisionEnvelope.publicKey.toArrayBuffer();
|
||||
const message = provisionEnvelope.body.toArrayBuffer();
|
||||
assert(
|
||||
provisionEnvelope.publicKey && provisionEnvelope.body,
|
||||
'Missing required fields in ProvisionEnvelope'
|
||||
);
|
||||
const masterEphemeral = provisionEnvelope.publicKey;
|
||||
const message = provisionEnvelope.body;
|
||||
if (new Uint8Array(message)[0] !== 1) {
|
||||
throw new Error('Bad version number on ProvisioningMessage');
|
||||
}
|
||||
|
@ -45,25 +54,34 @@ class ProvisioningCipherInner {
|
|||
throw new Error('ProvisioningCipher.decrypt: No keypair!');
|
||||
}
|
||||
|
||||
const ecRes = calculateAgreement(masterEphemeral, this.keyPair.privKey);
|
||||
const ecRes = calculateAgreement(
|
||||
typedArrayToArrayBuffer(masterEphemeral),
|
||||
this.keyPair.privKey
|
||||
);
|
||||
const keys = deriveSecrets(
|
||||
ecRes,
|
||||
new ArrayBuffer(32),
|
||||
bytesFromString('TextSecure Provisioning Message')
|
||||
);
|
||||
await verifyHmacSha256(ivAndCiphertext, keys[1], mac, 32);
|
||||
await verifyHmacSha256(
|
||||
typedArrayToArrayBuffer(ivAndCiphertext),
|
||||
keys[1],
|
||||
typedArrayToArrayBuffer(mac),
|
||||
32
|
||||
);
|
||||
|
||||
const plaintext = await decryptAes256CbcPkcsPadding(
|
||||
keys[0],
|
||||
ciphertext,
|
||||
iv
|
||||
typedArrayToArrayBuffer(ciphertext),
|
||||
typedArrayToArrayBuffer(iv)
|
||||
);
|
||||
const provisionMessage = window.textsecure.protobuf.ProvisionMessage.decode(
|
||||
plaintext
|
||||
const provisionMessage = Proto.ProvisionMessage.decode(
|
||||
new FIXMEU8(plaintext)
|
||||
);
|
||||
const privKey = provisionMessage.identityKeyPrivate.toArrayBuffer();
|
||||
const privKey = provisionMessage.identityKeyPrivate;
|
||||
assert(privKey, 'Missing identityKeyPrivate in ProvisionMessage');
|
||||
|
||||
const keyPair = createKeyPair(privKey);
|
||||
const keyPair = createKeyPair(typedArrayToArrayBuffer(privKey));
|
||||
window.normalizeUuids(
|
||||
provisionMessage,
|
||||
['uuid'],
|
||||
|
@ -79,7 +97,7 @@ class ProvisioningCipherInner {
|
|||
readReceipts: provisionMessage.readReceipts,
|
||||
};
|
||||
if (provisionMessage.profileKey) {
|
||||
ret.profileKey = provisionMessage.profileKey.toArrayBuffer();
|
||||
ret.profileKey = typedArrayToArrayBuffer(provisionMessage.profileKey);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
@ -106,7 +124,7 @@ export default class ProvisioningCipher {
|
|||
}
|
||||
|
||||
decrypt: (
|
||||
provisionEnvelope: ProvisionEnvelopeClass
|
||||
provisionEnvelope: Proto.ProvisionEnvelope
|
||||
) => Promise<ProvisionDecryptResult>;
|
||||
|
||||
getPublicKey: () => Promise<ArrayBuffer>;
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable no-param-reassign */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
/* eslint-disable max-classes-per-file */
|
||||
/*
|
||||
* WebSocket-Resources
|
||||
|
@ -29,228 +26,152 @@
|
|||
|
||||
import { connection as WebSocket, IMessage } from 'websocket';
|
||||
|
||||
import { ByteBufferClass } from '../window.d';
|
||||
import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto';
|
||||
|
||||
import EventTarget from './EventTarget';
|
||||
|
||||
import { dropNull } from '../util/dropNull';
|
||||
import { isOlderThan } from '../util/timestamp';
|
||||
import { strictAssert } from '../util/assert';
|
||||
import { normalizeNumber } from '../util/normalizeNumber';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
|
||||
class Request {
|
||||
verb: string;
|
||||
|
||||
path: string;
|
||||
|
||||
headers: Array<string>;
|
||||
|
||||
body: ByteBufferClass | null;
|
||||
|
||||
success: Function;
|
||||
|
||||
error: Function;
|
||||
|
||||
id: number;
|
||||
|
||||
response?: any;
|
||||
|
||||
constructor(options: any) {
|
||||
this.verb = options.verb || options.type;
|
||||
this.path = options.path || options.url;
|
||||
this.headers = options.headers;
|
||||
this.body = options.body || options.data;
|
||||
this.success = options.success;
|
||||
this.error = options.error;
|
||||
this.id = options.id;
|
||||
|
||||
if (this.id === undefined) {
|
||||
const bits = new Uint32Array(2);
|
||||
window.crypto.getRandomValues(bits);
|
||||
this.id = window.dcodeIO.Long.fromBits(bits[0], bits[1], true);
|
||||
}
|
||||
|
||||
if (this.body === undefined) {
|
||||
this.body = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
type Callback = (
|
||||
message: string,
|
||||
status: number,
|
||||
request: OutgoingWebSocketRequest
|
||||
) => void;
|
||||
|
||||
export class IncomingWebSocketRequest {
|
||||
verb: string;
|
||||
private readonly id: Long | number;
|
||||
|
||||
path: string;
|
||||
public readonly verb: string;
|
||||
|
||||
body: ByteBufferClass | null;
|
||||
public readonly path: string;
|
||||
|
||||
headers: Array<string>;
|
||||
public readonly body: Uint8Array | undefined;
|
||||
|
||||
respond: (status: number, message: string) => void;
|
||||
public readonly headers: ReadonlyArray<string>;
|
||||
|
||||
constructor(options: unknown) {
|
||||
const request = new Request(options);
|
||||
const { socket } = options as { socket: WebSocket };
|
||||
constructor(
|
||||
request: Proto.IWebSocketRequestMessage,
|
||||
private readonly socket: WebSocket
|
||||
) {
|
||||
strictAssert(request.id, 'request without id');
|
||||
strictAssert(request.verb, 'request without verb');
|
||||
strictAssert(request.path, 'request without path');
|
||||
|
||||
this.id = request.id;
|
||||
this.verb = request.verb;
|
||||
this.path = request.path;
|
||||
this.body = request.body;
|
||||
this.headers = request.headers;
|
||||
this.body = dropNull(request.body);
|
||||
this.headers = request.headers || [];
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
this.respond = (status, message) => {
|
||||
const ab = new window.textsecure.protobuf.WebSocketMessage({
|
||||
type: window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
|
||||
response: { id: request.id, message, status },
|
||||
})
|
||||
.encode()
|
||||
.toArrayBuffer();
|
||||
socket.sendBytes(Buffer.from(ab));
|
||||
};
|
||||
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.socket.sendBytes(Buffer.from(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
const outgoing: {
|
||||
[id: number]: Request;
|
||||
} = {};
|
||||
class OutgoingWebSocketRequest {
|
||||
constructor(options: any, socket: WebSocket) {
|
||||
const request = new Request(options);
|
||||
outgoing[request.id] = request;
|
||||
const ab = new window.textsecure.protobuf.WebSocketMessage({
|
||||
type: window.textsecure.protobuf.WebSocketMessage.Type.REQUEST,
|
||||
export type OutgoingWebSocketRequestOptions = Readonly<{
|
||||
verb: string;
|
||||
path: string;
|
||||
body?: Uint8Array;
|
||||
headers?: ReadonlyArray<string>;
|
||||
error?: Callback;
|
||||
success?: Callback;
|
||||
}>;
|
||||
|
||||
export class OutgoingWebSocketRequest {
|
||||
public readonly error: Callback | undefined;
|
||||
|
||||
public readonly success: Callback | undefined;
|
||||
|
||||
public response: Proto.IWebSocketResponseMessage | undefined;
|
||||
|
||||
constructor(
|
||||
id: number,
|
||||
options: OutgoingWebSocketRequestOptions,
|
||||
socket: WebSocket
|
||||
) {
|
||||
this.error = options.error;
|
||||
this.success = options.success;
|
||||
|
||||
const bytes = Proto.WebSocketMessage.encode({
|
||||
type: Proto.WebSocketMessage.Type.REQUEST,
|
||||
request: {
|
||||
verb: request.verb,
|
||||
path: request.path,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
id: request.id,
|
||||
verb: options.verb,
|
||||
path: options.path,
|
||||
body: options.body,
|
||||
headers: options.headers ? options.headers.slice() : undefined,
|
||||
id,
|
||||
},
|
||||
})
|
||||
.encode()
|
||||
.toArrayBuffer();
|
||||
socket.sendBytes(Buffer.from(ab));
|
||||
}).finish();
|
||||
socket.sendBytes(Buffer.from(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
export type WebSocketResourceOptions = {
|
||||
handleRequest?: (request: IncomingWebSocketRequest) => void;
|
||||
keepalive?: KeepAliveOptionsType | true;
|
||||
};
|
||||
|
||||
export default class WebSocketResource extends EventTarget {
|
||||
closed?: boolean;
|
||||
private outgoingId = 1;
|
||||
|
||||
close: (code?: number, reason?: string) => void;
|
||||
private closed?: boolean;
|
||||
|
||||
sendRequest: (options: any) => OutgoingWebSocketRequest;
|
||||
private readonly outgoingMap = new Map<number, OutgoingWebSocketRequest>();
|
||||
|
||||
keepalive?: KeepAlive;
|
||||
private readonly boundOnMessage: (message: IMessage) => void;
|
||||
|
||||
constructor(socket: WebSocket, opts: any = {}) {
|
||||
// Public for tests
|
||||
public readonly keepalive?: KeepAlive;
|
||||
|
||||
constructor(
|
||||
private readonly socket: WebSocket,
|
||||
private readonly options: WebSocketResourceOptions = {}
|
||||
) {
|
||||
super();
|
||||
|
||||
let { handleRequest } = opts;
|
||||
if (typeof handleRequest !== 'function') {
|
||||
handleRequest = (request: IncomingWebSocketRequest) => {
|
||||
request.respond(404, 'Not found');
|
||||
};
|
||||
}
|
||||
this.sendRequest = options => new OutgoingWebSocketRequest(options, socket);
|
||||
this.boundOnMessage = this.onMessage.bind(this);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
const onMessage = ({ type, binaryData }: IMessage): void => {
|
||||
if (type !== 'binary' || !binaryData) {
|
||||
throw new Error(`Unsupported websocket message type: ${type}`);
|
||||
}
|
||||
socket.on('message', this.boundOnMessage);
|
||||
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
toArrayBuffer(binaryData)
|
||||
if (options.keepalive) {
|
||||
const keepalive = new KeepAlive(
|
||||
this,
|
||||
options.keepalive === true ? {} : options.keepalive
|
||||
);
|
||||
if (
|
||||
message.type ===
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST &&
|
||||
message.request
|
||||
) {
|
||||
handleRequest(
|
||||
new IncomingWebSocketRequest({
|
||||
verb: message.request.verb,
|
||||
path: message.request.path,
|
||||
body: message.request.body,
|
||||
headers: message.request.headers,
|
||||
id: message.request.id,
|
||||
socket,
|
||||
})
|
||||
);
|
||||
} else if (
|
||||
message.type ===
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE &&
|
||||
message.response
|
||||
) {
|
||||
const { response } = message;
|
||||
const request = outgoing[response.id];
|
||||
if (request) {
|
||||
request.response = response;
|
||||
let callback = request.error;
|
||||
if (
|
||||
response.status &&
|
||||
response.status >= 200 &&
|
||||
response.status < 300
|
||||
) {
|
||||
callback = request.success;
|
||||
}
|
||||
this.keepalive = keepalive;
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback(response.message, response.status, request);
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`Received response for unknown request ${message.response.id}`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
socket.on('message', onMessage);
|
||||
|
||||
if (opts.keepalive) {
|
||||
this.keepalive = new KeepAlive(this, {
|
||||
path: opts.keepalive.path,
|
||||
disconnect: opts.keepalive.disconnect,
|
||||
});
|
||||
const resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive);
|
||||
|
||||
this.keepalive.reset();
|
||||
|
||||
socket.on('message', resetKeepAliveTimer);
|
||||
socket.on('close', this.keepalive.stop.bind(this.keepalive));
|
||||
keepalive.reset();
|
||||
socket.on('message', () => keepalive.reset());
|
||||
socket.on('close', () => keepalive.stop());
|
||||
}
|
||||
|
||||
socket.on('close', () => {
|
||||
this.closed = true;
|
||||
});
|
||||
}
|
||||
|
||||
this.close = (code = 3000, reason) => {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
public sendRequest(
|
||||
options: OutgoingWebSocketRequestOptions
|
||||
): OutgoingWebSocketRequest {
|
||||
const id = this.outgoingId;
|
||||
strictAssert(!this.outgoingMap.has(id), 'Duplicate outgoing request');
|
||||
|
||||
window.log.info('WebSocketResource.close()');
|
||||
if (this.keepalive) {
|
||||
this.keepalive.stop();
|
||||
}
|
||||
// eslint-disable-next-line no-bitwise
|
||||
this.outgoingId = Math.max(1, (this.outgoingId + 1) & 0x7fffffff);
|
||||
|
||||
socket.close(code, reason);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
socket.removeListener('message', onMessage);
|
||||
const outgoing = new OutgoingWebSocketRequest(id, options, this.socket);
|
||||
this.outgoingMap.set(id, outgoing);
|
||||
|
||||
// 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.
|
||||
setTimeout(() => {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.closed = true;
|
||||
|
||||
window.log.warn('Dispatching our own socket close event');
|
||||
const ev = new Event('close');
|
||||
ev.code = code;
|
||||
ev.reason = reason;
|
||||
this.dispatchEvent(ev);
|
||||
}, 5000);
|
||||
};
|
||||
return outgoing;
|
||||
}
|
||||
|
||||
public forceKeepAlive(): void {
|
||||
|
@ -259,9 +180,83 @@ export default class WebSocketResource extends EventTarget {
|
|||
}
|
||||
this.keepalive.send();
|
||||
}
|
||||
|
||||
public close(code = 3000, reason?: string): void {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.log.info('WebSocketResource.close()');
|
||||
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.
|
||||
setTimeout(() => {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.log.warn('Dispatching our own socket close event');
|
||||
const ev = new Event('close');
|
||||
ev.code = code;
|
||||
ev.reason = reason;
|
||||
this.dispatchEvent(ev);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
private 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'));
|
||||
handleRequest(new IncomingWebSocketRequest(message.request, this.socket));
|
||||
} else if (
|
||||
message.type === Proto.WebSocketMessage.Type.RESPONSE &&
|
||||
message.response
|
||||
) {
|
||||
const { response } = message;
|
||||
strictAssert(response.id, 'response without id');
|
||||
|
||||
const responseId = normalizeNumber(response.id);
|
||||
const request = this.outgoingMap.get(responseId);
|
||||
this.outgoingMap.delete(responseId);
|
||||
|
||||
if (!request) {
|
||||
throw new Error(`Received response for unknown request ${responseId}`);
|
||||
}
|
||||
|
||||
request.response = dropNull(response);
|
||||
|
||||
let callback = request.error;
|
||||
|
||||
const status = response.status ?? -1;
|
||||
if (status >= 200 && status < 300) {
|
||||
callback = request.success;
|
||||
}
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback(response.message ?? '', status, request);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type KeepAliveOptionsType = {
|
||||
export type KeepAliveOptionsType = {
|
||||
path?: string;
|
||||
disconnect?: boolean;
|
||||
};
|
||||
|
|
17
ts/util/normalizeNumber.ts
Normal file
17
ts/util/normalizeNumber.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export function normalizeNumber(value: number | Long): number;
|
||||
export function normalizeNumber(value?: number | Long): number | undefined;
|
||||
|
||||
export function normalizeNumber(value?: number | Long): number | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
|
||||
return value.toNumber();
|
||||
}
|
|
@ -11596,7 +11596,7 @@ loglevel@^1.6.8:
|
|||
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
|
||||
integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==
|
||||
|
||||
long@^4.0.0:
|
||||
long@4.0.0, long@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
|
||||
|
||||
|
|
Loading…
Reference in a new issue