import type { IncomingMessage, Server, ServerResponse } from "node:http" import { createServer } from "node:http" import { networkInterfaces } from "node:os" import type { ApiCoreHandler, ApiHostStartResult, ApiHostOptions } from "../api/api-host.ts" import { ApiHost } from "../api/api-host.ts" import { ApiCoreNodeHttp } from "./api-core-node-http.ts" const internalGetListeningPort = (server: Server): number => { const address = server.address() if (address === null) { throw new Error("Server is not running.") } if (typeof address === "string") { throw new TypeError("HTTP server is not listening on a TCP port.") } return address.port } const internalFormatHttpHost = (address: string): string => { const normalizedAddress = address.replaceAll("%", "%25") if (normalizedAddress.includes(":")) { return `[${normalizedAddress}]` } return normalizedAddress } const internalBuildHttpUrlList = (port: number): string[] => { const urlSet = new Set([ `http://127.0.0.1:${port}`, `http://localhost:${port}`, `http://[::1]:${port}`, ]) for (const networkInterfaceInfo of Object.values(networkInterfaces()).flat()) { if ( networkInterfaceInfo === undefined || networkInterfaceInfo.address.length === 0 || networkInterfaceInfo.internal === true || networkInterfaceInfo.address === "0.0.0.0" || networkInterfaceInfo.address === "::" ) { continue } const address = internalFormatHttpHost(networkInterfaceInfo.address) urlSet.add(`http://${address}:${port}`) } return [...urlSet] } export interface ApiHostNodeHttpOptions extends ApiHostOptions {} export class ApiHostNodeHttp extends ApiHost { protected apiCoreHandler: ApiCoreHandler | undefined constructor(options: ApiHostNodeHttpOptions) { super(options) this.apiCoreHandler = undefined } attachApiCoreHandler(apiCoreHandler: ApiCoreHandler): void { this.apiCoreHandler = apiCoreHandler } detachApiCoreHandler(): void { this.apiCoreHandler = undefined } protected async handleRequest( request: IncomingMessage, response: ServerResponse, ): Promise { const apiCore = new ApiCoreNodeHttp({ request, response }) const apiCoreHandler = this.apiCoreHandler if (apiCoreHandler === undefined) { this.logger.error("Request handling skipped because no ApiCore handler is attached.") await apiCore.error() return } try { await apiCoreHandler(apiCore) } catch (exception) { this.logger.error(`Request handling failed, exception: ${String(exception)}`) await apiCore.error() } } async start(): Promise { if (this.server !== undefined) { throw new Error("Server is already running.") } const server = createServer((request, response) => { void this.handleRequest(request, response) }) const result = await new Promise((resolve, reject) => { const handleError = (error: Error): void => { server.off("listening", handleListening) this.clearRunningServer(server) reject(error) } const handleListening = (): void => { server.off("error", handleError) const startResult = { urlList: internalBuildHttpUrlList(internalGetListeningPort(server)), } this.setRunningServer(server, startResult) server.once("close", () => { this.clearRunningServer(server) }) resolve(this.getStartResultOrThrow()) } server.once("error", handleError) server.once("listening", handleListening) server.listen(this.options.port) }) return result } async close(): Promise { if (this.isRunning() === false) { return } const server = this.getServerOrThrow() await new Promise((resolve, reject) => { server.close((error) => { if (error !== undefined) { reject(error) return } resolve() }) }) } } export const createApiHostNodeHttp = (options: ApiHostNodeHttpOptions): ApiHostNodeHttp => { return new ApiHostNodeHttp(options) }