You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lib/src/service/HTTPServer.ts

182 lines
7.7 KiB

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<void> {
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<void> {
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<void>(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<void> {
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<void> {
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'],
}))
})
})
}
}
}