mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
118 lines
3.7 KiB
TypeScript
118 lines
3.7 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import type {Buffer} from 'node:buffer';
|
|
import type http from 'node:http';
|
|
import {
|
|
DesktopOriginTrust,
|
|
type DesktopOutboundHTTP,
|
|
getDesktopOutboundHTTP,
|
|
readBoundedMessage,
|
|
readMessageContentLength,
|
|
} from '@electron/main/DesktopOutboundHTTP';
|
|
import {HttpStatus} from '@fluxer/constants/src/HttpConstants';
|
|
|
|
const INSTANCE_RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
const INSTANCE_RESPONSE_MAX_CHUNKS = 1024;
|
|
const INSTANCE_FETCH_TIMEOUT_MS = 15_000;
|
|
|
|
const INSTANCE_RESPONSE_DESCRIPTION = 'Instance response';
|
|
const RESET_CONTENT_STATUS = 205;
|
|
|
|
interface SelectedInstanceFetchRequest {
|
|
readonly body?: Uint8Array | null;
|
|
readonly expectedOrigin: string;
|
|
readonly headers?: Readonly<Record<string, string>> | null;
|
|
readonly method?: string | null;
|
|
readonly signal?: AbortSignal | null;
|
|
readonly timeoutMs?: number | null;
|
|
readonly url: string;
|
|
}
|
|
|
|
interface DesktopInstanceResponse {
|
|
readonly body: Buffer | null;
|
|
readonly headers: http.IncomingHttpHeaders;
|
|
readonly ok: boolean;
|
|
readonly status: number;
|
|
readonly statusText: string;
|
|
}
|
|
|
|
class InstanceRedirectRefusedError extends Error {
|
|
public readonly status: number;
|
|
|
|
public constructor(url: string, status: number) {
|
|
super(`Instance request to ${url} was answered with an HTTP ${status} redirect, which is never followed`);
|
|
this.name = 'InstanceRedirectRefusedError';
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
class InvalidInstanceTimeoutError extends RangeError {
|
|
public constructor(timeoutMs: number) {
|
|
super(`Instance request timeout ${timeoutMs} must be a positive safe integer`);
|
|
this.name = 'InvalidInstanceTimeoutError';
|
|
}
|
|
}
|
|
|
|
function hasNoBody(method: string, status: number): boolean {
|
|
if (method === 'HEAD') {
|
|
return true;
|
|
}
|
|
return status === HttpStatus.NO_CONTENT || status === RESET_CONTENT_STATUS || status === HttpStatus.NOT_MODIFIED;
|
|
}
|
|
|
|
export class DesktopSelectedInstanceClient {
|
|
private readonly outboundHTTP: DesktopOutboundHTTP;
|
|
|
|
public constructor(outboundHTTP: DesktopOutboundHTTP) {
|
|
this.outboundHTTP = outboundHTTP;
|
|
}
|
|
|
|
public async fetch(request: SelectedInstanceFetchRequest): Promise<DesktopInstanceResponse> {
|
|
const timeoutMs = request.timeoutMs ?? INSTANCE_FETCH_TIMEOUT_MS;
|
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
|
|
throw new InvalidInstanceTimeoutError(timeoutMs);
|
|
}
|
|
const method = (request.method ?? 'GET').toUpperCase();
|
|
const message = await this.outboundHTTP.request({
|
|
body: request.body ?? null,
|
|
expectedOrigin: request.expectedOrigin,
|
|
headers: request.headers ?? null,
|
|
method,
|
|
originTrust: DesktopOriginTrust.BOUND,
|
|
signal: request.signal ?? null,
|
|
timeoutMs,
|
|
url: request.url,
|
|
});
|
|
if (message.status >= 300 && message.status < 400) {
|
|
message.message.destroy();
|
|
throw new InstanceRedirectRefusedError(request.url, message.status);
|
|
}
|
|
let body: Buffer | null = null;
|
|
if (hasNoBody(method, message.status)) {
|
|
message.message.resume();
|
|
} else {
|
|
body = await readBoundedMessage({
|
|
declaredBytes: readMessageContentLength(message, INSTANCE_RESPONSE_DESCRIPTION),
|
|
description: INSTANCE_RESPONSE_DESCRIPTION,
|
|
maxBytes: INSTANCE_RESPONSE_MAX_BYTES,
|
|
maxChunks: INSTANCE_RESPONSE_MAX_CHUNKS,
|
|
message: message.message,
|
|
});
|
|
}
|
|
return {
|
|
body,
|
|
headers: message.headers,
|
|
ok: message.status >= 200 && message.status < 300,
|
|
status: message.status,
|
|
statusText: message.statusText,
|
|
};
|
|
}
|
|
}
|
|
|
|
let sharedSelectedInstanceClient: DesktopSelectedInstanceClient | null = null;
|
|
|
|
export function getDesktopSelectedInstanceClient(): DesktopSelectedInstanceClient {
|
|
sharedSelectedInstanceClient ??= new DesktopSelectedInstanceClient(getDesktopOutboundHTTP());
|
|
return sharedSelectedInstanceClient;
|
|
}
|