UNPKG

@interopio/gateway-server

Version:

## `@glue42/gateway-ent` compatibility

380 lines (325 loc) 9.9 kB
import { HeaderValue, HeaderValues, HttpRequest, HttpResponse, MutableHttpHeaders, ReadonlyHttpHeaders, ServerHttpRequest, ServerHttpResponse, WebExchange } from './types.js'; import http from 'node:http'; import http2 from 'node:http2'; function requestToProtocol(request: ServerHttpRequest, defaultProtocol: string): string { let proto = request.headers.get('x-forwarded-proto'); if (Array.isArray(proto)) { proto = proto[0]; } if (proto !== undefined) { return (proto as string).split(',', 1)[0].trim(); } return defaultProtocol; } function requestToHost(request: ServerHttpRequest, defaultHost?: string): string | undefined { let host = request.headers.get('x-forwarded-for'); if (host === undefined) { host = request.headers.get('x-forwarded-host'); if (Array.isArray(host)) { host = host[0]; } if (host) { const port = request.headers.one('x-forwarded-port'); if (port) { host = `${host}:${port}`; } } if (host === undefined) { host = request.headers.one('host'); } } if (Array.isArray(host)) { host = host[0]; } if (host) { return (host as string).split(',', 1)[0].trim(); } return defaultHost; } export class HttpServerRequest implements ServerHttpRequest { private _body: Promise<Blob> | undefined; private _url: URL | undefined; private readonly _headers: ReadonlyHttpHeaders; constructor(readonly _req: http.IncomingMessage) { this._headers = new IncomingMessageHeaders(_req); } get http2(): boolean { return this._req.httpVersionMajor >= 2; } get headers() { return this._headers; } get path() { return this.URL?.pathname; } get URL() { this._url ??= new URL(this._req.url!, `${this.protocol}://${this.host}`); return this._url; } get query() { return this.URL?.search; } get method() { return this._req.method; } get host() { let dh: string | undefined = undefined; if (this._req.httpVersionMajor >= 2) { dh = (this._req?.headers as http2.IncomingHttpHeaders)[':authority']; } if (dh === undefined) { dh = this._req?.socket.remoteAddress; } return requestToHost(this, dh); } get protocol() { let dp: string | undefined = undefined; if (this._req.httpVersionMajor > 2) { dp = (this._req.headers as http2.IncomingHttpHeaders)[':scheme']; } if (dp === undefined) { dp = this._req?.socket['encrypted'] ? 'https' : 'http' } return requestToProtocol(this, dp); } get socket() { return this._req.socket; } get body() { this._body ??= new Promise((resolve, reject) => { const chunks: Uint8Array[] = []; this._req .on('error', (err: Error) => reject(err)) .on('data', (chunk) => chunks.push(chunk)) .on('end', () => { resolve(new Blob(chunks)); }); }); return this._body; } get text() { return this.body.then(async (blob) => await blob.text()); } get json() { return this.body.then(async (blob) => { const json = JSON.parse(await blob.text()); return json; // try { // } catch (e) { // reject(e instanceof Error ? e : new Error(`parse failed :${e}`)); // } }); } } class IncomingMessageHeaders implements ReadonlyHttpHeaders { constructor(private readonly _msg: http.IncomingMessage) { } has(name: string): boolean { return this._msg.headers[name] !== undefined; } get(name: string): string | (readonly string[]) | undefined { return this._msg.headers[name]; } list(name: string): string[] { return toList(this._msg.headers[name]); } one(name: string): string | undefined { const value = this._msg.headers[name]; if (Array.isArray(value)) { return value[0]; } return value; } keys() { return Object.keys(this._msg.headers).values(); } } class OutgoingMessageHeaders implements MutableHttpHeaders { constructor(private readonly _msg: http.OutgoingMessage) { } has(name: string): boolean { return this._msg.hasHeader(name); } keys() { return this._msg.getHeaderNames().values(); } get(name: string): HeaderValues { return this._msg.getHeader(name); } one(name: string): HeaderValue { const value = this._msg.getHeader(name); if (Array.isArray(value)) { return value[0]; } return value; } set(name: string, value: HeaderValues): this { if (!this._msg.headersSent) { if (Array.isArray(value)) { value = value.map(v => typeof v === 'number' ? String(v) : v); } else if (typeof value === 'number') { value = String(value); } if (value) { this._msg.setHeader(name, value); } else { this._msg.removeHeader(name); } } return this; } add(name: string, value: string | (readonly string[])): this { if (!this._msg.headersSent) { this._msg.appendHeader(name, value); } return this; } list(name: string): string[] { const values = this.get(name); return toList(values); } } export class HttpServerResponse implements ServerHttpResponse { private readonly _headers: MutableHttpHeaders; constructor(readonly _res: http.ServerResponse) { this._headers = new OutgoingMessageHeaders(_res); } get statusCode(): number { return this._res.statusCode; } set statusCode(value: number) { if (this._res.headersSent) { return; } this._res.statusCode = value; } get headers() { return this._headers; } } export class DefaultWebExchange<Request extends ServerHttpRequest, Response extends ServerHttpResponse> extends WebExchange<Request, Response> { constructor(readonly request: Request, readonly response: Response) { super(); } } function toList(values: number | string | (readonly string[]) | undefined): string[] { if (typeof values === 'string') { values = [values]; } if (typeof values === 'number') { values = [String(values)]; } const list: string[] = []; if (values) { for (const value of values) { if (value) { list.push(...parseHeader(value)); } } } return list; } function parseHeader(value: string): string[] { const list: string[] = []; { let start = 0; let end = 0; for (let i = 0; i < value.length; i++) { switch (value.charCodeAt(i)) { case 0x20: // space if (start === end) { start = end = i + 1; } break; case 0x2c: // comma list.push(value.slice(start, end)); start = end = i + 1; break; default: end = end + 1; break; } } list.push(value.slice(start, end)); } return list; } export class MapHttpHeaders extends Map<string, (readonly string[])> implements MutableHttpHeaders { get(name: string) { return super.get(name.toLowerCase()); } one(name: string): string | undefined { return this.get(name)?.[0]; } list(name: string) { const values = super.get(name.toLowerCase()); return toList(values); } set(name: string, value: number | string | (readonly string[]) | undefined): this { if (typeof value === 'number') { value = String(value); } if (typeof value === 'string') { value = [value]; } if (value) { return super.set(name.toLowerCase(), value); } else { super.delete(name.toLowerCase()); return this; } } add(name: string, value: string | (readonly string[])) { const prev = super.get(name.toLowerCase()); if (typeof value === 'string') { value = [value]; } if (prev) { value = prev.concat(value); } this.set(name, value); return this; } } export class MockHttpRequest implements HttpRequest<MutableHttpHeaders> { private _body?: Blob; readonly headers = new MapHttpHeaders(); constructor(private readonly url: URL, method?: string) { this.method = method ?? 'GET'; this.headers.set('Host', url.hostname); this.path = url?.pathname ?? '/'; } method: string; path: string; get host() { return requestToHost(this, this.url.host); } get protocol() { return requestToProtocol(this, this.url.protocol.slice(0, -1)); } get body(): Promise<Blob> { const body = this._body; return body ? Promise.resolve(body): Promise.reject(new Error(`no body set`)); } set body(value: Blob) { this._body = value; } get URL() { return new URL(this.path, `${this.protocol}://${this.host}`); } } export class MockHttpResponse implements HttpResponse<MutableHttpHeaders> { statusCode!: number; headers = new MapHttpHeaders(); }