mirror of
https://github.com/lux-tel/session-desktop.git
synced 2026-10-30 20:33:24 +00:00
262 lines
7.5 KiB
TypeScript
262 lines
7.5 KiB
TypeScript
import AbortController from 'abort-controller';
|
|
import type { ZodError } from 'zod';
|
|
import z, { zodSafeParse } from '../../util/zod';
|
|
import { BlindingActions } from '../../webworker/workers/browser/libsession_worker_interface';
|
|
import { isOnionV4JSONSnodeResponse, OnionSending } from '../onions/onionSend';
|
|
import { fromUInt8ArrayToBase64 } from '../utils/String';
|
|
import { NetworkTime } from '../../util/NetworkTime';
|
|
import {
|
|
batchGlobalIsSuccess,
|
|
parseBatchGlobalStatusCode,
|
|
} from './open_group_api/sogsv3/sogsV3BatchPoll';
|
|
import { UserUtils } from '../utils';
|
|
import { timeoutWithAbort } from '../utils/Promise';
|
|
import { DURATION } from '../constants';
|
|
import { getFeatureFlag } from '../../state/ducks/types/releasedFeaturesReduxTypes';
|
|
import { FetchDestination } from '../utils/InsecureNodeFetch';
|
|
|
|
export type SessionServerConfigType = {
|
|
name: string;
|
|
url: `${'http' | 'https'}://${string}`;
|
|
edPkHex: string;
|
|
xPkHex: string;
|
|
requestTimeoutMs: number;
|
|
abortControllerTimeoutMs: number;
|
|
};
|
|
|
|
export type SessionBackendServerApiOptions = Omit<
|
|
SessionServerConfigType,
|
|
'requestTimeoutMs' | 'abortControllerTimeoutMs'
|
|
> & {
|
|
requestTimeoutMs?: number;
|
|
abortControllerTimeoutMs?: number;
|
|
};
|
|
|
|
type HTTPMethod = 'GET' | 'POST';
|
|
|
|
type SessionBackendServerMakeRequestParams = {
|
|
path: `/${string}`;
|
|
method: HTTPMethod;
|
|
bodyGetter?: () => Promise<string | null>;
|
|
blindSignRequest?: boolean;
|
|
};
|
|
|
|
type BlindSignedHeaders = {
|
|
'X-FS-Pubkey': string;
|
|
'X-FS-Timestamp': string;
|
|
'X-FS-Signature': string;
|
|
};
|
|
|
|
export const SessionBackendBaseResponseSchema = z.object({
|
|
status_code: z.number(),
|
|
error: z.string().optional(),
|
|
t: z.number(),
|
|
});
|
|
|
|
export type SessionBackendBaseResponseType = z.infer<typeof SessionBackendBaseResponseSchema>;
|
|
|
|
export type SessionBackendServerApiResponse = SessionBackendBaseResponseType &
|
|
Record<string, unknown>;
|
|
|
|
export default class SessionBackendServerApi {
|
|
readonly server: SessionServerConfigType;
|
|
|
|
constructor(
|
|
server: SessionBackendServerApiOptions,
|
|
requestTimeoutMsOverride?: number,
|
|
abortControllerTimeoutMs?: number
|
|
) {
|
|
this.server = {
|
|
...server,
|
|
requestTimeoutMs:
|
|
server.requestTimeoutMs || requestTimeoutMsOverride || 10 * DURATION.SECONDS,
|
|
abortControllerTimeoutMs:
|
|
server.abortControllerTimeoutMs || abortControllerTimeoutMs || 30 * DURATION.SECONDS,
|
|
};
|
|
}
|
|
|
|
private static getGenericErrorResponse(error?: string) {
|
|
return {
|
|
status_code: 500,
|
|
error,
|
|
t: NetworkTime.nowSeconds(),
|
|
};
|
|
}
|
|
|
|
public logInfo(msg: string, path?: string) {
|
|
window?.log?.info(`[${this.server.name}] ${path?.length ? `${path}: ` : ''}${msg}`);
|
|
}
|
|
public logError(msg: string, path?: string) {
|
|
window?.log?.error(`[${this.server.name}] ${path?.length ? `${path}: ` : ''}${msg}`);
|
|
}
|
|
public logZodError(zodError: ZodError, path?: string) {
|
|
window?.log?.error(`[${this.server.name}] ${path?.length ? `${path}: ` : ''}`, zodError);
|
|
}
|
|
|
|
// NOTE: returns null if the ed25519 key pair is not found
|
|
private async getBlindSignedHeaders({
|
|
method,
|
|
path,
|
|
body,
|
|
}: {
|
|
method: HTTPMethod;
|
|
path: string;
|
|
body: string | null;
|
|
}): Promise<BlindSignedHeaders | null> {
|
|
const ed25519SecretKey = (await UserUtils.getUserED25519KeyPairBytes()).privKeyBytes;
|
|
if (!ed25519SecretKey) {
|
|
this.logError(`${path} error: ed25519Secret key was not found when creating headers!`);
|
|
return null;
|
|
}
|
|
|
|
const blindedPkHex = await BlindingActions.blindVersionPubkey({
|
|
ed25519SecretKey,
|
|
});
|
|
|
|
const sigTimestampSeconds = NetworkTime.nowSeconds();
|
|
const signedData = await BlindingActions.blindVersionSignRequest({
|
|
ed25519SecretKey,
|
|
sigTimestampSeconds,
|
|
sigMethod: method,
|
|
sigPath: path,
|
|
sigBody: body ? new TextEncoder().encode(body) : null,
|
|
});
|
|
|
|
return {
|
|
'X-FS-Pubkey': blindedPkHex,
|
|
'X-FS-Timestamp': sigTimestampSeconds.toString(),
|
|
'X-FS-Signature': fromUInt8ArrayToBase64(signedData),
|
|
};
|
|
}
|
|
|
|
private handleError(message: string, path: string) {
|
|
this.logError(message, path);
|
|
return SessionBackendServerApi.getGenericErrorResponse(message);
|
|
}
|
|
|
|
private async _makeRequest({
|
|
path,
|
|
method,
|
|
bodyGetter,
|
|
blindSignRequest,
|
|
}: SessionBackendServerMakeRequestParams): Promise<SessionBackendServerApiResponse> {
|
|
const body = typeof bodyGetter === 'function' ? await bodyGetter() : null;
|
|
|
|
const headers = blindSignRequest
|
|
? await this.getBlindSignedHeaders({
|
|
method,
|
|
path,
|
|
body,
|
|
})
|
|
: {};
|
|
|
|
if (headers === null) {
|
|
return this.handleError('failed to blind sign request parameters', path);
|
|
}
|
|
|
|
const url = new URL(path, this.server.url);
|
|
|
|
const controller = new AbortController();
|
|
const result = await timeoutWithAbort(
|
|
OnionSending.sendViaOnionV4ToNonSnodeWithRetries(
|
|
this.server.xPkHex,
|
|
url,
|
|
{
|
|
method,
|
|
headers,
|
|
body,
|
|
useV4: true,
|
|
},
|
|
false,
|
|
controller.signal,
|
|
this.server.requestTimeoutMs,
|
|
FetchDestination.SESSION_SERVER,
|
|
'SessionBackendServerApi._makeRequest'
|
|
),
|
|
this.server.abortControllerTimeoutMs,
|
|
controller
|
|
);
|
|
|
|
if (!result || !isOnionV4JSONSnodeResponse(result) || typeof result.body !== 'object') {
|
|
return this.handleError(`returned a non json response: ${JSON.stringify(result)}`, path);
|
|
}
|
|
|
|
if (!batchGlobalIsSuccess(result)) {
|
|
return this.handleError(`failed with status ${parseBatchGlobalStatusCode(result)}`, path);
|
|
}
|
|
|
|
return {
|
|
...result.body,
|
|
status_code: result.status_code,
|
|
t:
|
|
't' in result.body && typeof result.body.t === 'number'
|
|
? result.body.t
|
|
: NetworkTime.nowSeconds(),
|
|
};
|
|
}
|
|
|
|
private parseSchema<T>({
|
|
path,
|
|
response,
|
|
schema,
|
|
}: {
|
|
path: string;
|
|
response: SessionBackendServerApiResponse;
|
|
schema: z.ZodType<T>;
|
|
}): T | null {
|
|
const result = zodSafeParse(schema, response);
|
|
if (result.success) {
|
|
return result.data;
|
|
}
|
|
|
|
if (result.error) {
|
|
this.logZodError(result.error, path);
|
|
} else {
|
|
this.logError('Failed to parse response', path);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public async makeRequest(
|
|
params: SessionBackendServerMakeRequestParams
|
|
): Promise<SessionBackendServerApiResponse> {
|
|
if (getFeatureFlag('debugServerRequests')) {
|
|
this.logInfo(`\nrequest: ${JSON.stringify({ url: this.server.url, params })}`, params.path);
|
|
}
|
|
|
|
const response = await this._makeRequest(params);
|
|
|
|
if (getFeatureFlag('debugServerRequests')) {
|
|
this.logInfo(`\nresponse: ${JSON.stringify(response)}`, params.path);
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
public async makeRequestWithSchema<T extends SessionBackendBaseResponseType>({
|
|
withZodSchema,
|
|
...requestParams
|
|
}: SessionBackendServerMakeRequestParams & {
|
|
withZodSchema: z.ZodType<T>;
|
|
}): Promise<T | null> {
|
|
const response = await this.makeRequest(requestParams);
|
|
if (getFeatureFlag('debugServerRequests')) {
|
|
this.logInfo(
|
|
`\nfrom ${requestParams.method} on "${requestParams.path}: response: ${JSON.stringify(response)}`,
|
|
requestParams.path
|
|
);
|
|
}
|
|
|
|
if (response.status_code !== 200) {
|
|
return this.parseSchema({
|
|
path: requestParams.path,
|
|
response,
|
|
schema: SessionBackendBaseResponseSchema,
|
|
}) as T | null;
|
|
}
|
|
|
|
return this.parseSchema({ path: requestParams.path, response, schema: withZodSchema });
|
|
}
|
|
}
|