Better types for WebAPI

This commit is contained in:
Fedor Indutny 2021-09-21 17:58:03 -07:00 committed by GitHub
parent c05d23e628
commit b9d6497cb1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 156 additions and 107 deletions

View file

@ -8,6 +8,7 @@ import { render, unstable_batchedUpdates as batchedUpdates } from 'react-dom';
import MessageReceiver from './textsecure/MessageReceiver'; import MessageReceiver from './textsecure/MessageReceiver';
import { SessionResetsType, ProcessedDataMessage } from './textsecure/Types.d'; import { SessionResetsType, ProcessedDataMessage } from './textsecure/Types.d';
import { HTTPError } from './textsecure/Errors';
import { import {
MessageAttributesType, MessageAttributesType,
ConversationAttributesType, ConversationAttributesType,
@ -1797,7 +1798,7 @@ export async function startApp(): Promise<void> {
try { try {
await window.Signal.RemoteConfig.maybeRefreshRemoteConfig(server); await window.Signal.RemoteConfig.maybeRefreshRemoteConfig(server);
} catch (error) { } catch (error) {
if (error && window._.isNumber(error.code)) { if (error instanceof HTTPError) {
log.warn( log.warn(
`registerForActive: Failed to to refresh remote config. Code: ${error.code}` `registerForActive: Failed to to refresh remote config. Code: ${error.code}`
); );
@ -3403,8 +3404,7 @@ export async function startApp(): Promise<void> {
log.error('background onError:', Errors.toLogFormat(error)); log.error('background onError:', Errors.toLogFormat(error));
if ( if (
error && error instanceof HTTPError &&
error.name === 'HTTPError' &&
(error.code === 401 || error.code === 403) (error.code === 401 || error.code === 403)
) { ) {
unlinkAndDisconnect(RemoveAllConfiguration.Full); unlinkAndDisconnect(RemoveAllConfiguration.Full);
@ -3412,8 +3412,7 @@ export async function startApp(): Promise<void> {
} }
if ( if (
error && error instanceof HTTPError &&
error.name === 'HTTPError' &&
(error.code === -1 || error.code === 502) (error.code === -1 || error.code === 502)
) { ) {
// Failed to connect to server // Failed to connect to server

View file

@ -19,6 +19,7 @@ import { isOlderThan } from './util/timestamp';
import { parseRetryAfter } from './util/parseRetryAfter'; import { parseRetryAfter } from './util/parseRetryAfter';
import { getEnvironment, Environment } from './environment'; import { getEnvironment, Environment } from './environment';
import { StorageInterface } from './types/Storage.d'; import { StorageInterface } from './types/Storage.d';
import { HTTPError } from './textsecure/Errors';
import * as log from './logging/log'; import * as log from './logging/log';
export type ChallengeResponse = { export type ChallengeResponse = {
@ -454,8 +455,7 @@ export class ChallengeHandler {
await this.options.sendChallengeResponse(data); await this.options.sendChallengeResponse(data);
} catch (error) { } catch (error) {
if ( if (
!(error instanceof Error) || !(error instanceof HTTPError) ||
error.name !== 'HTTPError' ||
error.code !== 413 || error.code !== 413 ||
!error.responseHeaders !error.responseHeaders
) { ) {

View file

@ -8,6 +8,7 @@ import { action } from '@storybook/addon-actions';
import { times } from 'lodash'; import { times } from 'lodash';
import { setupI18n } from '../../../util/setupI18n'; import { setupI18n } from '../../../util/setupI18n';
import { CapabilityError } from '../../../types/errors';
import enMessages from '../../../../_locales/en/messages.json'; import enMessages from '../../../../_locales/en/messages.json';
import { ConversationDetails, Props } from './ConversationDetails'; import { ConversationDetails, Props } from './ConversationDetails';
import { ConversationType } from '../../../state/ducks/conversations'; import { ConversationType } from '../../../state/ducks/conversations';
@ -152,9 +153,7 @@ story.add('Group add with missing capabilities', () => (
{...createProps()} {...createProps()}
canEditGroupInfo canEditGroupInfo
addMembers={async () => { addMembers={async () => {
const error = new Error(); throw new CapabilityError('stories');
error.code = 'E_NO_CAPABILITY';
throw error;
}} }}
/> />
)); ));

View file

@ -9,6 +9,7 @@ import { getMutedUntilText } from '../../../util/getMutedUntilText';
import { LocalizerType } from '../../../types/Util'; import { LocalizerType } from '../../../types/Util';
import { MediaItemType } from '../../../types/MediaItem'; import { MediaItemType } from '../../../types/MediaItem';
import { CapabilityError } from '../../../types/errors';
import { missingCaseError } from '../../../util/missingCaseError'; import { missingCaseError } from '../../../util/missingCaseError';
import { DisappearingTimerSelect } from '../../DisappearingTimerSelect'; import { DisappearingTimerSelect } from '../../DisappearingTimerSelect';
@ -224,7 +225,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
setModalState(ModalState.NothingOpen); setModalState(ModalState.NothingOpen);
setAddGroupMembersRequestState(RequestState.Inactive); setAddGroupMembersRequestState(RequestState.Inactive);
} catch (err) { } catch (err) {
if (err.code === 'E_NO_CAPABILITY') { if (err instanceof CapabilityError) {
setMembersMissingCapability(true); setMembersMissingCapability(true);
setAddGroupMembersRequestState(RequestState.InactiveWithError); setAddGroupMembersRequestState(RequestState.InactiveWithError);
} else { } else {

View file

@ -3,6 +3,7 @@
import type { LoggerType } from '../../types/Logging'; import type { LoggerType } from '../../types/Logging';
import { parseIntWithFallback } from '../../util/parseIntWithFallback'; import { parseIntWithFallback } from '../../util/parseIntWithFallback';
import { HTTPError } from '../../textsecure/Errors';
import { sleepFor413RetryAfterTimeIfApplicable } from './sleepFor413RetryAfterTimeIfApplicable'; import { sleepFor413RetryAfterTimeIfApplicable } from './sleepFor413RetryAfterTimeIfApplicable';
export async function handleCommonJobRequestError({ export async function handleCommonJobRequestError({
@ -14,7 +15,7 @@ export async function handleCommonJobRequestError({
log: LoggerType; log: LoggerType;
timeRemaining: number; timeRemaining: number;
}>): Promise<void> { }>): Promise<void> {
if (!(err instanceof Error)) { if (!(err instanceof HTTPError)) {
throw err; throw err;
} }

View file

@ -5,6 +5,7 @@ import type { LoggerType } from '../../types/Logging';
import { sleep } from '../../util/sleep'; import { sleep } from '../../util/sleep';
import { parseRetryAfter } from '../../util/parseRetryAfter'; import { parseRetryAfter } from '../../util/parseRetryAfter';
import { isRecord } from '../../util/isRecord'; import { isRecord } from '../../util/isRecord';
import { HTTPError } from '../../textsecure/Errors';
export async function sleepFor413RetryAfterTimeIfApplicable({ export async function sleepFor413RetryAfterTimeIfApplicable({
err, err,
@ -17,7 +18,7 @@ export async function sleepFor413RetryAfterTimeIfApplicable({
}>): Promise<void> { }>): Promise<void> {
if ( if (
timeRemaining <= 0 || timeRemaining <= 0 ||
!(err instanceof Error) || !(err instanceof HTTPError) ||
err.code !== 413 || err.code !== 413 ||
!isRecord(err.responseHeaders) !isRecord(err.responseHeaders)
) { ) {

View file

@ -20,6 +20,7 @@ import { getSendOptions } from '../util/getSendOptions';
import { SignalService as Proto } from '../protobuf'; import { SignalService as Proto } from '../protobuf';
import { handleMessageSend } from '../util/handleMessageSend'; import { handleMessageSend } from '../util/handleMessageSend';
import type { CallbackResultType } from '../textsecure/Types.d'; import type { CallbackResultType } from '../textsecure/Types.d';
import { HTTPError } from '../textsecure/Errors';
import { isSent } from '../messages/MessageSendState'; import { isSent } from '../messages/MessageSendState';
import { getLastChallengeError, isOutgoing } from '../state/selectors/message'; import { getLastChallengeError, isOutgoing } from '../state/selectors/message';
import { parseIntWithFallback } from '../util/parseIntWithFallback'; import { parseIntWithFallback } from '../util/parseIntWithFallback';
@ -340,7 +341,7 @@ export class NormalMessageSendJobQueue extends JobQueue<NormalMessageSendJobData
let maybe413Error: undefined | Error; let maybe413Error: undefined | Error;
messageSendErrors.forEach((messageSendError: unknown) => { messageSendErrors.forEach((messageSendError: unknown) => {
formattedMessageSendErrors.push(Errors.toLogFormat(messageSendError)); formattedMessageSendErrors.push(Errors.toLogFormat(messageSendError));
if (!(messageSendError instanceof Error)) { if (!(messageSendError instanceof HTTPError)) {
return; return;
} }
switch (parseIntWithFallback(messageSendError.code, -1)) { switch (parseIntWithFallback(messageSendError.code, -1)) {

View file

@ -15,6 +15,7 @@ import { JobQueue } from './JobQueue';
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore'; import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
import { parseIntWithFallback } from '../util/parseIntWithFallback'; import { parseIntWithFallback } from '../util/parseIntWithFallback';
import type { WebAPIType } from '../textsecure/WebAPI'; import type { WebAPIType } from '../textsecure/WebAPI';
import { HTTPError } from '../textsecure/Errors';
const RETRY_WAIT_TIME = durations.MINUTE; const RETRY_WAIT_TIME = durations.MINUTE;
const RETRYABLE_4XX_FAILURE_STATUSES = new Set([ const RETRYABLE_4XX_FAILURE_STATUSES = new Set([
@ -83,7 +84,7 @@ export class ReportSpamJobQueue extends JobQueue<ReportSpamJobData> {
map(serverGuids, serverGuid => server.reportMessage(e164, serverGuid)) map(serverGuids, serverGuid => server.reportMessage(e164, serverGuid))
); );
} catch (err: unknown) { } catch (err: unknown) {
if (!(err instanceof Error)) { if (!(err instanceof HTTPError)) {
throw err; throw err;
} }

View file

@ -17,6 +17,7 @@ import {
import { AttachmentType } from '../types/Attachment'; import { AttachmentType } from '../types/Attachment';
import { CallMode, CallHistoryDetailsType } from '../types/Calling'; import { CallMode, CallHistoryDetailsType } from '../types/Calling';
import * as Stickers from '../types/Stickers'; import * as Stickers from '../types/Stickers';
import { CapabilityError } from '../types/errors';
import type { import type {
GroupV1InfoType, GroupV1InfoType,
GroupV2InfoType, GroupV2InfoType,
@ -1889,11 +1890,9 @@ export class ConversationModel extends window.Backbone
return Boolean(model?.get('capabilities')?.announcementGroup); return Boolean(model?.get('capabilities')?.announcementGroup);
}); });
if (!isEveryMemberCapable) { if (!isEveryMemberCapable) {
const error = new Error( throw new CapabilityError(
'addMembersV2: some or all members need to upgrade.' 'addMembersV2: some or all members need to upgrade.'
); );
error.code = 'E_NO_CAPABILITY';
throw error;
} }
} }

View file

@ -1902,7 +1902,7 @@ export class CallingClass {
throw new Error('getCallSettings: offline!'); throw new Error('getCallSettings: offline!');
} }
const iceServerJson = await window.textsecure.messaging.server.getIceServers(); const iceServer = await window.textsecure.messaging.server.getIceServers();
const shouldRelayCalls = window.Events.getAlwaysRelayCalls(); const shouldRelayCalls = window.Events.getAlwaysRelayCalls();
@ -1910,7 +1910,7 @@ export class CallingClass {
const isContactUnknown = !conversation.isFromOrAddedByTrustedContact(); const isContactUnknown = !conversation.isFromOrAddedByTrustedContact();
return { return {
iceServer: JSON.parse(iceServerJson), iceServer,
hideIp: shouldRelayCalls || isContactUnknown, hideIp: shouldRelayCalls || isContactUnknown,
bandwidthMode: BandwidthMode.Normal, bandwidthMode: BandwidthMode.Normal,
}; };

View file

@ -10,7 +10,8 @@ import PQueue from 'p-queue';
import { omit } from 'lodash'; import { omit } from 'lodash';
import EventTarget from './EventTarget'; import EventTarget from './EventTarget';
import { WebAPIType } from './WebAPI'; import type { WebAPIType } from './WebAPI';
import { HTTPError } from './Errors';
import { KeyPairType, CompatSignedPreKeyType } from './Types.d'; import { KeyPairType, CompatSignedPreKeyType } from './Types.d';
import utils from './Helpers'; import utils from './Helpers';
import ProvisioningCipher from './ProvisioningCipher'; import ProvisioningCipher from './ProvisioningCipher';
@ -29,6 +30,7 @@ import {
generateSignedPreKey, generateSignedPreKey,
generatePreKey, generatePreKey,
} from '../Curve'; } from '../Curve';
import { UUID } from '../types/UUID';
import { isMoreRecentThan, isOlderThan } from '../util/timestamp'; import { isMoreRecentThan, isOlderThan } from '../util/timestamp';
import { ourProfileKeyService } from '../services/ourProfileKey'; import { ourProfileKeyService } from '../services/ourProfileKey';
import { assert, strictAssert } from '../util/assert'; import { assert, strictAssert } from '../util/assert';
@ -390,8 +392,7 @@ export default class AccountManager extends EventTarget {
log.error('rotateSignedPrekey error:', e && e.stack ? e.stack : e); log.error('rotateSignedPrekey error:', e && e.stack ? e.stack : e);
if ( if (
e instanceof Error && e instanceof HTTPError &&
e.name === 'HTTPError' &&
e.code && e.code &&
e.code >= 400 && e.code >= 400 &&
e.code <= 599 e.code <= 599
@ -589,7 +590,7 @@ export default class AccountManager extends EventTarget {
// update our own identity key, which may have changed // update our own identity key, which may have changed
// if we're relinking after a reinstall on the master device // if we're relinking after a reinstall on the master device
await storage.protocol.saveIdentityWithAttributes(ourUuid, { await storage.protocol.saveIdentityWithAttributes(new UUID(ourUuid), {
publicKey: identityKeyPair.pubKey, publicKey: identityKeyPair.pubKey,
firstUse: true, firstUse: true,
timestamp: Date.now(), timestamp: Date.now(),

View file

@ -1,7 +1,6 @@
// Copyright 2020-2021 Signal Messenger, LLC // Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
import { parseRetryAfter } from '../util/parseRetryAfter'; import { parseRetryAfter } from '../util/parseRetryAfter';
@ -102,14 +101,14 @@ export class OutgoingIdentityKeyError extends ReplayableError {
export class OutgoingMessageError extends ReplayableError { export class OutgoingMessageError extends ReplayableError {
identifier: string; identifier: string;
code?: any; code?: number;
// Note: Data to resend message is no longer captured // Note: Data to resend message is no longer captured
constructor( constructor(
incomingIdentifier: string, incomingIdentifier: string,
_m: unknown, _m: unknown,
_t: unknown, _t: unknown,
httpError?: Error httpError?: HTTPError
) { ) {
const identifier = incomingIdentifier.split('.')[0]; const identifier = incomingIdentifier.split('.')[0];
@ -128,11 +127,13 @@ export class OutgoingMessageError extends ReplayableError {
} }
export class SendMessageNetworkError extends ReplayableError { export class SendMessageNetworkError extends ReplayableError {
code: number;
identifier: string; identifier: string;
responseHeaders?: HeaderListType | undefined; responseHeaders?: HeaderListType | undefined;
constructor(identifier: string, _m: unknown, httpError: Error) { constructor(identifier: string, _m: unknown, httpError: HTTPError) {
super({ super({
name: 'SendMessageNetworkError', name: 'SendMessageNetworkError',
message: httpError.message, message: httpError.message,
@ -152,13 +153,15 @@ export type SendMessageChallengeData = {
}; };
export class SendMessageChallengeError extends ReplayableError { export class SendMessageChallengeError extends ReplayableError {
public code: number;
public identifier: string; public identifier: string;
public readonly data: SendMessageChallengeData | undefined; public readonly data: SendMessageChallengeData | undefined;
public readonly retryAfter: number; public readonly retryAfter: number;
constructor(identifier: string, httpError: Error) { constructor(identifier: string, httpError: HTTPError) {
super({ super({
name: 'SendMessageChallengeError', name: 'SendMessageChallengeError',
message: httpError.message, message: httpError.message,
@ -166,7 +169,7 @@ export class SendMessageChallengeError extends ReplayableError {
[this.identifier] = identifier.split('.'); [this.identifier] = identifier.split('.');
this.code = httpError.code; this.code = httpError.code;
this.data = httpError.response; this.data = httpError.response as SendMessageChallengeData;
const headers = httpError.responseHeaders || {}; const headers = httpError.responseHeaders || {};
@ -241,9 +244,9 @@ export class SignedPreKeyRotationError extends ReplayableError {
} }
export class MessageError extends ReplayableError { export class MessageError extends ReplayableError {
code?: any; code: number;
constructor(_m: unknown, httpError: Error) { constructor(_m: unknown, httpError: HTTPError) {
super({ super({
name: 'MessageError', name: 'MessageError',
message: httpError.message, message: httpError.message,
@ -258,9 +261,9 @@ export class MessageError extends ReplayableError {
export class UnregisteredUserError extends Error { export class UnregisteredUserError extends Error {
identifier: string; identifier: string;
code?: any; code: number;
constructor(identifier: string, httpError: Error) { constructor(identifier: string, httpError: HTTPError) {
const { message } = httpError; const { message } = httpError;
super(message); super(message);
@ -282,3 +285,5 @@ export class UnregisteredUserError extends Error {
} }
export class ConnectTimeoutError extends Error {} export class ConnectTimeoutError extends Error {}
export class WarnOnlyError extends Error {}

View file

@ -63,6 +63,7 @@ import { IncomingWebSocketRequest } from './WebsocketResources';
import { ContactBuffer, GroupBuffer } from './ContactsParser'; import { ContactBuffer, GroupBuffer } from './ContactsParser';
import type { WebAPIType } from './WebAPI'; import type { WebAPIType } from './WebAPI';
import type { Storage } from './Storage'; import type { Storage } from './Storage';
import { WarnOnlyError } from './Errors';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import { import {
ProcessedDataMessage, ProcessedDataMessage,
@ -922,7 +923,7 @@ export default class MessageReceiver
':', ':',
Errors.toLogFormat(error), Errors.toLogFormat(error),
]; ];
if (error.warn) { if (error instanceof WarnOnlyError) {
log.warn(...args); log.warn(...args);
} else { } else {
log.error(...args); log.error(...args);

View file

@ -21,7 +21,7 @@ import {
UnidentifiedSenderMessageContent, UnidentifiedSenderMessageContent,
} from '@signalapp/signal-client'; } from '@signalapp/signal-client';
import { WebAPIType } from './WebAPI'; import type { WebAPIType } from './WebAPI';
import { SendMetadataType, SendOptionsType } from './SendMessage'; import { SendMetadataType, SendOptionsType } from './SendMessage';
import { import {
OutgoingIdentityKeyError, OutgoingIdentityKeyError,
@ -29,6 +29,7 @@ import {
SendMessageNetworkError, SendMessageNetworkError,
SendMessageChallengeError, SendMessageChallengeError,
UnregisteredUserError, UnregisteredUserError,
HTTPError,
} from './Errors'; } from './Errors';
import { CallbackResultType, CustomError } from './Types.d'; import { CallbackResultType, CustomError } from './Types.d';
import { isValidNumber } from '../types/PhoneNumber'; import { isValidNumber } from '../types/PhoneNumber';
@ -221,7 +222,7 @@ export default class OutgoingMessage {
): void { ): void {
let error = providedError; let error = providedError;
if (!error || (error.name === 'HTTPError' && error.code !== 404)) { if (!error || (error instanceof HTTPError && error.code !== 404)) {
if (error && error.code === 428) { if (error && error.code === 428) {
error = new SendMessageChallengeError(identifier, error); error = new SendMessageChallengeError(identifier, error);
} else { } else {
@ -313,7 +314,7 @@ export default class OutgoingMessage {
} }
return promise.catch(e => { return promise.catch(e => {
if (e.name === 'HTTPError' && e.code !== 409 && e.code !== 410) { if (e instanceof HTTPError && e.code !== 409 && e.code !== 410) {
// 409 and 410 should bubble and be handled by doSendMessage // 409 and 410 should bubble and be handled by doSendMessage
// 404 should throw UnregisteredUserError // 404 should throw UnregisteredUserError
// 428 should throw SendMessageChallengeError // 428 should throw SendMessageChallengeError
@ -517,7 +518,10 @@ export default class OutgoingMessage {
} }
}, },
async (error: Error) => { async (error: Error) => {
if (error.code === 401 || error.code === 403) { if (
error instanceof HTTPError &&
(error.code === 401 || error.code === 403)
) {
if (this.failoverIdentifiers.indexOf(identifier) === -1) { if (this.failoverIdentifiers.indexOf(identifier) === -1) {
this.failoverIdentifiers.push(identifier); this.failoverIdentifiers.push(identifier);
} }
@ -556,8 +560,7 @@ export default class OutgoingMessage {
}) })
.catch(async error => { .catch(async error => {
if ( if (
error instanceof Error && error instanceof HTTPError &&
error.name === 'HTTPError' &&
(error.code === 410 || error.code === 409) (error.code === 410 || error.code === 409)
) { ) {
if (!recurse) { if (!recurse) {
@ -569,15 +572,20 @@ export default class OutgoingMessage {
return undefined; return undefined;
} }
const response = error.response as {
extraDevices?: Array<number>;
staleDevices?: Array<number>;
missingDevices?: Array<number>;
};
let p: Promise<any> = Promise.resolve(); let p: Promise<any> = Promise.resolve();
if (error.code === 409) { if (error.code === 409) {
p = this.removeDeviceIdsForIdentifier( p = this.removeDeviceIdsForIdentifier(
identifier, identifier,
error.response.extraDevices || [] response.extraDevices || []
); );
} else { } else {
p = Promise.all( p = Promise.all(
error.response.staleDevices.map(async (deviceId: number) => { (response.staleDevices || []).map(async (deviceId: number) => {
await window.textsecure.storage.protocol.archiveSession( await window.textsecure.storage.protocol.archiveSession(
new QualifiedAddress( new QualifiedAddress(
ourUuid, ourUuid,
@ -591,8 +599,8 @@ export default class OutgoingMessage {
return p.then(async () => { return p.then(async () => {
const resetDevices = const resetDevices =
error.code === 410 error.code === 410
? error.response.staleDevices ? response.staleDevices
: error.response.missingDevices; : response.missingDevices;
return this.getKeysForIdentifier(identifier, resetDevices).then( return this.getKeysForIdentifier(identifier, resetDevices).then(
// We continue to retry as long as the error code was 409; the assumption is // We continue to retry as long as the error code was 409; the assumption is
// that we'll request new device info and the next request will succeed. // that we'll request new device info and the next request will succeed.
@ -678,7 +686,10 @@ export default class OutgoingMessage {
if (!uuid) { if (!uuid) {
throw new UnregisteredUserError( throw new UnregisteredUserError(
identifier, identifier,
new Error('User is not registered') new HTTPError('User is not registered', {
code: -1,
headers: {},
})
); );
} }
identifier = uuid; identifier = uuid;

View file

@ -9,6 +9,7 @@
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
import { Dictionary } from 'lodash'; import { Dictionary } from 'lodash';
import Long from 'long';
import PQueue from 'p-queue'; import PQueue from 'p-queue';
import { import {
PlaintextContent, PlaintextContent,
@ -54,6 +55,7 @@ import {
MessageError, MessageError,
SignedPreKeyRotationError, SignedPreKeyRotationError,
SendMessageProtoError, SendMessageProtoError,
HTTPError,
} from './Errors'; } from './Errors';
import { BodyRangesType } from '../types/Util'; import { BodyRangesType } from '../types/Util';
import { import {
@ -526,7 +528,7 @@ export default class MessageSender {
const id = await this.server.putAttachment(result.ciphertext); const id = await this.server.putAttachment(result.ciphertext);
const proto = new Proto.AttachmentPointer(); const proto = new Proto.AttachmentPointer();
proto.cdnId = id; proto.cdnId = Long.fromString(id);
proto.contentType = attachment.contentType; proto.contentType = attachment.contentType;
proto.key = new FIXMEU8(key); proto.key = new FIXMEU8(key);
proto.size = attachment.size; proto.size = attachment.size;
@ -563,7 +565,7 @@ export default class MessageSender {
message.attachmentPointers = attachmentPointers; message.attachmentPointers = attachmentPointers;
}) })
.catch(error => { .catch(error => {
if (error instanceof Error && error.name === 'HTTPError') { if (error instanceof HTTPError) {
throw new MessageError(message, error); throw new MessageError(message, error);
} else { } else {
throw error; throw error;
@ -584,7 +586,7 @@ export default class MessageSender {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
message.preview = preview; message.preview = preview;
} catch (error) { } catch (error) {
if (error instanceof Error && error.name === 'HTTPError') { if (error instanceof HTTPError) {
throw new MessageError(message, error); throw new MessageError(message, error);
} else { } else {
throw error; throw error;
@ -609,7 +611,7 @@ export default class MessageSender {
attachmentPointer: await this.makeAttachmentPointer(sticker.data), attachmentPointer: await this.makeAttachmentPointer(sticker.data),
}; };
} catch (error) { } catch (error) {
if (error instanceof Error && error.name === 'HTTPError') { if (error instanceof HTTPError) {
throw new MessageError(message, error); throw new MessageError(message, error);
} else { } else {
throw error; throw error;
@ -637,7 +639,7 @@ export default class MessageSender {
}); });
}) })
).catch(error => { ).catch(error => {
if (error instanceof Error && error.name === 'HTTPError') { if (error instanceof HTTPError) {
throw new MessageError(message, error); throw new MessageError(message, error);
} else { } else {
throw error; throw error;

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import * as log from '../logging/log'; import * as log from '../logging/log';
import { HTTPError } from './Errors';
export async function handleStatusCode(status: number): Promise<void> { export async function handleStatusCode(status: number): Promise<void> {
if (status === 499) { if (status === 499) {
@ -11,7 +12,7 @@ export async function handleStatusCode(status: number): Promise<void> {
} }
} }
export function translateError(error: Error): Error | undefined { export function translateError(error: HTTPError): HTTPError | undefined {
const { code } = error; const { code } = error;
if (code === 200) { if (code === 200) {
// Happens sometimes when we get no response. Might be nice to get 204 instead. // Happens sometimes when we get no response. Might be nice to get 204 instead.

View file

@ -598,7 +598,7 @@ async function _retryAjax(
const limit = providedLimit || 3; const limit = providedLimit || 3;
return _promiseAjax(url, options).catch(async (e: Error) => { return _promiseAjax(url, options).catch(async (e: Error) => {
if (e.name === 'HTTPError' && e.code === -1 && count < limit) { if (e instanceof HTTPError && e.code === -1 && count < limit) {
return new Promise(resolve => { return new Promise(resolve => {
setTimeout(() => { setTimeout(() => {
resolve(_retryAjax(url, options, limit, count)); resolve(_retryAjax(url, options, limit, count));
@ -615,17 +615,6 @@ async function _outerAjax(url: string | null, options: PromiseAjaxOptionsType) {
return _retryAjax(url, options); return _retryAjax(url, options);
} }
declare global {
// We want to extend `Error`, so we need an interface.
// eslint-disable-next-line no-restricted-syntax
interface Error {
code?: number | string;
response?: any;
responseHeaders?: HeaderListType;
warn?: boolean;
}
}
function makeHTTPError( function makeHTTPError(
message: string, message: string,
providedCode: number, providedCode: number,
@ -722,7 +711,7 @@ type InitializeOptionsType = {
version: string; version: string;
}; };
type MessageType = any; type MessageType = unknown;
type AjaxOptionsType = { type AjaxOptionsType = {
accessKey?: string; accessKey?: string;
@ -737,7 +726,7 @@ type AjaxOptionsType = {
password?: string; password?: string;
redactUrl?: RedactUrl; redactUrl?: RedactUrl;
responseType?: 'json' | 'arraybuffer' | 'arraybufferwithdetails'; responseType?: 'json' | 'arraybuffer' | 'arraybufferwithdetails';
schema?: any; schema?: unknown;
timeout?: number; timeout?: number;
unauthenticated?: boolean; unauthenticated?: boolean;
urlParameters?: string; urlParameters?: string;
@ -768,7 +757,7 @@ export type CapabilitiesUploadType = {
changeNumber: true; changeNumber: true;
}; };
type StickerPackManifestType = any; type StickerPackManifestType = ArrayBuffer;
export type GroupCredentialType = { export type GroupCredentialType = {
credential: string; credential: string;
@ -808,6 +797,20 @@ const uploadAvatarHeadersZod = z
.passthrough(); .passthrough();
export type UploadAvatarHeadersType = z.infer<typeof uploadAvatarHeadersZod>; export type UploadAvatarHeadersType = z.infer<typeof uploadAvatarHeadersZod>;
export type ProfileType = Readonly<{
identityKey?: string;
name?: string;
about?: string;
aboutEmoji?: string;
avatar?: string;
unidentifiedAccess?: string;
unrestrictedUnidentifiedAccess?: string;
username?: string;
uuid?: string;
credential?: string;
capabilities?: unknown;
}>;
export type WebAPIType = { export type WebAPIType = {
confirmCode: ( confirmCode: (
number: string, number: string,
@ -816,14 +819,21 @@ export type WebAPIType = {
registrationId: number, registrationId: number,
deviceName?: string | null, deviceName?: string | null,
options?: { accessKey?: ArrayBuffer; uuid?: string } options?: { accessKey?: ArrayBuffer; uuid?: string }
) => Promise<any>; ) => Promise<{ uuid?: string; deviceId: number }>;
createGroup: ( createGroup: (
group: Proto.IGroup, group: Proto.IGroup,
options: GroupCredentialsType options: GroupCredentialsType
) => Promise<void>; ) => Promise<void>;
getAttachment: (cdnKey: string, cdnNumber?: number) => Promise<any>; getAttachment: (cdnKey: string, cdnNumber?: number) => Promise<ArrayBuffer>;
getAvatar: (path: string) => Promise<any>; getAvatar: (path: string) => Promise<ArrayBuffer>;
getDevices: () => Promise<any>; getDevices: () => Promise<
Array<{
id: number;
name: string;
lastSeen: number;
created: number;
}>
>;
getGroup: (options: GroupCredentialsType) => Promise<Proto.Group>; getGroup: (options: GroupCredentialsType) => Promise<Proto.Group>;
getGroupFromLink: ( getGroupFromLink: (
inviteLinkPassword: string, inviteLinkPassword: string,
@ -841,7 +851,11 @@ export type WebAPIType = {
startVersion: number, startVersion: number,
options: GroupCredentialsType options: GroupCredentialsType
) => Promise<GroupLogResponseType>; ) => Promise<GroupLogResponseType>;
getIceServers: () => Promise<any>; getIceServers: () => Promise<{
username: string;
password: string;
urls: Array<string>;
}>;
getKeysForIdentifier: ( getKeysForIdentifier: (
identifier: string, identifier: string,
deviceId?: number deviceId?: number
@ -858,7 +872,7 @@ export type WebAPIType = {
profileKeyVersion?: string; profileKeyVersion?: string;
profileKeyCredentialRequest?: string; profileKeyCredentialRequest?: string;
} }
) => Promise<any>; ) => Promise<ProfileType>;
getProfileUnauth: ( getProfileUnauth: (
identifier: string, identifier: string,
options: { options: {
@ -866,7 +880,7 @@ export type WebAPIType = {
profileKeyVersion?: string; profileKeyVersion?: string;
profileKeyCredentialRequest?: string; profileKeyCredentialRequest?: string;
} }
) => Promise<any>; ) => Promise<ProfileType>;
getProvisioningResource: ( getProvisioningResource: (
handler: IRequestHandler handler: IRequestHandler
) => Promise<WebSocketResource>; ) => Promise<WebSocketResource>;
@ -892,7 +906,13 @@ export type WebAPIType = {
makeProxiedRequest: ( makeProxiedRequest: (
targetUrl: string, targetUrl: string,
options?: ProxiedRequestOptionsType options?: ProxiedRequestOptionsType
) => Promise<any>; ) => Promise<
| ArrayBufferWithDetailsType
| {
result: ArrayBufferWithDetailsType;
totalSize: number;
}
>;
makeSfuRequest: ( makeSfuRequest: (
targetUrl: string, targetUrl: string,
type: HTTPCodeType, type: HTTPCodeType,
@ -905,7 +925,7 @@ export type WebAPIType = {
inviteLinkBase64?: string inviteLinkBase64?: string
) => Promise<Proto.IGroupChange>; ) => Promise<Proto.IGroupChange>;
modifyStorageRecords: MessageSender['modifyStorageRecords']; modifyStorageRecords: MessageSender['modifyStorageRecords'];
putAttachment: (encryptedBin: ArrayBuffer) => Promise<any>; putAttachment: (encryptedBin: ArrayBuffer) => Promise<string>;
putProfile: ( putProfile: (
jsonData: ProfileRequestDataType jsonData: ProfileRequestDataType
) => Promise<UploadAvatarHeadersType | undefined>; ) => Promise<UploadAvatarHeadersType | undefined>;
@ -916,10 +936,10 @@ export type WebAPIType = {
onProgress?: () => void onProgress?: () => void
) => Promise<string>; ) => Promise<string>;
registerKeys: (genKeys: KeysType) => Promise<void>; registerKeys: (genKeys: KeysType) => Promise<void>;
registerSupportForUnauthenticatedDelivery: () => Promise<any>; registerSupportForUnauthenticatedDelivery: () => Promise<void>;
reportMessage: (senderE164: string, serverGuid: string) => Promise<void>; reportMessage: (senderE164: string, serverGuid: string) => Promise<void>;
requestVerificationSMS: (number: string) => Promise<any>; requestVerificationSMS: (number: string) => Promise<void>;
requestVerificationVoice: (number: string) => Promise<any>; requestVerificationVoice: (number: string) => Promise<void>;
sendMessages: ( sendMessages: (
destination: string, destination: string,
messageArray: Array<MessageType>, messageArray: Array<MessageType>,
@ -949,8 +969,11 @@ export type WebAPIType = {
avatarData: Uint8Array, avatarData: Uint8Array,
options: GroupCredentialsType options: GroupCredentialsType
) => Promise<string>; ) => Promise<string>;
whoami: () => Promise<any>; whoami: () => Promise<{
sendChallengeResponse: (challengeResponse: ChallengeType) => Promise<any>; uuid?: string;
number?: string;
}>;
sendChallengeResponse: (challengeResponse: ChallengeType) => Promise<void>;
getConfig: () => Promise< getConfig: () => Promise<
Array<{ name: string; enabled: boolean; value: string | null }> Array<{ name: string; enabled: boolean; value: string | null }>
>; >;
@ -1188,6 +1211,9 @@ export function initialize({
unauthenticated: param.unauthenticated, unauthenticated: param.unauthenticated,
accessKey: param.accessKey, accessKey: param.accessKey,
}).catch((e: Error) => { }).catch((e: Error) => {
if (!(e instanceof HTTPError)) {
throw e;
}
const translatedError = translateError(e); const translatedError = translateError(e);
if (translatedError) { if (translatedError) {
throw translatedError; throw translatedError;
@ -1458,7 +1484,7 @@ export function initialize({
async function getAvatar(path: string) { async function getAvatar(path: string) {
// Using _outerAJAX, since it's not hardcoded to the Signal Server. Unlike our // Using _outerAJAX, since it's not hardcoded to the Signal Server. Unlike our
// attachment CDN, it uses our self-signed certificate, so we pass it in. // attachment CDN, it uses our self-signed certificate, so we pass it in.
return _outerAjax(`${cdnUrlObject['0']}/${path}`, { return (await _outerAjax(`${cdnUrlObject['0']}/${path}`, {
certificateAuthority, certificateAuthority,
contentType: 'application/octet-stream', contentType: 'application/octet-stream',
proxyUrl, proxyUrl,
@ -1470,7 +1496,7 @@ export function initialize({
return href.replace(pattern, `[REDACTED]${path.slice(-3)}`); return href.replace(pattern, `[REDACTED]${path.slice(-3)}`);
}, },
version, version,
}); })) as ArrayBuffer;
} }
async function reportMessage( async function reportMessage(
@ -1571,6 +1597,7 @@ export function initialize({
return _ajax({ return _ajax({
call: 'getIceServers', call: 'getIceServers',
httpType: 'GET', httpType: 'GET',
responseType: 'json',
}); });
} }
@ -1832,7 +1859,7 @@ export function initialize({
if (!isPackIdValid(packId)) { if (!isPackIdValid(packId)) {
throw new Error('getSticker: pack ID was invalid'); throw new Error('getSticker: pack ID was invalid');
} }
return _outerAjax( return (await _outerAjax(
`${cdnUrlObject['0']}/stickers/${packId}/full/${stickerId}`, `${cdnUrlObject['0']}/stickers/${packId}/full/${stickerId}`,
{ {
certificateAuthority, certificateAuthority,
@ -1842,14 +1869,14 @@ export function initialize({
redactUrl: redactStickerUrl, redactUrl: redactStickerUrl,
version, version,
} }
); )) as ArrayBuffer;
} }
async function getStickerPackManifest(packId: string) { async function getStickerPackManifest(packId: string) {
if (!isPackIdValid(packId)) { if (!isPackIdValid(packId)) {
throw new Error('getStickerPackManifest: pack ID was invalid'); throw new Error('getStickerPackManifest: pack ID was invalid');
} }
return _outerAjax( return (await _outerAjax(
`${cdnUrlObject['0']}/stickers/${packId}/manifest.proto`, `${cdnUrlObject['0']}/stickers/${packId}/manifest.proto`,
{ {
certificateAuthority, certificateAuthority,
@ -1859,7 +1886,7 @@ export function initialize({
redactUrl: redactStickerUrl, redactUrl: redactStickerUrl,
version, version,
} }
); )) as ArrayBuffer;
} }
type ServerAttachmentType = { type ServerAttachmentType = {
@ -1989,7 +2016,7 @@ export function initialize({
? cdnUrlObject[cdnNumber] || cdnUrlObject['0'] ? cdnUrlObject[cdnNumber] || cdnUrlObject['0']
: cdnUrlObject['0']; : cdnUrlObject['0'];
// This is going to the CDN, not the service, so we use _outerAjax // This is going to the CDN, not the service, so we use _outerAjax
return _outerAjax(`${cdnUrl}/attachments/${cdnKey}`, { return (await _outerAjax(`${cdnUrl}/attachments/${cdnKey}`, {
certificateAuthority, certificateAuthority,
proxyUrl, proxyUrl,
responseType: 'arraybuffer', responseType: 'arraybuffer',
@ -1997,7 +2024,7 @@ export function initialize({
type: 'GET', type: 'GET',
redactUrl: _createRedactor(cdnKey), redactUrl: _createRedactor(cdnKey),
version, version,
}); })) as ArrayBuffer;
} }
async function putAttachment(encryptedBin: ArrayBuffer) { async function putAttachment(encryptedBin: ArrayBuffer) {
@ -2066,7 +2093,7 @@ export function initialize({
headers.Range = `bytes=${start}-${end}`; headers.Range = `bytes=${start}-${end}`;
} }
const result = await _outerAjax(targetUrl, { const result = (await _outerAjax(targetUrl, {
responseType: returnArrayBuffer ? 'arraybufferwithdetails' : undefined, responseType: returnArrayBuffer ? 'arraybufferwithdetails' : undefined,
proxyUrl: contentProxyUrl, proxyUrl: contentProxyUrl,
type: 'GET', type: 'GET',
@ -2074,7 +2101,7 @@ export function initialize({
redactUrl: () => '[REDACTED_URL]', redactUrl: () => '[REDACTED_URL]',
headers, headers,
version, version,
}); })) as ArrayBufferWithDetailsType;
if (!returnArrayBuffer) { if (!returnArrayBuffer) {
return result; return result;

View file

@ -8,7 +8,7 @@ import {
PublicKey, PublicKey,
} from '@signalapp/signal-client'; } from '@signalapp/signal-client';
import { UnregisteredUserError } from './Errors'; import { UnregisteredUserError, HTTPError } from './Errors';
import { Sessions, IdentityKeys } from '../LibSignalStores'; import { Sessions, IdentityKeys } from '../LibSignalStores';
import { Address } from '../types/Address'; import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress'; import { QualifiedAddress } from '../types/QualifiedAddress';
@ -35,7 +35,7 @@ export async function getKeysForIdentifier(
accessKeyFailed, accessKeyFailed,
}; };
} catch (error) { } catch (error) {
if (error.name === 'HTTPError' && error.code === 404) { if (error instanceof HTTPError && error.code === 404) {
const theirUuid = UUID.lookup(identifier); const theirUuid = UUID.lookup(identifier);
if (theirUuid) { if (theirUuid) {

View file

@ -23,6 +23,7 @@ import {
ProcessedReaction, ProcessedReaction,
ProcessedDelete, ProcessedDelete,
} from './Types.d'; } from './Types.d';
import { WarnOnlyError } from './Errors';
// TODO: remove once we move away from ArrayBuffers // TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array; const FIXMEU8 = Uint8Array;
@ -335,11 +336,9 @@ export async function processDataMessage(
// Cleaned up in `processGroupContext` // Cleaned up in `processGroupContext`
break; break;
default: { default: {
const err = new Error( throw new WarnOnlyError(
`Unknown group message type: ${result.group.type}` `Unknown group message type: ${result.group.type}`
); );
err.warn = true;
throw err;
} }
} }
} }

View file

@ -8,3 +8,5 @@ export function toLogFormat(error: unknown): string {
return String(error); return String(error);
} }
export class CapabilityError extends Error {}

View file

@ -30,6 +30,7 @@ import {
GroupSendOptionsType, GroupSendOptionsType,
SendOptionsType, SendOptionsType,
} from '../textsecure/SendMessage'; } from '../textsecure/SendMessage';
import { HTTPError } from '../textsecure/Errors';
import { IdentityKeys, SenderKeys, Sessions } from '../LibSignalStores'; import { IdentityKeys, SenderKeys, Sessions } from '../LibSignalStores';
import { ConversationModel } from '../models/conversations'; import { ConversationModel } from '../models/conversations';
import { DeviceType, CallbackResultType } from '../textsecure/Types.d'; import { DeviceType, CallbackResultType } from '../textsecure/Types.d';
@ -696,7 +697,7 @@ function isIdentifierRegistered(identifier: string) {
return !isUnregistered; return !isUnregistered;
} }
async function handle409Response(logId: string, error: Error) { async function handle409Response(logId: string, error: HTTPError) {
const parsed = multiRecipient409ResponseSchema.safeParse(error.response); const parsed = multiRecipient409ResponseSchema.safeParse(error.response);
if (parsed.success) { if (parsed.success) {
await _waitForAll({ await _waitForAll({
@ -734,7 +735,7 @@ async function handle409Response(logId: string, error: Error) {
async function handle410Response( async function handle410Response(
conversation: ConversationModel, conversation: ConversationModel,
error: Error error: HTTPError
) { ) {
const logId = conversation.idForLogging(); const logId = conversation.idForLogging();

View file

@ -3,6 +3,7 @@
import * as log from '../logging/log'; import * as log from '../logging/log';
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser'; import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
import { HTTPError } from '../textsecure/Errors';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
const { Whisper } = window; const { Whisper } = window;
@ -47,19 +48,19 @@ Whisper.InstallView = Whisper.View.extend({
if (this.error) { if (this.error) {
if ( if (
this.error.name === 'HTTPError' && this.error instanceof HTTPError &&
this.error.code === TOO_MANY_DEVICES this.error.code === TOO_MANY_DEVICES
) { ) {
errorMessage = window.i18n('installTooManyDevices'); errorMessage = window.i18n('installTooManyDevices');
} else if ( } else if (
this.error.name === 'HTTPError' && this.error instanceof HTTPError &&
this.error.code === TOO_OLD this.error.code === TOO_OLD
) { ) {
errorMessage = window.i18n('installTooOld'); errorMessage = window.i18n('installTooOld');
errorButton = window.i18n('upgrade'); errorButton = window.i18n('upgrade');
errorSecondButton = window.i18n('quit'); errorSecondButton = window.i18n('quit');
} else if ( } else if (
this.error.name === 'HTTPError' && this.error instanceof HTTPError &&
this.error.code === CONNECTION_ERROR this.error.code === CONNECTION_ERROR
) { ) {
errorMessage = window.i18n('installConnectionFailed'); errorMessage = window.i18n('installConnectionFailed');
@ -102,11 +103,7 @@ Whisper.InstallView = Whisper.View.extend({
window.shutdown(); window.shutdown();
}, },
async connect() { async connect() {
if ( if (this.error instanceof HTTPError && this.error.code === TOO_OLD) {
this.error &&
this.error.name === 'HTTPError' &&
this.error.code === TOO_OLD
) {
openLinkInWebBrowser('https://signal.org/download'); openLinkInWebBrowser('https://signal.org/download');
return; return;
} }
@ -142,7 +139,7 @@ Whisper.InstallView = Whisper.View.extend({
if (error.message === 'websocket closed') { if (error.message === 'websocket closed') {
this.trigger('disconnected'); this.trigger('disconnected');
} else if ( } else if (
error.name !== 'HTTPError' || !(error instanceof HTTPError) ||
(error.code !== CONNECTION_ERROR && error.code !== TOO_MANY_DEVICES) (error.code !== CONNECTION_ERROR && error.code !== TOO_MANY_DEVICES)
) { ) {
throw error; throw error;