diff --git a/ts/challenge.ts b/ts/challenge.ts index 9232853ad..b1f3f2bee 100644 --- a/ts/challenge.ts +++ b/ts/challenge.ts @@ -14,7 +14,7 @@ import { assert } from './util/assert'; import { isOlderThan } from './util/timestamp'; -import { parseRetryAfter } from './util/parseRetryAfter'; +import { parseRetryAfterWithDefault } from './util/parseRetryAfter'; import { clearTimeoutIfNecessary } from './util/clearTimeoutIfNecessary'; import { getEnvironment, Environment } from './environment'; import type { StorageInterface } from './types/Storage.d'; @@ -70,7 +70,7 @@ export const STORAGE_KEY = 'challenge:conversations'; export type RegisteredChallengeType = Readonly<{ conversationId: string; createdAt: number; - retryAt: number; + retryAt?: number; token?: string; }>; @@ -80,7 +80,12 @@ const CAPTCHA_STAGING_URL = 'https://signalcaptchas.org/staging/challenge/generate.html'; function shouldStartQueue(registered: RegisteredChallengeType): boolean { - if (!registered.retryAt || registered.retryAt <= Date.now()) { + // No retryAt provided; waiting for user to complete captcha + if (!registered.retryAt) { + return false; + } + + if (registered.retryAt <= Date.now()) { return true; } @@ -214,21 +219,26 @@ export class ChallengeHandler { return; } - const waitTime = Math.max(0, challenge.retryAt - Date.now()); - const oldTimer = this.startTimers.get(conversationId); - if (oldTimer) { - clearTimeoutIfNecessary(oldTimer); + if (challenge.retryAt) { + const waitTime = Math.max(0, challenge.retryAt - Date.now()); + const oldTimer = this.startTimers.get(conversationId); + if (oldTimer) { + clearTimeoutIfNecessary(oldTimer); + } + this.startTimers.set( + conversationId, + setTimeout(() => { + this.startTimers.delete(conversationId); + + this.startQueue(conversationId); + }, waitTime) + ); + log.info( + `challenge: tracking ${conversationId} with waitTime=${waitTime}` + ); + } else { + log.info(`challenge: tracking ${conversationId} with no waitTime`); } - this.startTimers.set( - conversationId, - setTimeout(() => { - this.startTimers.delete(conversationId); - - this.startQueue(conversationId); - }, waitTime) - ); - - log.info(`challenge: tracking ${conversationId} with waitTime=${waitTime}`); if (data && !data.options?.includes('recaptcha')) { log.error( @@ -379,7 +389,9 @@ export class ChallengeHandler { throw error; } - const retryAfter = parseRetryAfter(error.responseHeaders['retry-after']); + const retryAfter = parseRetryAfterWithDefault( + error.responseHeaders['retry-after'] + ); log.info(`challenge: retry after ${retryAfter}ms`); this.options.onChallengeFailed(retryAfter); diff --git a/ts/jobs/helpers/findRetryAfterTimeFromError.ts b/ts/jobs/helpers/findRetryAfterTimeFromError.ts index d6ffcfd07..d2303af5b 100644 --- a/ts/jobs/helpers/findRetryAfterTimeFromError.ts +++ b/ts/jobs/helpers/findRetryAfterTimeFromError.ts @@ -3,7 +3,7 @@ import { isRecord } from '../../util/isRecord'; import { HTTPError } from '../../textsecure/Errors'; -import { parseRetryAfter } from '../../util/parseRetryAfter'; +import { parseRetryAfterWithDefault } from '../../util/parseRetryAfter'; export function findRetryAfterTimeFromError(err: unknown): number { let rawValue: unknown; @@ -16,5 +16,5 @@ export function findRetryAfterTimeFromError(err: unknown): number { } } - return parseRetryAfter(rawValue); + return parseRetryAfterWithDefault(rawValue); } diff --git a/ts/test-both/util/parseRetryAfter_test.ts b/ts/test-both/util/parseRetryAfter_test.ts index e0642f0d3..9b3d04b22 100644 --- a/ts/test-both/util/parseRetryAfter_test.ts +++ b/ts/test-both/util/parseRetryAfter_test.ts @@ -4,25 +4,25 @@ import { assert } from 'chai'; import { MINUTE } from '../../util/durations'; -import { parseRetryAfter } from '../../util/parseRetryAfter'; +import { parseRetryAfterWithDefault } from '../../util/parseRetryAfter'; describe('parseRetryAfter', () => { it('should return 1 minute when passed non-strings', () => { - assert.equal(parseRetryAfter(undefined), MINUTE); - assert.equal(parseRetryAfter(1234), MINUTE); + assert.equal(parseRetryAfterWithDefault(undefined), MINUTE); + assert.equal(parseRetryAfterWithDefault(1234), MINUTE); }); it('should return 1 minute with invalid strings', () => { - assert.equal(parseRetryAfter('nope'), MINUTE); - assert.equal(parseRetryAfter('1ff'), MINUTE); + assert.equal(parseRetryAfterWithDefault('nope'), MINUTE); + assert.equal(parseRetryAfterWithDefault('1ff'), MINUTE); }); it('should return milliseconds on valid input', () => { - assert.equal(parseRetryAfter('100'), 100000); + assert.equal(parseRetryAfterWithDefault('100'), 100000); }); it('should return 1 second at minimum', () => { - assert.equal(parseRetryAfter('0'), 1000); - assert.equal(parseRetryAfter('-1'), 1000); + assert.equal(parseRetryAfterWithDefault('0'), 1000); + assert.equal(parseRetryAfterWithDefault('-1'), 1000); }); }); diff --git a/ts/textsecure/Errors.ts b/ts/textsecure/Errors.ts index 39f4e7ecc..58f23fd2c 100644 --- a/ts/textsecure/Errors.ts +++ b/ts/textsecure/Errors.ts @@ -156,7 +156,7 @@ export class SendMessageChallengeError extends ReplayableError { public readonly data: SendMessageChallengeData | undefined; - public readonly retryAt: number; + public readonly retryAt?: number; constructor(identifier: string, httpError: HTTPError) { super({ @@ -171,7 +171,10 @@ export class SendMessageChallengeError extends ReplayableError { const headers = httpError.responseHeaders || {}; - this.retryAt = Date.now() + parseRetryAfter(headers['retry-after']); + const retryAfter = parseRetryAfter(headers['retry-after']); + if (retryAfter) { + this.retryAt = Date.now() + retryAfter; + } appendStack(this, httpError); } diff --git a/ts/util/parseRetryAfter.ts b/ts/util/parseRetryAfter.ts index 546e26381..1b3d4e439 100644 --- a/ts/util/parseRetryAfter.ts +++ b/ts/util/parseRetryAfter.ts @@ -7,15 +7,24 @@ import { isNormalNumber } from './isNormalNumber'; const DEFAULT_RETRY_AFTER = MINUTE; const MINIMAL_RETRY_AFTER = SECOND; -export function parseRetryAfter(value: unknown): number { - if (typeof value !== 'string') { +export function parseRetryAfterWithDefault(value: unknown): number { + const retryAfter = parseRetryAfter(value); + if (retryAfter === undefined) { return DEFAULT_RETRY_AFTER; } + return Math.max(retryAfter, MINIMAL_RETRY_AFTER); +} + +export function parseRetryAfter(value: unknown): number | undefined { + if (typeof value !== 'string') { + return undefined; + } + const retryAfter = parseInt(value, 10); if (!isNormalNumber(retryAfter) || retryAfter.toString() !== value) { - return DEFAULT_RETRY_AFTER; + return undefined; } - return Math.max(retryAfter * SECOND, MINIMAL_RETRY_AFTER); + return retryAfter * SECOND; }