Files
multi-simadmin/apps/api/src/infrastructure/transport/safe-upstream-gateway.ts
T

87 lines
3.2 KiB
TypeScript

import type {
UpstreamRequest,
UpstreamResponse,
} from '../../application/connections/upstream-session-client.js';
import type { TransportResponse } from './safe-instance-transport.js';
import { UpstreamError } from './upstream-error.js';
import { OperationNotDispatchedError } from '../../application/operations/secure-operation-execution.js';
export interface SafeUpstreamTransport {
get(url: string, headers?: Readonly<Record<string, string>>): Promise<TransportResponse>;
post(
url: string,
headers: Readonly<Record<string, string>>,
body: string,
): Promise<TransportResponse>;
}
export class SafeUpstreamGateway {
constructor(private readonly options: { readonly transport: SafeUpstreamTransport }) {}
async postNetworkRegisterAuto(origin: string): Promise<UpstreamResponse> {
let parsed: URL;
try {
parsed = new URL(origin);
} catch {
throw this.notDispatched();
}
if (
(parsed.protocol !== 'http:' && parsed.protocol !== 'https:') ||
parsed.username ||
parsed.password ||
parsed.pathname !== '/' ||
parsed.search ||
parsed.hash
)
throw this.notDispatched();
const url = `${parsed.origin}/api/network/register-auto`;
try {
return await this.options.transport.post(url, {}, '');
} catch (error) {
if (
error instanceof UpstreamError &&
(error.code === 'UNSAFE_ORIGIN' || error.code === 'UNSAFE_RESOLUTION')
)
throw new OperationNotDispatchedError(error.code);
throw error;
}
}
async request(request: UpstreamRequest): Promise<UpstreamResponse> {
const url = new URL(request.url);
if (request.method === 'GET') {
if (
request.secret !== undefined ||
request.body !== undefined ||
(url.pathname !== '/api/stats' && url.pathname !== '/api/sim') ||
url.search ||
url.hash ||
url.username ||
url.password ||
(request.headers.cookie !== undefined &&
(typeof request.headers.cookie !== 'string' ||
!/^simadmin_session=[^;\s,]+$/.test(request.headers.cookie)))
)
throw new UpstreamError('UPSTREAM_REQUEST_INVALID');
return this.options.transport.get(request.url, request.headers);
}
if (url.protocol !== 'https:') throw new UpstreamError('UPSTREAM_INSECURE_AUTH');
if (request.method !== 'POST') throw new UpstreamError('UPSTREAM_REQUEST_INVALID');
if (request.url.endsWith('/api/auth/login')) {
if (typeof request.secret !== 'string' || request.body !== '[REDACTED]')
throw new UpstreamError('UPSTREAM_REQUEST_INVALID');
return this.options.transport.post(
request.url,
request.headers,
JSON.stringify({ password: request.secret }),
);
}
if (request.url.endsWith('/api/auth/logout')) {
if (request.secret !== undefined || request.body !== undefined)
throw new UpstreamError('UPSTREAM_REQUEST_INVALID');
return this.options.transport.post(request.url, request.headers, '');
}
throw new UpstreamError('UPSTREAM_REQUEST_INVALID');
}
private notDispatched(): OperationNotDispatchedError {
return new OperationNotDispatchedError('UPSTREAM_REQUEST_INVALID');
}
}