import { createServer as createHttpServer, type RequestListener, type Server as HttpServer, type ServerOptions, } from "node:http"; import { createServer as createHttpsServer, type Server as HttpsServer } from "node:https"; import type { AddressInfo } from "node:net"; import { Deferred } from "@milaboratories/helpers"; import type { PFrameInternal } from "@milaboratories/pl-model-middle-layer"; import { base64Encode, Base64Encoded, ensureError } from "@milaboratories/pl-model-common"; import { generate, type GenerateResult } from "selfsigned"; import { randomUUID } from "node:crypto"; import { authorizeRequestHandler } from "./handler"; /** Generate a self-signed certificate for localhost */ async function generateCertificate(): Promise { return await generate([{ name: "commonName", value: "localhost" }], { keySize: 2048, algorithm: "sha256", extensions: [ { name: "subjectAltName", altNames: [ { type: 2, value: "localhost" }, // DNS { type: 7, ip: "127.0.0.1" }, // IPv4 { type: 7, ip: "::1" }, // IPv6 ], }, ], }); } /** Create an object store URL from the server address info. */ function createObjectStoreUrl(info: AddressInfo, noHttps?: true): PFrameInternal.ObjectStoreUrl { const protocol = noHttps ? "http" : "https"; switch (info.family) { case "IPv4": return `${protocol}://${info.address}:${info.port}/` as PFrameInternal.ObjectStoreUrl; case "IPv6": return `${protocol}://[${info.address}]:${info.port}/` as PFrameInternal.ObjectStoreUrl; default: return `${protocol}://localhost:${info.port}/` as PFrameInternal.ObjectStoreUrl; } } /** * Serve HTTP requests using the provided handler. * Returns a promise that resolves when the server is stopped. */ export async function serve({ handler, port = 0, noHttps, noAuth, }: PFrameInternal.HttpServerOptions): Promise { const started = new Deferred(); try { let stopped: Deferred | null = null; let authToken: PFrameInternal.HttpAuthorizationToken | undefined; let effectiveHandler: RequestListener = handler; if (!noAuth) { authToken = randomUUID() as PFrameInternal.HttpAuthorizationToken; effectiveHandler = authorizeRequestHandler(effectiveHandler, authToken); } // Create HTTP server let encodedCaCert: Base64Encoded | undefined; const defaultOptions: ServerOptions = { keepAlive: true, }; let server: HttpServer | HttpsServer; if (noHttps) { server = createHttpServer(defaultOptions, effectiveHandler); } else { const { cert, private: key, public: ca } = await generateCertificate(); encodedCaCert = base64Encode(cert as PFrameInternal.PemCertificate); server = createHttpsServer({ ...defaultOptions, cert, key, ca }, effectiveHandler); } server .on("listening", () => { // Cast is safe by specification const url = createObjectStoreUrl(server.address() as AddressInfo, noHttps); stopped = new Deferred(); started.resolve({ get info(): PFrameInternal.HttpServerInfo { return { url, authToken, encodedCaCert }; }, get stopped(): Promise { return stopped!.promise; }, stop(): Promise { server.close(); return stopped!.promise; }, }); }) .on("error", (err) => { started.reject(err); stopped?.reject(err); }) .on("close", () => stopped?.resolve()) .listen({ host: "localhost", port, }); } catch (error: unknown) { started.reject(ensureError(error)); } return started.promise; }