import * as plugins from '../plugins.js'; import { Route } from './classes.route.js'; import { Handler } from './classes.handler.js'; import { HandlerTypedRouter } from './classes.handlertypedrouter.js'; // export types import { setupRobots } from './tools.robots.js'; import { setupManifest } from './tools.manifest.js'; import { Sitemap } from './classes.sitemap.js'; import { Feed } from './classes.feed.js'; import { type IServerOptions } from '../classes.typedserver.js'; export type TServerStatus = 'initiated' | 'running' | 'stopped'; /** * can be used to spawn a server to answer http/https calls * for constructor options see [[IServerOptions]] */ export class Server { public httpServer: plugins.http.Server | plugins.https.Server; public expressAppInstance: plugins.express.Application; public routeObjectMap = new Array(); public options: IServerOptions; public serverStatus: TServerStatus = 'initiated'; public feed: Feed; public sitemap: Sitemap; public executeAfterStartFunctions: (() => Promise)[] = []; // do stuff when server is ready private startedDeferred = plugins.smartpromise.defer(); // tslint:disable-next-line:member-ordering public startedPromise = this.startedDeferred.promise; private socketMap = new plugins.lik.ObjectMap(); constructor(optionsArg: IServerOptions) { this.options = { ...optionsArg, }; } /** * allows updating of server options * @param optionsArg */ public updateServerOptions(optionsArg: IServerOptions) { Object.assign(this.options, optionsArg); } public addTypedRequest(typedrouter: plugins.typedrequest.TypedRouter) { this.addRoute('/typedrequest', new HandlerTypedRouter(typedrouter)); } public addTypedSocket(typedrouter: plugins.typedrequest.TypedRouter): void { this.executeAfterStartFunctions.push(async () => { plugins.typedsocket.TypedSocket.createServer(typedrouter, this); }); } public addRoute(routeStringArg: string, handlerArg?: Handler) { const route = new Route(this, routeStringArg); if (handlerArg) { route.addHandler(handlerArg); } this.routeObjectMap.push(route); return route; } public addRouteBefore(routeStringArg: string, handlerArg?: Handler) { const route = new Route(this, routeStringArg); if (handlerArg) { route.addHandler(handlerArg); } this.routeObjectMap.unshift(route); return route; } /** * starts the server and sets up the routes * @param portArg * @param doListen */ public async start(portArg: number | string = this.options.port, doListen = true) { const done = plugins.smartpromise.defer(); if (typeof portArg === 'string') { portArg = parseInt(portArg); } this.expressAppInstance = plugins.express(); if (!this.httpServer && (!this.options.privateKey || !this.options.publicKey)) { console.log('Got no SSL certificates. Please ensure encryption using e.g. a reverse proxy'); this.httpServer = plugins.http.createServer(this.expressAppInstance); } else if (!this.httpServer) { console.log('Got SSL certificate. Using it for the http server'); this.httpServer = plugins.https.createServer( { key: this.options.privateKey, cert: this.options.publicKey, }, this.expressAppInstance ); } else { console.log('Using externally supplied http server'); } this.httpServer.keepAliveTimeout = 600 * 1000; this.httpServer.headersTimeout = 20 * 1000; // general request handlling this.expressAppInstance.use((req, res, next) => { next(); }); // forceSsl if (this.options.forceSsl) { this.expressAppInstance.set('forceSSLOptions', { enable301Redirects: true, trustXFPHeader: true, sslRequiredMessage: 'SSL Required.', }); this.expressAppInstance.use(plugins.expressForceSsl); } // cors if (this.options.cors) { const cors = plugins.cors({ allowedHeaders: '*', methods: '*', origin: '*', }); this.expressAppInstance.use(cors); this.expressAppInstance.options('/*', cors); } this.expressAppInstance.use((req, res, next) => { res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin'); res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('SERVEZONE_ROUTE', 'LOSSLESS_ORIGIN_CONTAINER'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Expires', new Date(Date.now()).toUTCString()); next(); }); // body parsing this.expressAppInstance.use(async (req, res, next) => { if (req.headers['content-type'] === 'application/json') { let data = ''; req.on('data', chunk => { data += chunk; }); req.on('end', () => { try { req.body = plugins.smartjson.parse(data); next(); } catch (error) { res.status(400).send('Invalid JSON'); } }); } else { next(); } }); this.expressAppInstance.use(plugins.bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded // robots if (this.options.robots && this.options.domain) { await setupRobots(this, this.options.domain); } // manifest.json if (this.options.manifest) { await setupManifest(this.expressAppInstance, this.options.manifest); } // sitemaps if (this.options.sitemap) { this.sitemap = new Sitemap(this); } if (this.options.feed) { // feed this.feed = new Feed(this); } // appVersion if (this.options.appVersion) { this.expressAppInstance.use((req, res, next) => { res.set('appversion', this.options.appVersion); next(); }); this.addRoute( '/appversion', new Handler('GET', async (req, res) => { res.write(this.options.appVersion); res.end(); }) ); } // set up routes in for express await this.routeObjectMap.forEach(async (routeArg) => { console.log( `"${routeArg.routeString}" maps to ${routeArg.handlerObjectMap.getArray().length} handlers` ); const expressRoute = this.expressAppInstance.route(routeArg.routeString); routeArg.handlerObjectMap.forEach(async (handler) => { console.log(` -> ${handler.httpMethod}`); switch (handler.httpMethod) { case 'GET': expressRoute.get(handler.handlerFunction); return; case 'POST': expressRoute.post(handler.handlerFunction); return; case 'PUT': expressRoute.put(handler.handlerFunction); return; case 'ALL': expressRoute.all(handler.handlerFunction); return; case 'DELETE': expressRoute.delete(handler.handlerFunction); return; default: return; } }); }); if (this.options.defaultAnswer) { this.expressAppInstance.get('/', async (request, response) => { response.send(await this.options.defaultAnswer()); }); } this.httpServer.on('connection', (connection: plugins.net.Socket) => { this.socketMap.add(connection); console.log(`added connection. now ${this.socketMap.getArray().length} sockets connected.`); const closeListener = () => { console.log('connection closed'); cleanupConnection(); }; const errorListener = () => { console.log('connection errored'); cleanupConnection(); }; const endListener = () => { console.log('connection ended'); cleanupConnection(); }; const timeoutListener = () => { console.log('connection timed out'); cleanupConnection(); }; connection.addListener('close', closeListener); connection.addListener('error', errorListener); connection.addListener('end', endListener); connection.addListener('timeout', timeoutListener); const cleanupConnection = async () => { connection.removeListener('close', closeListener); connection.removeListener('error', errorListener); connection.removeListener('end', endListener); connection.removeListener('timeout', timeoutListener); if (this.socketMap.checkForObject(connection)) { this.socketMap.remove(connection); console.log(`removed connection. ${this.socketMap.getArray().length} sockets remaining.`); await plugins.smartdelay.delayFor(0); if (connection.destroyed === false) { connection.destroy(); } } }; }); // finally listen on a port if (doListen) { this.httpServer.listen(portArg, '0.0.0.0', () => { console.log(`now listening on ${portArg}!`); this.startedDeferred.resolve(); this.serverStatus = 'running'; done.resolve(); }); } else { console.log( 'The server does not listen on a network stack and instead expects to get handed requests by other mechanics' ); } await done.promise; for (const executeAfterStartFunction of this.executeAfterStartFunctions) { await executeAfterStartFunction(); } } public getHttpServer() { return this.httpServer; } public getExpressAppInstance() { return this.expressAppInstance; } public async stop() { const done = plugins.smartpromise.defer(); if (this.httpServer) { this.httpServer.close(async () => { this.serverStatus = 'stopped'; done.resolve(); }); await this.socketMap.forEach(async (socket) => { socket.destroy(); }); } else { throw new Error('There is no Server to be stopped. Have you started it?'); } return await done.promise; } /** * allows handling requests and responses that come from other * @param req * @param res */ public async handleReqRes(req: plugins.express.Request, res: plugins.express.Response) { this.expressAppInstance(req, res); } }