import {Inject, Singleton} from '../di' import {HTTPStatus, universalPath, UniversalPath, withTimeout} from '../util' import {Unit} from '../lifecycle/Unit' import {createServer, IncomingMessage, RequestListener, Server, ServerResponse} from 'http' import {Logging} from './Logging' import {Request} from '../http/lifecycle/Request' import {HTTPKernel} from '../http/kernel/HTTPKernel' import {PoweredByHeaderInjectionHTTPModule} from '../http/kernel/module/PoweredByHeaderInjectionHTTPModule' import {SetSessionCookieHTTPModule} from '../http/kernel/module/SetSessionCookieHTTPModule' import {InjectSessionHTTPModule} from '../http/kernel/module/InjectSessionHTTPModule' import {PersistSessionHTTPModule} from '../http/kernel/module/PersistSessionHTTPModule' import {MountActivatedRouteHTTPModule} from '../http/kernel/module/MountActivatedRouteHTTPModule' import {ExecuteResolvedRouteHandlerHTTPModule} from '../http/kernel/module/ExecuteResolvedRouteHandlerHTTPModule' import {error} from '../http/response/ErrorResponseFactory' import {ExecuteResolvedRoutePreflightHTTPModule} from '../http/kernel/module/ExecuteResolvedRoutePreflightHTTPModule' import {ExecuteResolvedRoutePostflightHTTPModule} from '../http/kernel/module/ExecuteResolvedRoutePostflightHTTPModule' import {ParseIncomingBodyHTTPModule} from '../http/kernel/module/ParseIncomingBodyHTTPModule' import {Config} from './Config' import {InjectRequestEventBusHTTPModule} from '../http/kernel/module/InjectRequestEventBusHTTPModule' import {Routing} from './Routing' import {Route} from '../http/routing/Route' import {staticServer} from '../http/servers/static' import {EventBus} from '../event/EventBus' import {PackageDiscovered} from '../support/PackageDiscovered' /** * Application unit that starts the HTTP/S server, creates Request and Response objects * for it, and handles those requests using the HTTPKernel. */ @Singleton() export class HTTPServer extends Unit { @Inject() protected readonly logging!: Logging @Inject() protected readonly config!: Config @Inject() protected readonly kernel!: HTTPKernel @Inject() protected readonly routing!: Routing @Inject() protected readonly bus!: EventBus /** The underlying native Node.js server. */ protected server?: Server /** * Register an asset directory for the given package. * This creates a static server route for the package with the * configured vendor prefix. * @param packageName * @param basePath */ public async registerVendorAssets(packageName: string, basePath: UniversalPath): Promise { if ( this.config.get('server.builtIns.vendor.enabled', true) ) { this.logging.debug(`Registering vendor assets route for package ${packageName} on ${basePath}...`) await this.routing.registerRoutes(() => { const prefix = this.config.get('server.builtIns.vendor.prefix', '/vendor') Route.group(prefix, () => { Route.group(packageName, () => { Route.get('/**', staticServer({ basePath, directoryListing: false, })) }) }) }) } } public async up(): Promise { const port = this.config.get('server.port', 8000) // TODO register these by config PoweredByHeaderInjectionHTTPModule.register(this.kernel) SetSessionCookieHTTPModule.register(this.kernel) InjectSessionHTTPModule.register(this.kernel) PersistSessionHTTPModule.register(this.kernel) MountActivatedRouteHTTPModule.register(this.kernel) ExecuteResolvedRouteHandlerHTTPModule.register(this.kernel) ExecuteResolvedRoutePreflightHTTPModule.register(this.kernel) ExecuteResolvedRoutePostflightHTTPModule.register(this.kernel) ParseIncomingBodyHTTPModule.register(this.kernel) InjectRequestEventBusHTTPModule.register(this.kernel) await this.registerBuiltIns() await new Promise(res => { this.server = createServer(this.handler) this.server.listen(port, () => { this.logging.success(`Server listening on port ${port}. Press ^C to stop.`) }) process.on('SIGINT', res) }) } public async down(): Promise { if ( this.server ) { this.server.close(err => { if ( err ) { this.logging.error(`Error encountered while closing HTTP server: ${err.message}`) this.logging.debug(err) } }) } } public get handler(): RequestListener { const timeout = this.config.get('server.timeout', 10000) return async (request: IncomingMessage, response: ServerResponse) => { const extolloReq = new Request(request, response) withTimeout(timeout, extolloReq.response.sent$.toPromise()) .onTime(() => { this.logging.verbose(`Request lifecycle finished on time. (Path: ${extolloReq.path})`) }) .late(() => { if ( !extolloReq.bypassTimeout ) { this.logging.warn(`Request lifecycle finished late, so an error response was returned! (Path: ${extolloReq.path})`) } }) .timeout(() => { if ( extolloReq.bypassTimeout ) { this.logging.info(`Request lifecycle has timed out, but bypassRequest was set. (Path: ${extolloReq.path})`) return } this.logging.error(`Request lifecycle has timed out. Will send error response instead. (Path: ${extolloReq.path})`) extolloReq.response.setStatus(HTTPStatus.REQUEST_TIMEOUT) extolloReq.response.body = 'Sorry, your request timed out.' extolloReq.response.send() }) .run() .catch(e => this.logging.error(e)) try { await this.kernel.handle(extolloReq) } catch (e) { await error(e).write(extolloReq) } await extolloReq.response.send() } } /** Register built-in servers and routes. */ protected async registerBuiltIns(): Promise { const extolloAssets = universalPath(__dirname, '..', 'resources', 'assets') await this.registerVendorAssets('@extollo/lib', extolloAssets) this.bus.subscribe(PackageDiscovered, async (event: PackageDiscovered) => { if ( event.packageConfig?.extollo?.assets?.discover && event.packageConfig.name ) { this.logging.debug(`Registering vendor assets for discovered package: ${event.packageConfig.name}`) const basePath = event.packageConfig?.extollo?.assets?.basePath if ( basePath && Array.isArray(basePath) ) { const assetPath = event.packageJson.concat('..', ...basePath) await this.registerVendorAssets(event.packageConfig.name, assetPath) } } }) if ( this.config.get('server.builtIns.assets.enabled', true) ) { const prefix = this.config.get('server.builtIns.assets.prefix', '/assets') this.logging.debug(`Registering built-in assets server with prefix: ${prefix}`) await this.routing.registerRoutes(() => { Route.group(prefix, () => { Route.get('/**', staticServer({ directoryListing: false, basePath: ['resources', 'assets'], })) }) }) } } }