Improve challenge handling

This commit is contained in:
Fedor Indutny 2022-09-14 17:31:37 -07:00 committed by GitHub
parent e0852abcdc
commit e3f418105b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 92 additions and 52 deletions

View File

@ -18,38 +18,40 @@ import { parseRetryAfterWithDefault } from './util/parseRetryAfter';
import { clearTimeoutIfNecessary } from './util/clearTimeoutIfNecessary'; import { clearTimeoutIfNecessary } from './util/clearTimeoutIfNecessary';
import { getEnvironment, Environment } from './environment'; import { getEnvironment, Environment } from './environment';
import type { StorageInterface } from './types/Storage.d'; import type { StorageInterface } from './types/Storage.d';
import * as Errors from './types/errors';
import { HTTPError } from './textsecure/Errors'; import { HTTPError } from './textsecure/Errors';
import type { SendMessageChallengeData } from './textsecure/Errors'; import type { SendMessageChallengeData } from './textsecure/Errors';
import * as log from './logging/log'; import * as log from './logging/log';
export type ChallengeResponse = { export type ChallengeResponse = Readonly<{
readonly captcha: string; captcha: string;
}; }>;
export type IPCRequest = { export type IPCRequest = Readonly<{
readonly seq: number; seq: number;
}; reason: string;
}>;
export type IPCResponse = { export type IPCResponse = Readonly<{
readonly seq: number; seq: number;
readonly data: ChallengeResponse; data: ChallengeResponse;
}; }>;
type Handler = { type Handler = Readonly<{
readonly token: string | undefined; token: string | undefined;
resolve(response: ChallengeResponse): void; resolve(response: ChallengeResponse): void;
reject(error: Error): void; reject(error: Error): void;
}; }>;
export type ChallengeData = { export type ChallengeData = Readonly<{
readonly type: 'recaptcha'; type: 'recaptcha';
readonly token: string; token: string;
readonly captcha: string; captcha: string;
}; }>;
export type Options = { export type Options = Readonly<{
readonly storage: Pick<StorageInterface, 'get' | 'put'>; storage: Pick<StorageInterface, 'get' | 'put'>;
requestChallenge(request: IPCRequest): void; requestChallenge(request: IPCRequest): void;
@ -63,17 +65,33 @@ export type Options = {
onChallengeFailed(retryAfter?: number): void; onChallengeFailed(retryAfter?: number): void;
expireAfter?: number; expireAfter?: number;
}; }>;
export const STORAGE_KEY = 'challenge:conversations'; export const STORAGE_KEY = 'challenge:conversations';
export type RegisteredChallengeType = Readonly<{ export type RegisteredChallengeType = Readonly<{
conversationId: string; conversationId: string;
createdAt: number; createdAt: number;
reason: string;
retryAt?: number; retryAt?: number;
token?: string; token?: string;
}>; }>;
type SolveOptionsType = Readonly<{
token: string;
reason: string;
}>;
export type MaybeSolveOptionsType = Readonly<{
conversationId: string;
reason: string;
}>;
export type RequestCaptchaOptionsType = Readonly<{
reason: string;
token?: string;
}>;
const DEFAULT_EXPIRE_AFTER = 24 * 3600 * 1000; // one day const DEFAULT_EXPIRE_AFTER = 24 * 3600 * 1000; // one day
const CAPTCHA_URL = 'https://signalcaptchas.org/challenge/generate.html'; const CAPTCHA_URL = 'https://signalcaptchas.org/challenge/generate.html';
const CAPTCHA_STAGING_URL = const CAPTCHA_STAGING_URL =
@ -181,7 +199,7 @@ export class ChallengeHandler {
await this.startAllQueues(); await this.startAllQueues();
} }
public maybeSolve(conversationId: string): void { public maybeSolve({ conversationId, reason }: MaybeSolveOptionsType): void {
const challenge = this.registeredConversations.get(conversationId); const challenge = this.registeredConversations.get(conversationId);
if (!challenge) { if (!challenge) {
return; return;
@ -192,7 +210,7 @@ export class ChallengeHandler {
} }
if (challenge.token) { if (challenge.token) {
this.solve(challenge.token); this.solve({ reason, token: challenge.token });
} }
} }
@ -200,10 +218,11 @@ export class ChallengeHandler {
challenge: RegisteredChallengeType, challenge: RegisteredChallengeType,
data?: SendMessageChallengeData data?: SendMessageChallengeData
): Promise<void> { ): Promise<void> {
const { conversationId } = challenge; const { conversationId, reason } = challenge;
const logId = `challenge(${reason})`;
if (this.isRegistered(conversationId)) { if (this.isRegistered(conversationId)) {
log.info(`challenge: conversation ${conversationId} already registered`); log.info(`${logId}: conversation ${conversationId} already registered`);
return; return;
} }
@ -212,9 +231,7 @@ export class ChallengeHandler {
// Challenge is already retryable - start the queue // Challenge is already retryable - start the queue
if (shouldStartQueue(challenge)) { if (shouldStartQueue(challenge)) {
log.info( log.info(`${logId}: starting conversation ${conversationId} immediately`);
`challenge: starting conversation ${conversationId} immediately`
);
await this.startQueue(conversationId); await this.startQueue(conversationId);
return; return;
} }
@ -234,27 +251,25 @@ export class ChallengeHandler {
}, waitTime) }, waitTime)
); );
log.info( log.info(
`challenge: tracking ${conversationId} with waitTime=${waitTime}` `${logId}: tracking ${conversationId} with waitTime=${waitTime}`
); );
} else { } else {
log.info(`challenge: tracking ${conversationId} with no waitTime`); log.info(`${logId}: tracking ${conversationId} with no waitTime`);
} }
if (data && !data.options?.includes('recaptcha')) { if (data && !data.options?.includes('recaptcha')) {
log.error( log.error(`${logId}: unexpected options ${JSON.stringify(data.options)}`);
`challenge: unexpected options ${JSON.stringify(data.options)}`
);
} }
if (!challenge.token) { if (!challenge.token) {
const dataString = JSON.stringify(data); const dataString = JSON.stringify(data);
log.error( log.error(
`challenge: ${conversationId} is waiting; no token in data ${dataString}` `${logId}: ${conversationId} is waiting; no token in data ${dataString}`
); );
return; return;
} }
this.solve(challenge.token); this.solve({ token: challenge.token, reason });
} }
public onResponse(response: IPCResponse): void { public onResponse(response: IPCResponse): void {
@ -279,8 +294,11 @@ export class ChallengeHandler {
await this.persist(); await this.persist();
} }
public async requestCaptcha(token = ''): Promise<string> { public async requestCaptcha({
const request: IPCRequest = { seq: this.seq }; reason,
token = '',
}: RequestCaptchaOptionsType): Promise<string> {
const request: IPCRequest = { seq: this.seq, reason };
this.seq += 1; this.seq += 1;
this.options.requestChallenge(request); this.options.requestChallenge(request);
@ -335,12 +353,12 @@ export class ChallengeHandler {
this.options.startQueue(conversationId); this.options.startQueue(conversationId);
} }
private async solve(token: string): Promise<void> { private async solve({ reason, token }: SolveOptionsType): Promise<void> {
this.solving += 1; this.solving += 1;
this.options.setChallengeStatus('required'); this.options.setChallengeStatus('required');
this.challengeToken = token; this.challengeToken = token;
const captcha = await this.requestCaptcha(token); const captcha = await this.requestCaptcha({ reason, token });
// Another `.solve()` has completed earlier than us // Another `.solve()` has completed earlier than us
if (this.challengeToken === undefined) { if (this.challengeToken === undefined) {
@ -353,7 +371,7 @@ export class ChallengeHandler {
this.options.setChallengeStatus('pending'); this.options.setChallengeStatus('pending');
log.info('challenge: sending challenge to server'); log.info(`challenge(${reason}): sending challenge to server`);
try { try {
await this.sendChallengeResponse({ await this.sendChallengeResponse({
@ -362,13 +380,16 @@ export class ChallengeHandler {
captcha, captcha,
}); });
} catch (error) { } catch (error) {
log.error(`challenge: challenge failure, error: ${error && error.stack}`); log.error(
`challenge(${reason}): challenge failure, error:`,
Errors.toLogFormat(error)
);
this.options.setChallengeStatus('required'); this.options.setChallengeStatus('required');
this.solving -= 1; this.solving -= 1;
return; return;
} }
log.info('challenge: challenge success. force sending'); log.info(`challenge(${reason}): challenge success. force sending`);
this.options.setChallengeStatus('idle'); this.options.setChallengeStatus('idle');

View File

@ -133,7 +133,9 @@ export const StandaloneRegistration = ({
setError('Captcha handler is not ready!'); setError('Captcha handler is not ready!');
return; return;
} }
const token = await window.Signal.challengeHandler.requestCaptcha(); const token = await window.Signal.challengeHandler.requestCaptcha({
reason: 'standalone registration',
});
try { try {
requestVerification(type, number, token); requestVerification(type, number, token);

View File

@ -157,12 +157,15 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
data: Readonly<ConversationQueueJobData>, data: Readonly<ConversationQueueJobData>,
insert?: (job: ParsedJob<ConversationQueueJobData>) => Promise<void> insert?: (job: ParsedJob<ConversationQueueJobData>) => Promise<void>
): Promise<Job<ConversationQueueJobData>> { ): Promise<Job<ConversationQueueJobData>> {
const { conversationId } = data; const { conversationId, type } = data;
strictAssert( strictAssert(
window.Signal.challengeHandler, window.Signal.challengeHandler,
'conversationJobQueue.add: Missing challengeHandler!' 'conversationJobQueue.add: Missing challengeHandler!'
); );
window.Signal.challengeHandler.maybeSolve(conversationId); window.Signal.challengeHandler.maybeSolve({
conversationId,
reason: `conversationJobQueue.add(${conversationId}, ${type})`,
});
return super.add(data, insert); return super.add(data, insert);
} }
@ -382,6 +385,9 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
createdAt: Date.now(), createdAt: Date.now(),
retryAt: toProcess.retryAt, retryAt: toProcess.retryAt,
token: toProcess.data?.token, token: toProcess.data?.token,
reason:
'conversationJobQueue.run(' +
`${conversation.idForLogging()}, ${type}, ${timestamp})`,
}, },
toProcess.data toProcess.data
); );

View File

@ -1,10 +1,10 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable no-console */
import type { IpcMainEvent } from 'electron'; import type { IpcMainEvent } from 'electron';
import { ipcMain as ipc } from 'electron'; import { ipcMain as ipc } from 'electron';
import * as log from '../logging/log';
import type { IPCRequest, IPCResponse, ChallengeResponse } from '../challenge'; import type { IPCRequest, IPCResponse, ChallengeResponse } from '../challenge';
export class ChallengeMainHandler { export class ChallengeMainHandler {
@ -19,6 +19,12 @@ export class ChallengeMainHandler {
const { handlers } = this; const { handlers } = this;
this.handlers = []; this.handlers = [];
log.info(
'challengeMain.handleCaptcha: sending captcha response to ' +
`${handlers.length} handlers`,
response
);
for (const resolve of handlers) { for (const resolve of handlers) {
resolve(response); resolve(response);
} }
@ -28,13 +34,17 @@ export class ChallengeMainHandler {
event: IpcMainEvent, event: IpcMainEvent,
request: IPCRequest request: IPCRequest
): Promise<void> { ): Promise<void> {
console.log('Received challenge request, waiting for response'); const logId = `challengeMain.onRequest(${request.reason})`;
log.info(`${logId}: received challenge request, waiting for response`);
const start = Date.now();
const data = await new Promise<ChallengeResponse>(resolve => { const data = await new Promise<ChallengeResponse>(resolve => {
this.handlers.push(resolve); this.handlers.push(resolve);
}); });
console.log('Sending challenge response', data); const duration = Date.now() - start;
log.info(`${logId}: got response after ${duration}ms`);
const ipcResponse: IPCResponse = { const ipcResponse: IPCResponse = {
seq: request.seq, seq: request.seq,

View File

@ -56,6 +56,7 @@ describe('ChallengeHandler', () => {
token: '1', token: '1',
retryAt: NOW + DEFAULT_RETRY_AFTER, retryAt: NOW + DEFAULT_RETRY_AFTER,
createdAt: NOW - SECOND, createdAt: NOW - SECOND,
reason: 'test',
...options, ...options,
}; };
}; };

View File

@ -144,10 +144,10 @@ export class SendMessageNetworkError extends ReplayableError {
} }
} }
export type SendMessageChallengeData = { export type SendMessageChallengeData = Readonly<{
readonly token?: string; token?: string;
readonly options?: ReadonlyArray<string>; options?: ReadonlyArray<string>;
}; }>;
export class SendMessageChallengeError extends ReplayableError { export class SendMessageChallengeError extends ReplayableError {
public identifier: string; public identifier: string;