@interopio/gateway-server
Version:
4 lines • 239 kB
Source Map (JSON)
{
"version": 3,
"sources": ["../src/index.ts", "../src/server.ts", "../src/logger.ts", "../src/gateway/ws/core.ts", "../src/common/compose.ts", "../src/http/exchange.ts", "../src/http/status.ts", "../src/server/exchange.ts", "../src/server/address.ts", "../src/server/monitoring.ts", "../src/server/server-header.ts", "../src/server/ws-client-verify.ts", "../src/server/util/matchers.ts", "../src/app/route.ts", "../src/server/cors.ts", "../src/app/cors.ts", "../src/server/security/types.ts", "../src/server/security/http-headers.ts", "../src/server/security/entry-point-failure-handler.ts", "../src/server/security/http-basic-entry-point.ts", "../src/server/security/http-basic-converter.ts", "../src/server/security/security-context.ts", "../src/server/security/authentication-filter.ts", "../src/server/security/http-status-entry-point.ts", "../src/server/security/delegating-entry-point.ts", "../src/server/security/delegating-success-handler.ts", "../src/server/security/http-basic.ts", "../src/server/security/oauth2/token-error.ts", "../src/server/security/oauth2/token-converter.ts", "../src/server/security/oauth2/token-entry-point.ts", "../src/server/security/oauth2/jwt-auth-manager.ts", "../src/server/security/oauth2-resource-server.ts", "../src/server/security/config.ts", "../src/server/security/error-filter.ts", "../src/server/security/delegating-authorization-manager.ts", "../src/server/security/authorization-filter.ts", "../src/server/security/exchange-filter.ts", "../src/app/auth.ts", "../src/server/handler.ts", "../src/server/ws.ts", "../src/server/socket.ts"],
"sourcesContent": ["import * as GatewayServer from './server.js';\nexport {GatewayServer};\nexport default GatewayServer.Factory;\n", "import http from 'node:http';\nimport https from 'node:https';\nimport {SecureContextOptions} from 'node:tls';\nimport type {AddressInfo, Socket} from 'node:net';\nimport {readFileSync} from 'node:fs';\nimport {AsyncLocalStorage} from 'node:async_hooks';\nimport {IOGateway} from '@interopio/gateway';\nimport wsGateway from './gateway/ws/core.js';\nimport {compose} from './common/compose.js';\nimport {\n ExtendedHttpIncomingMessage,\n ExtendedHttpServerResponse,\n HttpServerRequest,\n HttpServerResponse\n} from './server/exchange.js';\nimport getLogger from './logger.js';\nimport {localIp, portRange} from './server/address.js';\nimport * as monitoring from './server/monitoring.js';\nimport {GatewayServer} from '../gateway-server';\nimport serverHeader from './server/server-header.js';\nimport {configure, type RouteConfig} from './app/route.js';\nimport {httpSecurity} from './app/auth.js';\nimport {ServerConfigurer, ServerHttpResponse, ServerWebExchange} from '../types/web/server';\nimport {HttpHeadResponseDecorator, WebHttpHandlerBuilder} from \"./server/handler.ts\";\nimport {initRoute} from './server/ws.ts';\nimport {SocketRoute, webSockets} from \"./server/socket.ts\";\nimport {HttpStatus} from \"./http/status.ts\";\n\nconst logger = getLogger('app');\n\nfunction secureContextOptions(ssl: GatewayServer.SslConfig): SecureContextOptions {\n const options: SecureContextOptions = {};\n if (ssl.key) options.key = readFileSync(ssl.key);\n if (ssl.cert) options.cert = readFileSync(ssl.cert);\n if (ssl.ca) options.ca = readFileSync(ssl.ca);\n return options;\n}\n\ntype RequestListener<\n Request extends typeof ExtendedHttpIncomingMessage = typeof ExtendedHttpIncomingMessage,\n Response extends typeof ExtendedHttpServerResponse<InstanceType<Request>> = typeof ExtendedHttpServerResponse,\n> = (req: InstanceType<Request>, resOrUpgradeHead: (InstanceType<Response> & { req: InstanceType<Request> }) | Buffer<ArrayBufferLike>) => void;\n\ntype ServerOptions = http.ServerOptions<typeof ExtendedHttpIncomingMessage, typeof ExtendedHttpServerResponse>;\n\nasync function createListener(builder: WebHttpHandlerBuilder,\n onSocketError: (err: Error) => void): Promise<RequestListener> {\n\n const httpHandler = builder.build();\n return async (req: ExtendedHttpIncomingMessage, resOrUpgradeHead: ExtendedHttpServerResponse | Buffer<ArrayBufferLike>) => {\n req.socket.addListener('error', onSocketError);\n let res: ExtendedHttpServerResponse;\n if (resOrUpgradeHead instanceof ExtendedHttpServerResponse) {\n res = resOrUpgradeHead;\n }\n else {\n req.upgradeHead = resOrUpgradeHead;\n res = new ExtendedHttpServerResponse(req);\n res.assignSocket(req.socket);\n }\n const request = new HttpServerRequest(req);\n const response = new HttpServerResponse(res);\n const decoratedResponse: ServerHttpResponse = request.method === 'HEAD' ? new HttpHeadResponseDecorator(response) : response;\n\n await httpHandler(request, decoratedResponse)\n\n };\n}\n\nfunction promisify<T>(fn: (callback?: (err?: Error) => void) => T): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const r = fn((err?: Error) => {\n if (err) {\n reject(err);\n } else {\n resolve(r);\n }\n });\n });\n}\n\nfunction memoryMonitor(config?: GatewayServer.ServerConfig['memory']) {\n if (config) {\n return monitoring.start({\n memoryLimit: config.memory_limit,\n dumpLocation: config.dump_location,\n dumpPrefix: config.dump_prefix,\n reportInterval: config.report_interval,\n maxBackups: config.max_backups\n });\n }\n}\n\nasync function initBuilder(context: RouteConfig) {\n const storage = context.storage;\n const security = await httpSecurity(context);\n // websocket upgrade handler\n const sockets = webSockets(context);\n const handler = compose<ServerWebExchange>(\n serverHeader(context.serverHeader),\n ...security,\n ...sockets,\n ...context.middleware,\n // health check\n async ({request, response}, next) => {\n if (request.method === 'GET' && request.path === '/health') {\n response.setStatusCode(HttpStatus.OK);\n const buffer = Buffer.from('UP', 'utf-8');\n response.headers.set('Content-Type', 'text/plain; charset=utf-8');\n await response.body(buffer);\n } else {\n await next();\n }\n },\n // home page\n async ({request, response}, next) => {\n if (request.method === 'GET' && request.path === '/') {\n response.setStatusCode(HttpStatus.OK);\n const buffer = Buffer.from('io.Gateway Server', 'utf-8');\n response.headers.set('Content-Type', 'text/plain; charset=utf-8');\n await response.body(buffer);\n } else {\n await next();\n }\n },\n // not found\n async ({response}, _next) => {\n response.setStatusCode(HttpStatus.NOT_FOUND);\n await response.end();\n }\n );\n\n return new WebHttpHandlerBuilder(handler).storage(storage);\n}\n\nexport const Factory = async (options: GatewayServer.ServerConfig): Promise<GatewayServer.Server> => {\n const ssl = options.ssl;\n const createServer = ssl\n ? (options: ServerOptions, handler: RequestListener) => https.createServer({...options, ...secureContextOptions(ssl)}, handler)\n : (options: ServerOptions, handler: RequestListener) => http.createServer(options, handler);\n const monitor = memoryMonitor(options.memory);\n const context: RouteConfig = {\n middleware: [],\n corsConfig: options.cors,\n cors: [],\n authConfig: options.auth,\n authorize: [],\n storage: new AsyncLocalStorage<{ exchange: ServerWebExchange }>(),\n sockets: new Map<string, SocketRoute>()\n };\n const gw = IOGateway.Factory({...options.gateway});\n if (options.gateway) {\n const config = options.gateway;\n\n await configure(async (configurer: ServerConfigurer) => {\n configurer.socket({path: config.route, factory: wsGateway.bind(gw), options: config})\n }, options, context);\n\n }\n if (options.app) {\n await configure(options.app, options, context)\n }\n\n const ports = portRange(options.port ?? 0);\n const host = options.host;\n const onSocketError = (err: Error) => logger.error(`socket error: ${err}`, err);\n const builder = await initBuilder(context);\n\n const listener = await createListener(builder, onSocketError);\n\n const serverP = new Promise<http.Server<typeof ExtendedHttpIncomingMessage, typeof ExtendedHttpServerResponse>>((resolve, reject) => {\n\n const server = createServer({\n IncomingMessage: ExtendedHttpIncomingMessage,\n ServerResponse: ExtendedHttpServerResponse,\n }, listener);\n\n server.on('error', (e: Error) => {\n if (e['code'] === 'EADDRINUSE') {\n logger.debug(`port ${e['port']} already in use on address ${e['address']}`);\n const {value: port} = ports.next();\n if (port) {\n logger.info(`retry starting server on port ${port} and host ${host ?? '<unspecified>'}`);\n server.close();\n server.listen(port, host);\n } else {\n logger.warn(`all configured port(s) ${options.port} are in use. closing...`);\n server.close();\n reject(e);\n }\n } else {\n logger.error(`server error: ${e.message}`, e);\n reject(e);\n }\n });\n server\n .on('listening', async () => {\n const info = server.address() as AddressInfo;\n\n for (const [path, route] of context.sockets) {\n const endpoint = `${ssl ? 'wss' : 'ws'}://${localIp}:${info.port}${path}`;\n await initRoute(path, route, endpoint, context.storage, onSocketError);\n }\n logger.info(`http server listening on ${info.address}:${info.port}`);\n resolve(server);\n });\n server\n .on('upgrade', (req: ExtendedHttpIncomingMessage, _socket: Socket, head: Buffer<ArrayBufferLike>) => {\n try {\n listener(req, head);\n } catch (err) {\n logger.error(`upgrade error: ${err}`, err);\n }\n })\n .on('close', async () => {\n logger.info(`http server closed.`);\n });\n try {\n const {value: port} = ports.next();\n server.listen(port, host);\n } catch (e) {\n logger.error(`error starting web socket server`, e);\n reject(e instanceof Error ? e : new Error(`listen failed: ${e}`));\n }\n });\n const server = await serverP;\n return new class implements GatewayServer.Server {\n readonly gateway = gw;\n\n async close(): Promise<void> {\n for (const [path, route] of context.sockets) {\n try {\n if (route.close !== undefined) {\n await route.close();\n }\n } catch (e) {\n logger.warn(`error closing route ${path}`, e);\n }\n }\n await promisify(cb => {\n server.closeAllConnections();\n server.close(cb);\n });\n if (monitor) {\n await monitoring.stop(monitor);\n }\n if (gw) {\n await gw.stop();\n }\n }\n }\n}\n", "import {IOGateway} from '@interopio/gateway';\nexport import Logger = IOGateway.Logging.Logger;\n\nexport default function getLogger(name: string): Logger {\n return IOGateway.Logging.getLogger(`gateway.server.${name}`);\n}\n\n// This function is used to ensure that RegExp objects are displayed correctly when logging.\nexport function regexAwareReplacer<T>(_key: string, value: T): string | T {\n return value instanceof RegExp ? value.toString() : value;\n}\n", "import type {WebSocket} from 'ws';\nimport getLogger from '../../logger.js';\nimport type {Authentication} from '../../server/security/types.ts';\nimport {IOGateway} from '@interopio/gateway';\n\nimport GatewayEncoders = IOGateway.Encoding;\nimport type {ServerWebSocketHandler} from '../../../types/web/server';\nimport type {AddressInfo} from 'node:net';\nimport {AsyncLocalStorage} from 'node:async_hooks';\n\nconst log = getLogger('ws');\nconst codec = GatewayEncoders.json<IOGateway.Message>();\n\nfunction principalName(authentication: Authentication): string | undefined {\n let name: string | undefined;\n if (authentication.authenticated) {\n name = authentication.name;\n if (name === undefined) {\n if (authentication['principal'] !== undefined) {\n const principal = authentication['principal'];\n if (typeof principal === 'object') {\n name = principal.name;\n }\n if (name === undefined) {\n if (principal === undefined) {\n name = '';\n }\n else {\n name = String(principal);\n }\n }\n }\n }\n }\n return name;\n}\n\nfunction initClient(this: IOGateway.Gateway,\n socket: WebSocket,\n authenticationPromise: () => Promise<Authentication | undefined>,\n remoteAddress?: AddressInfo\n): IOGateway.GatewayClient<string> | undefined {\n const key = `${remoteAddress?.address}:${remoteAddress?.port}`;\n const host = remoteAddress?.address ?? '<unknown>';\n const opts = {\n key,\n host,\n codec,\n onAuthenticate: async (): Promise<IOGateway.Auth.AuthenticationSuccess> => {\n const authentication = (await authenticationPromise());\n if (authentication?.authenticated) {\n return {type: 'success', user: principalName(authentication)};\n }\n throw new Error(`no valid client authentication ${key}`);\n },\n onPing: () => {\n socket.ping((err?: Error) => {\n if (err) {\n log.warn(`failed to ping ${key}`, err);\n }\n else {\n log.info(`ping sent to ${key}`);\n }\n });\n },\n onDisconnect: (reason: 'inactive' | 'shutdown') => {\n switch (reason) {\n case 'inactive': {\n log.warn(`no heartbeat (ping) received from ${key}, closing socket`);\n socket.close(4001, 'ping expected');\n break;\n }\n case 'shutdown': {\n socket.close(1001, 'shutdown');\n break;\n }\n }\n }\n };\n try {\n return this.client((data) => socket.send(data), opts as IOGateway.GatewayClientOptions<string>);\n } catch (err) {\n log.warn(`${key} failed to create client`, err);\n }\n}\n\nasync function create(this: IOGateway.Gateway, environment: {\n endpoint: string,\n storage?: AsyncLocalStorage<{ }>\n}): Promise<ServerWebSocketHandler> {\n log.info(`starting gateway on ${environment.endpoint}`);\n await this.start(environment);\n\n\n return async ({socket, handshake}) => {\n const {logPrefix, remoteAddress, principal: principalPromise} = handshake;\n log.info(`${logPrefix}connected on gw using ${remoteAddress?.address}`);\n const client = initClient.call(this, socket, principalPromise, remoteAddress);\n if (!client) {\n log.error(`${logPrefix}gw client init failed`);\n socket.terminate();\n return;\n }\n socket.on('error', (err: Error) => {\n log.error(`${logPrefix}websocket error: ${err}`, err);\n });\n\n const contextFn = environment.storage !== undefined ? AsyncLocalStorage.snapshot() : undefined;\n socket.on('message', (data, _isBinary) => {\n if (Array.isArray(data)) {\n data = Buffer.concat(data);\n }\n // JSON.parse will invoke abstract operation ToString (coerce it) prior parsing (https://262.ecma-international.org/5.1/#sec-15.12.2)\n if (contextFn !== undefined) {\n contextFn(() => client.send(data as unknown as string))\n }\n else {\n client.send(data as unknown as string);\n }\n\n });\n socket.on('close', (code) => {\n log.info(`${logPrefix}disconnected from gw. code: ${code}`);\n client.close();\n });\n }\n}\n\nexport default create;\n", "type MiddlewareFunction<T> = ((ctx: T, next: (ctx?: T) => Promise<void>) => Promise<void>) | ((ctx: T) => Promise<void>);\n\n// https://github.com/koajs/compose/blob/master/index.js\n/**\n * @typeParam T - Type of context passed through the middleware\n * @param middleware middleware stack\n * @return {MiddlewareFunction}\n */\nexport function compose<T>(...middleware: MiddlewareFunction<T>[]): (ctx: T) => Promise<void> {\n if (!Array.isArray(middleware)) {\n throw new Error('middleware must be array!');\n }\n const fns = middleware.flat();\n for (const fn of fns) {\n if (typeof fn !== 'function') {\n throw new Error('middleware must be compose of functions!');\n }\n }\n return async function (ctx: T, next?: (ctx?: T) => Promise<void>) {\n const dispatch = async (i: number, dispatchedCtx: T): Promise<void> => {\n const fn = i === fns.length ? next : fns[i];\n if (fn === undefined) {\n return;\n }\n let nextCalled = false;\n let nextResolved = false;\n const nextFn = async (nextCtx?: T) => {\n if (nextCalled) {\n throw new Error('next() called multiple times');\n }\n nextCalled = true;\n try {\n return await dispatch(i + 1, nextCtx ?? dispatchedCtx);\n }\n finally {\n nextResolved = true;\n }\n };\n const result = await fn(dispatchedCtx, nextFn);\n if (nextCalled && !nextResolved) {\n throw new Error('middleware resolved before downstream.\\n\\t You are probably missing an await or return statement in your middleware function.');\n }\n return result;\n }\n\n return dispatch(0, ctx);\n }\n}\n", "import type {\n HeaderValues,\n HttpCookie,\n HttpHeaders,\n MutableHttpHeaders,\n ReadonlyHeaderValues,\n ResponseCookie\n} from '../../types/web/http';\n\nimport { Cookie } from 'tough-cookie';\n\nfunction parseHost(headers: HttpHeaders, defaultHost?: string): string | undefined {\n let host = headers.get('x-forwarded-for');\n if (host === undefined) {\n host = headers.get('x-forwarded-host');\n if (Array.isArray(host)) {\n host = host[0];\n }\n if (host) {\n const port = headers.one('x-forwarded-port');\n if (port) {\n host = `${host}:${port}`;\n }\n }\n host ??= headers.one('host');\n }\n if (Array.isArray(host)) {\n host = host[0];\n }\n if (host) {\n return (host as string).split(',', 1)[0].trim();\n }\n\n\n\n return defaultHost;\n}\n\nfunction parseProtocol(headers: HttpHeaders, defaultProtocol: string): string {\n let proto = headers.get('x-forwarded-proto');\n\n if (Array.isArray(proto)) {\n proto = proto[0];\n }\n if (proto !== undefined) {\n return (proto as string).split(',', 1)[0].trim();\n }\n return defaultProtocol;\n}\n\nexport abstract class AbstractHttpMessage<Headers extends HttpHeaders = HttpHeaders> {\n readonly #headers: Headers;\n protected constructor(headers: Headers) {\n this.#headers = headers;\n }\n get headers(): Headers {\n return this.#headers;\n }\n}\n\nexport abstract class AbstractHttpRequest<Headers extends HttpHeaders = HttpHeaders> extends AbstractHttpMessage<Headers> {\n private static logIdCounter = 0;\n #id?: string\n\n get id(): string {\n if (this.#id === undefined) {\n this.#id = `${this.initId()}-${++AbstractHttpRequest.logIdCounter}`;\n\n }\n return this.#id;\n }\n\n protected initId(): string {\n return 'request';\n }\n\n get cookies(): HttpCookie[] {\n return parseCookies(this.headers);\n }\n\n protected parseHost(defaultHost?: string): string | undefined {\n return parseHost(this.headers, defaultHost);\n }\n\n\n protected parseProtocol(defaultProtocol: string): string {\n return parseProtocol(this.headers, defaultProtocol);\n }\n}\n\nexport abstract class AbstractHttpResponse<Headers extends HttpHeaders = HttpHeaders> extends AbstractHttpMessage<Headers> {\n get cookies(): ResponseCookie[] {\n return parseResponseCookies(this.headers);\n }\n\n protected setCookieValue(responseCookie: ResponseCookie): string {\n const cookie = new Cookie({\n key: responseCookie.name,\n value: responseCookie.value,\n maxAge: responseCookie.maxAge,\n domain: responseCookie.domain,\n path: responseCookie.path,\n secure: responseCookie.secure,\n httpOnly: responseCookie.httpOnly,\n sameSite: responseCookie.sameSite\n });\n return cookie.toString();\n }\n\n}\n\nfunction parseHeader(value: string): string[] {\n const list: string[] = [];\n {\n let start = 0;\n let end = 0;\n\n for (let i = 0; i < value.length; i++) {\n switch (value.charCodeAt(i)) {\n case 0x20: // space\n if (start === end) {\n start = end = i + 1;\n }\n break;\n case 0x2c: // comma\n list.push(value.slice(start, end));\n start = end = i + 1;\n break;\n default:\n end = end + 1;\n break;\n }\n }\n list.push(value.slice(start, end));\n }\n\n return list;\n}\n\nfunction toList(values: number | string | (readonly string[]) | undefined): string[] {\n if (typeof values === 'string') {\n values = [values];\n }\n if (typeof values === 'number') {\n values = [String(values)];\n }\n const list: string[] = [];\n if (values) {\n for (const value of values) {\n if (value) {\n list.push(...parseHeader(value));\n }\n }\n }\n return list;\n}\n\nfunction parseCookies(headers: HttpHeaders): HttpCookie[] {\n return headers.list('cookie')\n .map(s => s.split(';').map((cs) => Cookie.parse(cs)))\n .flat(1)\n .filter((tc) => tc !== undefined)\n .map((tc) => {\n const result: HttpCookie = Object.freeze({name: tc.key, value: tc.value});\n return result;\n });\n}\n\ntype Mutable<T> = {-readonly [K in keyof T]: T[K]};\n\nfunction parseResponseCookies(headers: HttpHeaders): ResponseCookie[] {\n return headers.list('set-cookie').map((cookie) => {\n const parsed = Cookie.parse(cookie);\n if (parsed) {\n const result: Mutable<ResponseCookie> = {name: parsed.key, value: parsed.value, maxAge: Number(parsed.maxAge ?? -1)};\n if (parsed.httpOnly) result.httpOnly = true;\n if (parsed.domain) result.domain = parsed.domain;\n if (parsed.path) result.path = parsed.path;\n if (parsed.secure) result.secure = true;\n if (parsed.httpOnly) result.httpOnly = true;\n if (parsed.sameSite) result.sameSite = parsed.sameSite as 'strict' | 'lax' | 'none';\n return Object.freeze(result);\n }\n }).filter((cookie): cookie is ResponseCookie => cookie !== undefined);\n}\n\n\nexport abstract class AbstractHttpHeaders<Values extends HeaderValues | ReadonlyHeaderValues> {\n protected constructor() {}\n abstract get(name: string): Values\n\n toList(name: string): string[] {\n const values = this.get(name);\n return toList(values);\n }\n}\n\nexport class MapHttpHeaders extends Map<string, (readonly string[])> implements MutableHttpHeaders {\n\n get(name: string) {\n return super.get(name.toLowerCase());\n }\n\n one(name: string): string | undefined {\n return this.get(name)?.[0];\n }\n\n list(name: string) {\n const values = super.get(name.toLowerCase());\n return toList(values);\n }\n\n set(name: string, value: number | string | (readonly string[]) | undefined): this {\n if (typeof value === 'number') {\n value = String(value);\n }\n if (typeof value === 'string') {\n value = [value];\n }\n if (value) {\n return super.set(name.toLowerCase(), value);\n }\n else {\n super.delete(name.toLowerCase());\n return this;\n }\n }\n\n add(name: string, value: string | (readonly string[])) {\n const prev = super.get(name.toLowerCase());\n if (typeof value === 'string') {\n value = [value];\n }\n if (prev) {\n value = prev.concat(value);\n }\n this.set(name, value);\n return this;\n }\n}\n", "import type {HttpStatusCode} from '../../types/web/http';\n\nclass DefaultHttpStatusCode implements HttpStatusCode {\n readonly #value: number;\n constructor(value: number) {\n this.#value = value;\n }\n get value(): number {\n return this.#value;\n }\n\n toString(): string {\n return this.#value.toString();\n }\n}\n\nexport class HttpStatus implements HttpStatusCode {\n static readonly CONTINUE = new HttpStatus(100, 'Continue');\n static readonly SWITCHING_PROTOCOLS = new HttpStatus(101, 'Switching Protocols');\n\n // 2xx Success\n\n static readonly OK = new HttpStatus(200, 'OK');\n static readonly CREATED = new HttpStatus(201, 'Created');\n static readonly ACCEPTED = new HttpStatus(202, 'Accepted');\n static readonly NON_AUTHORITATIVE_INFORMATION = new HttpStatus(203, 'Non-Authoritative Information');\n static readonly NO_CONTENT = new HttpStatus(204, 'No Content');\n static readonly RESET_CONTENT = new HttpStatus(205, 'Reset Content');\n static readonly PARTIAL_CONTENT = new HttpStatus(206, 'Partial Content');\n static readonly MULTI_STATUS = new HttpStatus(207, 'Multi-Status');\n\n static readonly IM_USED = new HttpStatus(226, 'IM Used');\n\n\n // 3xx Redirection\n static readonly MULTIPLE_CHOICES = new HttpStatus(300, 'Multiple Choices');\n static readonly MOVED_PERMANENTLY = new HttpStatus(301, 'Moved Permanently');\n\n // 4xx Client Error\n\n static readonly BAD_REQUEST = new HttpStatus(400, 'Bad Request');\n static readonly UNAUTHORIZED = new HttpStatus(401, 'Unauthorized');\n static readonly FORBIDDEN = new HttpStatus(403, 'Forbidden');\n static readonly NOT_FOUND = new HttpStatus(404, 'Not Found');\n static readonly METHOD_NOT_ALLOWED = new HttpStatus(405, 'Method Not Allowed');\n static readonly NOT_ACCEPTABLE = new HttpStatus(406, 'Not Acceptable');\n static readonly PROXY_AUTHENTICATION_REQUIRED = new HttpStatus(407, 'Proxy Authentication Required');\n static readonly REQUEST_TIMEOUT = new HttpStatus(408, 'Request Timeout');\n static readonly CONFLICT = new HttpStatus(409, 'Conflict');\n static readonly GONE = new HttpStatus(410, 'Gone');\n static readonly LENGTH_REQUIRED = new HttpStatus(411, 'Length Required');\n static readonly PRECONDITION_FAILED = new HttpStatus(412, 'Precondition Failed');\n static readonly PAYLOAD_TOO_LARGE = new HttpStatus(413, 'Payload Too Large');\n static readonly URI_TOO_LONG = new HttpStatus(414, 'URI Too Long');\n static readonly UNSUPPORTED_MEDIA_TYPE = new HttpStatus(415, 'Unsupported Media Type');\n\n static readonly EXPECTATION_FAILED = new HttpStatus(417, 'Expectation Failed');\n static readonly IM_A_TEAPOT = new HttpStatus(418, 'I\\'m a teapot');\n\n static readonly TOO_EARLY = new HttpStatus(425, 'Too Early');\n static readonly UPGRADE_REQUIRED = new HttpStatus(426, 'Upgrade Required');\n static readonly PRECONDITION_REQUIRED = new HttpStatus(428, 'Precondition Required');\n static readonly TOO_MANY_REQUESTS = new HttpStatus(429, 'Too Many Requests');\n static readonly REQUEST_HEADER_FIELDS_TOO_LARGE = new HttpStatus(431, 'Request Header Fields Too Large');\n static readonly UNAVAILABLE_FOR_LEGAL_REASONS = new HttpStatus(451, 'Unavailable For Legal Reasons');\n\n // 5xx Server Error\n static readonly INTERNAL_SERVER_ERROR = new HttpStatus(500, 'Internal Server Error');\n static readonly NOT_IMPLEMENTED = new HttpStatus(501, 'Not Implemented');\n static readonly BAD_GATEWAY = new HttpStatus(502, 'Bad Gateway');\n static readonly SERVICE_UNAVAILABLE = new HttpStatus(503, 'Service Unavailable');\n static readonly GATEWAY_TIMEOUT = new HttpStatus(504, 'Gateway Timeout');\n static readonly HTTP_VERSION_NOT_SUPPORTED = new HttpStatus(505, 'HTTP Version Not Supported');\n static readonly VARIANT_ALSO_NEGOTIATES = new HttpStatus(506, 'Variant Also Negotiates');\n static readonly INSUFFICIENT_STORAGE = new HttpStatus(507, 'Insufficient Storage');\n static readonly LOOP_DETECTED = new HttpStatus(508, 'Loop Detected');\n static readonly NOT_EXTENDED = new HttpStatus(510, 'Not Extended');\n static readonly NETWORK_AUTHENTICATION_REQUIRED = new HttpStatus(511, 'Network Authentication Required');\n\n static readonly #VALUES: HttpStatus[] = [];\n static {\n Object.keys(HttpStatus)\n .filter(key => key !== 'VALUES' && key !== 'resolve')\n .forEach(key => {\n const value = HttpStatus[key];\n if (value instanceof HttpStatus) {\n Object.defineProperty(value, 'name', {enumerable: true, value: key, writable: false});\n HttpStatus.#VALUES.push(value);\n }\n })\n\n }\n\n static resolve(code: number): HttpStatus | undefined {\n for (const status of HttpStatus.#VALUES) {\n if (status.value === code) {\n return status;\n }\n }\n }\n\n readonly #value: number;\n readonly #phrase: string;\n\n private constructor(value: number, phrase: string) {\n this.#value = value;\n this.#phrase = phrase;\n }\n\n get value(): number {\n return this.#value;\n }\n get phrase(): string {\n return this.#phrase;\n }\n\n toString() {\n return `${this.#value} ${this['name']}`;\n }\n}\n\n\nexport function httpStatusCode(value: number | HttpStatusCode): HttpStatusCode {\n if (typeof value === 'number') {\n if (value < 100 || value > 999) {\n throw new Error(`status code ${value} should be in range 100-999`);\n }\n const status = HttpStatus.resolve(value);\n if (status !== undefined) {\n return status;\n }\n return new DefaultHttpStatusCode(value);\n }\n return value;\n}\n", "import type {Principal} from '../../types/auth';\nimport {\n BodyChunk,\n HeaderValue,\n HeaderValues,\n HttpCookie,\n HttpMethod,\n HttpStatusCode,\n MutableHttpHeaders,\n ReadonlyHeaderValues,\n ReadonlyHttpHeaders,\n ResponseCookie\n} from '../../types/web/http'\n\nimport type {ServerHttpRequest, ServerHttpResponse, ServerWebExchange} from '../../types/web/server';\nimport {AbstractHttpHeaders, AbstractHttpRequest, AbstractHttpResponse} from '../http/exchange.js';\nimport {httpStatusCode} from '../http/status.js';\nimport net from 'node:net';\nimport http from 'node:http';\nimport http2 from 'node:http2';\n\nexport class ExtendedHttpIncomingMessage extends http.IncomingMessage {\n // circular reference to the exchange\n exchange?: ServerWebExchange;\n\n upgradeHead?: Buffer<ArrayBufferLike>\n\n\n get urlBang(): string {\n return this.url!;\n }\n\n get socketEncrypted(): boolean {\n // see tls.TLSSocket#encrypted\n return this.socket['encrypted'] === true;\n }\n}\n\nexport class ExtendedHttpServerResponse<Request extends ExtendedHttpIncomingMessage = ExtendedHttpIncomingMessage> extends http.ServerResponse<Request> {\n\n markHeadersSent(): void {\n this['_header'] = true; // prevent response from being sent\n }\n\n getRawHeaderNames(): string[] {\n return super['getRawHeaderNames']();\n }\n}\n\nabstract class AbstractServerHttpRequest extends AbstractHttpRequest<ReadonlyHttpHeaders> {\n abstract getNativeRequest<T>(): T;\n}\n\nexport abstract class AbstractServerHttpResponse extends AbstractHttpResponse<MutableHttpHeaders> {\n #cookies: ResponseCookie[] = [];\n #statusCode?: HttpStatusCode;\n #state: 'new' | 'committing' | 'commit-action-failed' | 'committed' = 'new';\n #commitActions: (() => Promise<void>)[] = [];\n\n setStatusCode(statusCode?: HttpStatusCode): boolean {\n if (this.#state === 'committed') {\n return false;\n }\n else {\n this.#statusCode = statusCode;\n return true;\n }\n }\n\n setRawStatusCode(statusCode?: number): boolean {\n return this.setStatusCode(statusCode === undefined ? undefined : httpStatusCode(statusCode));\n };\n\n get statusCode(): HttpStatusCode | undefined {\n return this.#statusCode;\n }\n\n addCookie(cookie: ResponseCookie): this {\n if (this.#state === 'committed') {\n throw new Error(`Cannot add cookie ${JSON.stringify(cookie)} because HTTP response has already been committed`);\n }\n this.#cookies.push(cookie);\n return this;\n }\n\n abstract getNativeResponse<T>(): T;\n\n beforeCommit(action: () => Promise<void>): void {\n this.#commitActions.push(action);\n }\n\n get commited(): boolean {\n const state = this.#state;\n return state !== 'new' && state !== 'commit-action-failed';\n }\n\n async body(body: ReadableStream<BodyChunk> | Promise<BodyChunk | void> | BodyChunk): Promise<boolean> {\n if (body instanceof ReadableStream) {\n throw new Error('ReadableStream body not supported yet');\n }\n const buffer = await body;\n try {\n // touch buffers if needed\n\n return await this.doCommit(async () => {\n\n return await this.bodyInternal(Promise.resolve(buffer));\n\n }).catch(error => {\n // release buffers\n throw error;\n });\n }\n catch (error) {\n // clear content headers\n throw error;\n }\n }\n\n abstract bodyInternal(body: Promise<BodyChunk | void>): Promise<boolean>;\n\n async end(): Promise<boolean> {\n if (!this.commited) {\n return this.doCommit(async () => {\n return await this.bodyInternal(Promise.resolve());\n });\n }\n else {\n return Promise.resolve(false);\n }\n }\n\n protected doCommit(writeAction?: () => Promise<boolean>): Promise<boolean> {\n const state = this.#state;\n let allActions = Promise.resolve();\n if (state === 'new') {\n this.#state = 'committing';\n if (this.#commitActions.length > 0) {\n allActions = this.#commitActions\n .reduce(\n (acc, cur) => acc.then(() => cur()),\n Promise.resolve())\n .catch((error) => {\n const state = this.#state;\n if (state === 'committing') {\n this.#state = 'commit-action-failed';\n // clear content headers\n }\n });\n }\n }\n else if (state === 'commit-action-failed') {\n this.#state = 'committing';\n // skip commit actions\n } else {\n return Promise.resolve(false);\n }\n\n allActions = allActions.then(() => {\n this.applyStatusCode();\n this.applyHeaders();\n this.applyCookies();\n this.#state = 'committed';\n });\n\n\n return allActions.then(async () => {\n return writeAction !== undefined ? await writeAction() : true;\n });\n }\n\n protected applyStatusCode(): void {}\n protected applyHeaders(): void {}\n protected applyCookies(): void {}\n}\nexport class HttpServerRequest extends AbstractServerHttpRequest implements ServerHttpRequest {\n\n #url?: URL;\n #cookies?: HttpCookie[];\n readonly #req: ExtendedHttpIncomingMessage;\n\n constructor(req: ExtendedHttpIncomingMessage) {\n super(new IncomingMessageHeaders(req));\n this.#req = req;\n }\n\n getNativeRequest<T>(): T {\n return this.#req as T;\n }\n\n get upgrade() {\n return this.#req['upgrade'];\n }\n\n get http2(): boolean {\n return this.#req.httpVersionMajor >= 2;\n }\n\n get path() {\n return this.URL?.pathname;\n }\n\n get URL() {\n this.#url ??= new URL(this.#req.urlBang, `${this.protocol}://${this.host}`);\n return this.#url;\n }\n\n get query() {\n return this.URL?.search;\n }\n\n get method() {\n return this.#req.method as HttpMethod;\n }\n\n get host() {\n let dh: string | undefined = undefined;\n if (this.#req.httpVersionMajor >= 2) {\n dh = (this.#req.headers as http2.IncomingHttpHeaders)[':authority'];\n }\n dh ??= this.#req.socket.remoteAddress;\n return super.parseHost(dh);\n }\n\n get protocol() {\n let dp: string | undefined = undefined;\n if (this.#req.httpVersionMajor > 2) {\n dp = (this.#req.headers as http2.IncomingHttpHeaders)[':scheme'];\n }\n dp ??= this.#req.socketEncrypted ? 'https' : 'http';\n return super.parseProtocol(dp);\n }\n\n get socket() {\n return this.#req.socket;\n }\n\n get remoteAddress(): net.AddressInfo | undefined {\n const family = this.#req.socket.remoteFamily;\n const address = this.#req.socket.remoteAddress;\n const port = this.#req.socket.remotePort;\n\n if (!family || !address || !port) {\n return undefined;\n }\n return {family, address, port};\n }\n\n get cookies(): HttpCookie[] {\n this.#cookies ??= super.cookies\n return this.#cookies;\n }\n\n get body(): ReadableStream<BodyChunk> {\n return http.IncomingMessage.toWeb(this.#req) as ReadableStream<BodyChunk>;\n }\n\n async blob() {\n const chunks: BodyChunk[] = [];\n if (this.body !== undefined) {\n for await (const chunk of this.body) {\n chunks.push(chunk);\n }\n }\n return new Blob(chunks, {type: this.headers.one('content-type') as string || 'application/octet-stream'});\n }\n\n\n async text() {\n const blob = await this.blob();\n return await blob.text();\n }\n\n async formData() {\n const blob = await this.blob();\n const text = await blob.text();\n return new URLSearchParams(text);\n }\n\n async json() {\n const blob = await this.blob();\n if (blob.size === 0) {\n return undefined;\n }\n const text = await blob.text();\n return JSON.parse(text);\n }\n\n\n protected initId(): string {\n const remoteIp = this.#req.socket.remoteAddress;\n if (!remoteIp) {\n throw new Error('Socket has no remote address');\n }\n return `${remoteIp}:${this.#req.socket.remotePort}`;\n }\n}\n\nclass IncomingMessageHeaders extends AbstractHttpHeaders<ReadonlyHeaderValues> implements ReadonlyHttpHeaders {\n readonly #msg: ExtendedHttpIncomingMessage;\n\n constructor(msg: ExtendedHttpIncomingMessage) {\n super();\n this.#msg = msg;\n }\n\n has(name: string): boolean {\n return this.#msg.headers[name] !== undefined;\n }\n\n get(name: string): string | (readonly string[]) | undefined {\n return this.#msg.headers[name];\n }\n\n list(name: string): string[] {\n return super.toList(name);\n }\n\n one(name: string): string | undefined {\n const value = this.#msg.headers[name];\n if (Array.isArray(value)) {\n return value[0];\n }\n return value;\n }\n\n keys() {\n return Object.keys(this.#msg.headers).values();\n }\n}\n\nclass OutgoingMessageHeaders extends AbstractHttpHeaders<HeaderValues> implements MutableHttpHeaders {\n readonly #msg: ExtendedHttpServerResponse;\n\n constructor(msg: ExtendedHttpServerResponse) {\n super();\n this.#msg = msg;\n }\n\n has(name: string): boolean {\n return this.#msg.hasHeader(name);\n }\n\n keys() {\n return this.#msg.getHeaderNames().values();\n }\n\n get(name: string): HeaderValues {\n return this.#msg.getHeader(name);\n }\n\n one(name: string): HeaderValue {\n const value = this.#msg.getHeader(name);\n if (Array.isArray(value)) {\n return value[0];\n }\n return value;\n }\n\n set(name: string, value: HeaderValues): this {\n if (!this.#msg.headersSent) {\n if (Array.isArray(value)) {\n value = value.map(v => typeof v === 'number' ? String(v) : v);\n } else if (typeof value === 'number') {\n value = String(value);\n }\n\n if (value) {\n this.#msg.setHeader(name, value);\n }\n else {\n this.#msg.removeHeader(name);\n }\n }\n return this;\n }\n\n add(name: string, value: string | (readonly string[])): this {\n if (!this.#msg.headersSent) {\n this.#msg.appendHeader(name, value);\n }\n return this;\n }\n\n list(name: string): string[] {\n return super.toList(name);\n }\n}\n\nexport class HttpServerResponse extends AbstractServerHttpResponse implements ServerHttpResponse {\n readonly #res: ExtendedHttpServerResponse;\n\n constructor(res: ExtendedHttpServerResponse) {\n super(new OutgoingMessageHeaders(res));\n this.#res = res;\n }\n\n getNativeResponse<T>(): T {\n return this.#res as T;\n }\n\n get statusCode(): HttpStatusCode {\n const status = super.statusCode;\n return status ?? {value: this.#res.statusCode};\n }\n\n applyStatusCode() {\n const status = super.statusCode;\n if (status !== undefined) {\n this.#res.statusCode = status.value;\n }\n }\n\n addCookie(cookie: ResponseCookie): this {\n this.headers.add('Set-Cookie', super.setCookieValue(cookie));\n return this;\n }\n\n async bodyInternal(body: Promise<BodyChunk | void>): Promise<boolean> {\n if (!this.#res.headersSent) {\n if (body instanceof ReadableStream) {\n // http.IncomingMessage.fromWeb(body)\n throw new Error('ReadableStream body not supported in response');\n }\n else {\n const chunk = await body;\n return await new Promise<boolean>((resolve, reject) => {\n try {\n if (chunk === undefined) {\n this.#res.end(() => {\n resolve(true);\n });\n } else {\n if (!this.headers.has('content-length')) {\n // set content-length if not already set\n if (typeof chunk === 'string') {\n this.headers.set('content-length', Buffer.byteLength(chunk));\n }\n else if (chunk instanceof Blob) {\n this.headers.set('content-length', chunk.size);\n }\n else {\n this.headers.set('content-length', chunk.byteLength);\n }\n }\n this.#res.end(chunk, () => {\n resolve(true);\n });\n }\n } catch (e) {\n reject(e instanceof Error ? e : new Error(`end failed: ${e}`));\n }\n });\n }\n }\n else {\n return false; // already ended\n }\n }\n}\n\nexport class ServerHttpRequestDecorator implements ServerHttpRequest {\n readonly #delegate: ServerHttpRequest;\n\n constructor(request: ServerHttpRequest) {\n this.#delegate = request;\n }\n\n get delegate(): ServerHttpRequest {\n return this.#delegate;\n }\n\n get id(): string {\n return this.#delegate.id;\n }\n\n get method(): HttpMethod {\n return this.#delegate.method;\n }\n\n get path(): string {\n return this.#delegate.path;\n }\n\n get protocol() {\n return this.#delegate.protocol;\n }\n\n get host(): string | undefined {\n return this.#delegate.host;\n }\n\n get URL(): URL {\n return this.#delegate.URL;\n }\n\n get headers(): ReadonlyHttpHeaders {\n return this.#delegate.headers;\n }\n\n get cookies(): HttpCookie[] {\n return this.#delegate.cookies;\n }\n\n get remoteAddress() {\n return this.#delegate.remoteAddress;\n }\n\n get upgrade() {\n return this.#delegate.upgrade;\n }\n\n get body(): ReadableStream<BodyChunk> | undefined {\n return this.#delegate.body;\n }\n async blob(): Promise<Blob> {\n return await this.#delegate.blob();\n }\n async text(): Promise<string> {\n return await this.#delegate.text();\n }\n async formData(): Promise<URLSearchParams> {\n return await this.#delegate.formData();\n }\n async json(): Promise<unknown> {\n return await this.#delegate.json();\n }\n\n toString(): string {\n return `${ServerHttpRequestDecorator.name} [delegate: ${this.delegate.toString()}]`;\n }\n\n public static getNativeRequest<T>(request: ServerHttpRequest): T {\n if (request instanceof AbstractServerHttpRequest) {\n return request.getNativeRequest<T>();\n }\n else if (request instanceof ServerHttpRequestDecorator) {\n return ServerHttpRequestDecorator.getNativeRequest<T>(request.delegate);\n }\n else {\n throw new Error(`Cannot get native request from ${request.constructor.name}`);\n }\n }\n}\nexport class ServerHttpResponseDecorator implements ServerHttpResponse {\n readonly #delegate: ServerHttpResponse;\n\n constructor(response: ServerHttpResponse) {\n this.#delegate = response;\n }\n\n get delegate(): ServerHttpResponse {\n return this.#delegate;\n }\n\n setStatusCode(statusCode: HttpStatusCode): boolean {\n return this.delegate.setStatusCode(statusCode);\n }\n\n setRawStatusCode(statusCode?: number): boolean {\n return this.delegate.setRawStatusCode(statusCode);\n }\n\n get statusCode(): HttpStatusCode {\n return this.delegate.statusCode;\n }\n\n get cookies(): ResponseCookie[] {\n return this.delegate.cookies;\n }\n\n addCookie(cookie: ResponseCookie): this {\n this.delegate.addCookie(cookie);\n return this;\n }\n\n async end(): Promise<boolean> {\n return await this.delegate.end();\n }\n\n async body(body: ReadableStream<BodyChunk> | Promise<BodyChunk | void> | BodyChunk): Promise<boolean> {\n return await this.#delegate.body(body);\n }\n\n get headers(): MutableHttpHeaders {\n return this.#delegate.headers;\n }\n\n toString(): string {\n return `${ServerHttpResponseDecorator.name} [delegate: ${this.delegate.toString()}]`;\n }\n\n public static getNativeResponse<T>(response: ServerHttpResponse): T {\n if (response instanceof AbstractServerHttpResponse) {\n return response.getNativeResponse<T>();\n }\n else if (response instanceof ServerHttpResponseDecorator) {\n return ServerHttpResponseDecorator.getNativeResponse<T>(response.delegate);\n }\n else {\n throw new Error(`Cannot get native response from ${response.constructor.name}`);\n }\n }\n}\n\nexport class ServerW