@interopio/gateway-server
Version:
## `@glue42/gateway-ent` compatibility
380 lines (325 loc) • 9.9 kB
text/typescript
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();
}