mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-02-15 07:28:59 +00:00
Use libsignal-net typed Chat API for lookup by username
Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
@@ -15,7 +15,8 @@ import EventListener from 'node:events';
|
||||
import type { IncomingMessage } from 'node:http';
|
||||
import { setTimeout as sleep } from 'node:timers/promises';
|
||||
|
||||
import type { AbortableProcess } from '../util/AbortableProcess.std.js';
|
||||
import type { UnauthUsernamesService } from '@signalapp/libsignal-client/dist/net';
|
||||
|
||||
import { strictAssert } from '../util/assert.std.js';
|
||||
import { explodePromise } from '../util/explodePromise.std.js';
|
||||
import {
|
||||
@@ -33,7 +34,10 @@ import * as Errors from '../types/errors.std.js';
|
||||
import * as Bytes from '../Bytes.std.js';
|
||||
import { createLogger } from '../logging/log.std.js';
|
||||
|
||||
import type { AbortableProcess } from '../util/AbortableProcess.std.js';
|
||||
import type {
|
||||
ChatKind,
|
||||
IChatConnection,
|
||||
IncomingWebSocketRequest,
|
||||
IWebSocketResource,
|
||||
WebSocketResourceOptions,
|
||||
@@ -97,8 +101,8 @@ export class SocketManager extends EventListener {
|
||||
jitter: JITTER,
|
||||
});
|
||||
|
||||
#authenticated?: AbortableProcess<IWebSocketResource>;
|
||||
#unauthenticated?: AbortableProcess<IWebSocketResource>;
|
||||
#authenticated?: AbortableProcess<IChatConnection<'auth'>>;
|
||||
#unauthenticated?: AbortableProcess<IChatConnection<'unauth'>>;
|
||||
#unauthenticatedExpirationTimer?: NodeJS.Timeout;
|
||||
#credentials?: WebAPICredentials;
|
||||
#lazyProxyAgent?: Promise<ProxyAgent>;
|
||||
@@ -260,7 +264,7 @@ export class SocketManager extends EventListener {
|
||||
}
|
||||
};
|
||||
|
||||
let authenticated: IWebSocketResource;
|
||||
let authenticated: IChatConnection<'auth'>;
|
||||
try {
|
||||
authenticated = await process.getResult();
|
||||
|
||||
@@ -364,7 +368,7 @@ export class SocketManager extends EventListener {
|
||||
|
||||
// Either returns currently connecting/active authenticated
|
||||
// IWebSocketResource or connects a fresh one.
|
||||
public async getAuthenticatedResource(): Promise<IWebSocketResource> {
|
||||
public async getAuthenticatedResource(): Promise<IChatConnection<'auth'>> {
|
||||
if (!this.#authenticated) {
|
||||
strictAssert(this.#credentials !== undefined, 'Missing credentials');
|
||||
await this.authenticate(this.#credentials);
|
||||
@@ -427,13 +431,18 @@ export class SocketManager extends EventListener {
|
||||
}).getResult();
|
||||
}
|
||||
|
||||
public async getUnauthenticatedLibsignalApi(): Promise<UnauthUsernamesService> {
|
||||
const resource = await this.#getUnauthenticatedResource();
|
||||
return resource.libsignalWebsocket;
|
||||
}
|
||||
|
||||
// Fetch-compatible wrapper around underlying unauthenticated/authenticated
|
||||
// websocket resources. This wrapper supports only limited number of features
|
||||
// of node-fetch despite being API compatible.
|
||||
public async fetch(url: string, init: RequestInit): Promise<Response> {
|
||||
const headers = new Headers(init.headers);
|
||||
|
||||
let resource: IWebSocketResource;
|
||||
let resource: IChatConnection<'auth'> | IChatConnection<'unauth'>;
|
||||
if (this.#isAuthenticated(headers)) {
|
||||
resource = await this.getAuthenticatedResource();
|
||||
} else {
|
||||
@@ -618,7 +627,7 @@ export class SocketManager extends EventListener {
|
||||
}
|
||||
}
|
||||
|
||||
async #getUnauthenticatedResource(): Promise<IWebSocketResource> {
|
||||
async #getUnauthenticatedResource(): Promise<IChatConnection<'unauth'>> {
|
||||
if (this.#expirationReason) {
|
||||
throw new HTTPError(`SocketManager ${this.#expirationReason} expired`, {
|
||||
code: 0,
|
||||
@@ -642,7 +651,7 @@ export class SocketManager extends EventListener {
|
||||
window.SignalContext.getResolvedMessagesLocale()
|
||||
);
|
||||
|
||||
const process: AbortableProcess<IWebSocketResource> =
|
||||
const process: AbortableProcess<IChatConnection<'unauth'>> =
|
||||
connectUnauthenticatedLibsignal({
|
||||
libsignalNet: this.libsignalNet,
|
||||
name: UNAUTHENTICATED_CHANNEL_NAME,
|
||||
@@ -652,7 +661,7 @@ export class SocketManager extends EventListener {
|
||||
|
||||
this.#unauthenticated = process;
|
||||
|
||||
let unauthenticated: IWebSocketResource;
|
||||
let unauthenticated: IChatConnection<'unauth'>;
|
||||
try {
|
||||
unauthenticated = await this.#unauthenticated.getResult();
|
||||
this.#setUnauthenticatedStatus({
|
||||
@@ -771,8 +780,8 @@ export class SocketManager extends EventListener {
|
||||
return webSocketResourceConnection;
|
||||
}
|
||||
|
||||
async #checkResource(
|
||||
process?: AbortableProcess<IWebSocketResource>
|
||||
async #checkResource<Chat extends ChatKind>(
|
||||
process?: AbortableProcess<IChatConnection<Chat>>
|
||||
): Promise<void> {
|
||||
if (!process) {
|
||||
return;
|
||||
@@ -786,7 +795,7 @@ export class SocketManager extends EventListener {
|
||||
);
|
||||
}
|
||||
|
||||
#dropAuthenticated(process: AbortableProcess<IWebSocketResource>): void {
|
||||
#dropAuthenticated(process: AbortableProcess<IChatConnection<'auth'>>): void {
|
||||
if (this.#authenticated !== process) {
|
||||
return;
|
||||
}
|
||||
@@ -807,7 +816,9 @@ export class SocketManager extends EventListener {
|
||||
}
|
||||
}
|
||||
|
||||
#dropUnauthenticated(process: AbortableProcess<IWebSocketResource>): void {
|
||||
#dropUnauthenticated(
|
||||
process: AbortableProcess<IChatConnection<'unauth'>>
|
||||
): void {
|
||||
if (this.#unauthenticated !== process) {
|
||||
return;
|
||||
}
|
||||
@@ -822,7 +833,7 @@ export class SocketManager extends EventListener {
|
||||
}
|
||||
|
||||
async #startUnauthenticatedExpirationTimer(
|
||||
expected: IWebSocketResource
|
||||
expected: IChatConnection<'unauth'>
|
||||
): Promise<void> {
|
||||
const process = this.#unauthenticated;
|
||||
strictAssert(
|
||||
|
||||
@@ -53,6 +53,7 @@ import type {
|
||||
UntaggedPniString,
|
||||
} from '../types/ServiceId.std.js';
|
||||
import {
|
||||
fromAciObject,
|
||||
ServiceIdKind,
|
||||
serviceIdSchema,
|
||||
aciSchema,
|
||||
@@ -628,27 +629,29 @@ async function _promiseAjax<Type extends ResponseType, OutputShape>(
|
||||
return result;
|
||||
}
|
||||
|
||||
async function _retryAjax<Type extends ResponseType, OutputShape>(
|
||||
url: string | null,
|
||||
options: PromiseAjaxOptionsType<Type, OutputShape>,
|
||||
providedLimit?: number,
|
||||
providedCount?: number
|
||||
): Promise<unknown> {
|
||||
const count = (providedCount || 0) + 1;
|
||||
const limit = providedLimit || 3;
|
||||
async function _retry<R>(
|
||||
f: () => Promise<R>,
|
||||
provided?: { abortSignal?: AbortSignal } & (
|
||||
| { limit: number; count: number }
|
||||
| { limit?: undefined; count?: undefined }
|
||||
)
|
||||
): Promise<R> {
|
||||
const count = (provided?.count ?? 0) + 1;
|
||||
const limit = provided?.limit ?? 3;
|
||||
const abortSignal = provided?.abortSignal;
|
||||
|
||||
try {
|
||||
return await _promiseAjax(url, options);
|
||||
return await f();
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof HTTPError &&
|
||||
e.code === -1 &&
|
||||
count < limit &&
|
||||
!options.abortSignal?.aborted
|
||||
!abortSignal?.aborted
|
||||
) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve(_retryAjax(url, options, limit, count));
|
||||
resolve(_retry(f, { abortSignal, limit, count }));
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
@@ -681,7 +684,9 @@ async function _outerAjax<Type extends ResponseType, OutputShape>(
|
||||
return _promiseAjax(url, options);
|
||||
}
|
||||
|
||||
return _retryAjax(url, options);
|
||||
return _retry(() => _promiseAjax(url, options), {
|
||||
abortSignal: options.abortSignal,
|
||||
});
|
||||
}
|
||||
|
||||
function makeHTTPError(
|
||||
@@ -944,13 +949,7 @@ export type GetAccountForUsernameOptionsType = Readonly<{
|
||||
hash: Uint8Array;
|
||||
}>;
|
||||
|
||||
const getAccountForUsernameResultZod = z.object({
|
||||
uuid: aciSchema,
|
||||
});
|
||||
|
||||
export type GetAccountForUsernameResultType = z.infer<
|
||||
typeof getAccountForUsernameResultZod
|
||||
>;
|
||||
export type GetAccountForUsernameResultType = AciString | null;
|
||||
|
||||
const getDevicesResultZod = z.object({
|
||||
devices: z.array(
|
||||
@@ -2450,19 +2449,12 @@ export async function getTransferArchive({
|
||||
export async function getAccountForUsername({
|
||||
hash,
|
||||
}: GetAccountForUsernameOptionsType): Promise<GetAccountForUsernameResultType> {
|
||||
const hashBase64 = toWebSafeBase64(Bytes.toBase64(hash));
|
||||
return _ajax({
|
||||
host: 'chatService',
|
||||
call: 'username',
|
||||
httpType: 'GET',
|
||||
urlParameters: `/${hashBase64}`,
|
||||
responseType: 'json',
|
||||
redactUrl: _createRedactor(hashBase64),
|
||||
unauthenticated: true,
|
||||
accessKey: undefined,
|
||||
groupSendToken: undefined,
|
||||
zodSchema: getAccountForUsernameResultZod,
|
||||
const aci = await _retry(async () => {
|
||||
const chat = await socketManager.getUnauthenticatedLibsignalApi();
|
||||
return chat.lookUpUsernameHash({ hash });
|
||||
});
|
||||
|
||||
return aci ? fromAciObject(aci) : null;
|
||||
}
|
||||
|
||||
export async function putProfile(
|
||||
|
||||
@@ -37,9 +37,11 @@ import type { LibSignalError, Net } from '@signalapp/libsignal-client';
|
||||
import { ErrorCode } from '@signalapp/libsignal-client';
|
||||
import { Buffer } from 'node:buffer';
|
||||
import type {
|
||||
AuthenticatedChatConnection,
|
||||
ChatServerMessageAck,
|
||||
ChatServiceListener,
|
||||
ConnectionEventsListener,
|
||||
UnauthenticatedChatConnection,
|
||||
} from '@signalapp/libsignal-client/dist/net/Chat.js';
|
||||
import type { EventHandler } from './EventTarget.std.js';
|
||||
import EventTarget from './EventTarget.std.js';
|
||||
@@ -281,6 +283,12 @@ export class CloseEvent extends Event {
|
||||
}
|
||||
}
|
||||
|
||||
export type ChatKind = 'auth' | 'unauth';
|
||||
|
||||
type LibsignalChatConnection<Kind extends ChatKind> = Kind extends 'auth'
|
||||
? AuthenticatedChatConnection
|
||||
: UnauthenticatedChatConnection;
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
export interface IWebSocketResource extends IResource {
|
||||
sendRequest(options: SendRequestOptions): Promise<Response>;
|
||||
@@ -296,8 +304,12 @@ export interface IWebSocketResource extends IResource {
|
||||
localPort(): number | undefined;
|
||||
}
|
||||
|
||||
type LibsignalWebSocketResourceHolder = {
|
||||
resource: LibsignalWebSocketResource | undefined;
|
||||
export type IChatConnection<Chat extends ChatKind> = IWebSocketResource & {
|
||||
get libsignalWebsocket(): LibsignalChatConnection<Chat>;
|
||||
};
|
||||
|
||||
type LibsignalWebSocketResourceHolder<Chat extends ChatKind> = {
|
||||
resource: LibsignalWebSocketResource<Chat> | undefined;
|
||||
};
|
||||
|
||||
const UNEXPECTED_DISCONNECT_CODE = 3001;
|
||||
@@ -312,20 +324,20 @@ export function connectUnauthenticatedLibsignal({
|
||||
name: string;
|
||||
userLanguages: ReadonlyArray<string>;
|
||||
keepalive: KeepAliveOptionsType;
|
||||
}): AbortableProcess<LibsignalWebSocketResource> {
|
||||
}): AbortableProcess<LibsignalWebSocketResource<'unauth'>> {
|
||||
const logId = `LibsignalWebSocketResource(${name})`;
|
||||
const listener: LibsignalWebSocketResourceHolder & ConnectionEventsListener =
|
||||
{
|
||||
resource: undefined,
|
||||
onConnectionInterrupted(cause: LibSignalError | null): void {
|
||||
if (!this.resource) {
|
||||
logDisconnectedListenerWarn(logId, 'onConnectionInterrupted');
|
||||
return;
|
||||
}
|
||||
this.resource.onConnectionInterrupted(cause);
|
||||
this.resource = undefined;
|
||||
},
|
||||
};
|
||||
const listener: LibsignalWebSocketResourceHolder<'unauth'> &
|
||||
ConnectionEventsListener = {
|
||||
resource: undefined,
|
||||
onConnectionInterrupted(cause: LibSignalError | null): void {
|
||||
if (!this.resource) {
|
||||
logDisconnectedListenerWarn(logId, 'onConnectionInterrupted');
|
||||
return;
|
||||
}
|
||||
this.resource.onConnectionInterrupted(cause);
|
||||
this.resource = undefined;
|
||||
},
|
||||
};
|
||||
return connectLibsignal(
|
||||
abortSignal =>
|
||||
libsignalNet.connectUnauthenticatedChat(listener, {
|
||||
@@ -356,9 +368,10 @@ export function connectAuthenticatedLibsignal({
|
||||
receiveStories: boolean;
|
||||
userLanguages: ReadonlyArray<string>;
|
||||
keepalive: KeepAliveOptionsType;
|
||||
}): AbortableProcess<LibsignalWebSocketResource> {
|
||||
}): AbortableProcess<LibsignalWebSocketResource<'auth'>> {
|
||||
const logId = `LibsignalWebSocketResource(${name})`;
|
||||
const listener: LibsignalWebSocketResourceHolder & ChatServiceListener = {
|
||||
const listener: LibsignalWebSocketResourceHolder<'auth'> &
|
||||
ChatServiceListener = {
|
||||
resource: undefined,
|
||||
onIncomingMessage(
|
||||
envelope: Uint8Array,
|
||||
@@ -418,16 +431,14 @@ function logDisconnectedListenerWarn(logId: string, method: string): void {
|
||||
log.warn(`${logId} received ${method}, but listener already disconnected`);
|
||||
}
|
||||
|
||||
function connectLibsignal(
|
||||
function connectLibsignal<Chat extends ChatKind>(
|
||||
makeConnection: (
|
||||
abortSignal: AbortSignal
|
||||
) => Promise<
|
||||
Net.UnauthenticatedChatConnection | Net.AuthenticatedChatConnection
|
||||
>,
|
||||
resourceHolder: LibsignalWebSocketResourceHolder,
|
||||
) => Promise<LibsignalChatConnection<Chat>>,
|
||||
resourceHolder: LibsignalWebSocketResourceHolder<Chat>,
|
||||
logId: string,
|
||||
keepalive: KeepAliveOptionsType
|
||||
): AbortableProcess<LibsignalWebSocketResource> {
|
||||
): AbortableProcess<LibsignalWebSocketResource<Chat>> {
|
||||
const abortController = new AbortController();
|
||||
const connectAsync = async () => {
|
||||
try {
|
||||
@@ -454,7 +465,7 @@ function connectLibsignal(
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
return new AbortableProcess<LibsignalWebSocketResource>(
|
||||
return new AbortableProcess<LibsignalWebSocketResource<Chat>>(
|
||||
`${logId}.connect`,
|
||||
{
|
||||
abort() {
|
||||
@@ -470,9 +481,9 @@ function connectLibsignal(
|
||||
);
|
||||
}
|
||||
|
||||
export class LibsignalWebSocketResource
|
||||
export class LibsignalWebSocketResource<Chat extends ChatKind>
|
||||
extends EventTarget
|
||||
implements IWebSocketResource
|
||||
implements IChatConnection<Chat>
|
||||
{
|
||||
// The reason that the connection was closed, if it was closed.
|
||||
//
|
||||
@@ -487,7 +498,7 @@ export class LibsignalWebSocketResource
|
||||
#keepalive: KeepAlive;
|
||||
|
||||
constructor(
|
||||
private readonly chatService: Net.ChatConnection,
|
||||
private readonly chatService: LibsignalChatConnection<Chat>,
|
||||
private readonly socketIpVersion: IpVersion,
|
||||
private readonly localPortNumber: number,
|
||||
private readonly logId: string,
|
||||
@@ -576,6 +587,10 @@ export class LibsignalWebSocketResource
|
||||
return response;
|
||||
}
|
||||
|
||||
get libsignalWebsocket(): LibsignalChatConnection<Chat> {
|
||||
return this.chatService;
|
||||
}
|
||||
|
||||
public async sendRequestGetDebugInfo(
|
||||
options: SendRequestOptions
|
||||
): Promise<Response> {
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { usernames, LibSignalErrorBase } from '@signalapp/libsignal-client';
|
||||
import { usernames } from '@signalapp/libsignal-client';
|
||||
|
||||
import type { UserNotFoundModalStateType } from '../state/ducks/globalModals.preload.js';
|
||||
import { createLogger } from '../logging/log.std.js';
|
||||
import type { AciString } from '../types/ServiceId.std.js';
|
||||
import * as Errors from '../types/errors.std.js';
|
||||
import { ToastType } from '../types/Toast.dom.js';
|
||||
import { strictAssert } from './assert.std.js';
|
||||
import type { UUIDFetchStateKeyType } from './uuidFetchState.std.js';
|
||||
import { getServiceIdsForE164s } from './getServiceIdsForE164s.dom.js';
|
||||
import {
|
||||
getAccountForUsername,
|
||||
cdsLookup,
|
||||
} from '../textsecure/WebAPI.preload.js';
|
||||
import * as Errors from '../types/errors.std.js';
|
||||
import { ToastType } from '../types/Toast.dom.js';
|
||||
import { HTTPError } from '../types/HTTPError.std.js';
|
||||
import { strictAssert } from './assert.std.js';
|
||||
import type { UUIDFetchStateKeyType } from './uuidFetchState.std.js';
|
||||
import { getServiceIdsForE164s } from './getServiceIdsForE164s.dom.js';
|
||||
|
||||
const log = createLogger('lookupConversationWithoutServiceId');
|
||||
|
||||
@@ -155,33 +154,17 @@ export async function checkForUsername(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const account = await getAccountForUsername({
|
||||
hash,
|
||||
});
|
||||
const accountAci = await getAccountForUsername({
|
||||
hash,
|
||||
});
|
||||
|
||||
if (!account.uuid) {
|
||||
log.error("checkForUsername: Returned account didn't include a uuid");
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
aci: account.uuid,
|
||||
username: fixedUsername,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPError) {
|
||||
if (error.code === 404) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid username
|
||||
if (error instanceof LibSignalErrorBase) {
|
||||
log.error('checkForUsername: invalid username');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
throw error;
|
||||
if (!accountAci) {
|
||||
log.error("checkForUsername: Returned account didn't include a uuid");
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
aci: accountAci,
|
||||
username: fixedUsername,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user