server/services/webserver_service/service.js

'use strict';

/* eslint-disable security/detect-object-injection */
/* eslint-disable security/detect-non-literal-require */

/**
 * Module dependencies, required for ALL Twyr' modules
 * @ignore
 */

/**
 * Module dependencies, required for this module
 * @ignore
 */
const TwyrBaseService = require('twyr-base-service').TwyrBaseService;

const TwyrBaseError = require('twyr-base-error').TwyrBaseError;
const TwyrSrvcError = require('twyr-service-error').TwyrServiceError;

/**
 * @class   WebserverService
 * @extends {TwyrBaseService}
 * @classdesc The Twyr Web Application Server Webserver Service - based on Koa.
 *
 * @description
 * Allows the rest of the Twyr Modules to expose REST API.
 *
 */
class WebserverService extends TwyrBaseService {
	// #region Constructor
	constructor(parent, loader) {
		super(parent, loader);
	}
	// #endregion

	// #region startup/teardown code
	/**
	 * @async
	 * @function
	 * @override
	 * @instance
	 * @memberof WebserverService
	 * @name     _setup
	 *
	 * @returns  {null} Nothing.
	 *
	 * @summary  Sets up the Web Server using Koa.
	 */
	async _setup() {
		try {
			if(this.$server) return null;
			await super._setup();

			this.$proxies = {};
			this.$serveFavicons = {};
			this.$serveStatics = {};

			/**
			 * Step 1: Setup Koa Application
			 * @ignore
			 */

			// Step 1.1: Instantiate Koa & do basic setup
			const Koa = require('koa');
			const path = require('path');
			const promises = require('bluebird');

			this.$koa = new Koa();
			this.$koa.keys = this.$config.session.keys;
			this.$koa.proxy = ((twyrEnv !== 'development') && (twyrEnv !== 'test'));
			this.$koa.subdomainOffset = this.$config.domain.split('.').length;

			// Step 1.2: Generate or Use a unique id for this request
			const requestId = require('koa-requestid');
			this.$koa.use(requestId({
				'expose': 'X-Request-Id',
				'header': 'X-Request-Id',
				'query': 'requestId'
			}));

			this.$koa.use(async (ctxt, next) => {
				ctxt.request.headers['x-request-id'] = ctxt.state.id;
				ctxt.set('x-powered-by', this.$config.poweredBy);

				await next();
			});

			// Step 1.3: Handle the bloody errors...
			this.$koa.use(async (ctxt, next) => {
				try {
					await next();
				}
				catch(err) {
					let error = err;
					if(error && !(error instanceof TwyrBaseError)) { // eslint-disable-line curly
						error = new TwyrBaseError(`Web Request Error: `, err);
					}

					ctxt.type = 'application/json; charset=utf-8';
					ctxt.status = 422;
					ctxt.body = error.toJSON();

					// ctxt.app.emit('error', error, ctxt);
				}
				finally {
					if(this.$config.protocol === 'http2')
						delete ctxt.status;
				}
			});

			// Step 1.4: Before proceeding further, check if this node will process this...
			this.$koa.use(this._setTenant.bind(this));
			this.$koa.use(this._handleOrProxytoCluster.bind(this));

			// Step 1.5: Security middlewares, Rate Limiters, etc. - first
			// But only in production - its a pain having to deal with these in development
			if((twyrEnv !== 'development') && (twyrEnv !== 'test')) {
				// Blacklisted IP? No chance...
				// const honeypot = require('nodejs-projecthoneypot');
				// honeypot.setApiKey(this.$config.honeyPot.apiKey);

				// this.$koa.use(async (ctxt, next) => {
				// 	try {
				// 		const honeyPayload = await honeypot.checkIP(ctxt.ip);
				// 		if(!honeyPayload.found) {
				// 			await next();
				// 			return;
				// 		}

				// 		throw new URIError(`Blacklisted Request IP Address: ${ctxt.ip}`);
				// 	}
				// 	catch(err) {
				// 		let error = err;

				// 		// eslint-disable-next-line curly
				// 		if(error && !(error instanceof TwyrSrvcError)) {
				// 			error = new TwyrSrvcError(`${this.name}::_checkHoneypot`, error);
				// 		}

				// 		throw error;
				// 	}
				// });

				// Not blacklisted but not whitelisted here? Forget it
				const koaCors = require('@koa/cors');
				this.$koa.use(koaCors({
					'credentials': true,
					'keepHeadersOnError': true,

					'origin': (ctx) => {
						return (ctx.hostname.indexOf(this.$config.domain) >= 0);
					}
				}));

				// Ok... whitelisted, but exceeding request quotas? Stop right now!
				const ratelimiter = require('koa-ratelimit');
				this.$koa.use(ratelimiter({
					'db': this.$dependencies.CacheService
				}));

				// All fine, but the server is overloaded? You gotta wait, dude!
				const overloadProtection = require('overload-protection')('koa');
				this.$koa.use(overloadProtection);

				// TODO: Enable proper Content-Security-Policy once we're done with figuring out where we get stuff from
				// And add a CSP report uri, as well
				const koaHelmet = require('koa-helmet');
				this.$koa.use(koaHelmet({
					'hidePoweredBy': false,
					'hpkp': false,
					'hsts': false
				}));
			}

			// Step 1.6: Add in the request modifying middlewares
			const acceptOverride = require('koa-accept-override');
			this.$koa.use(acceptOverride());

			const methodOverride = require('koa-methodoverride');
			this.$koa.use(methodOverride());

			const device = require('device');
			this.$koa.use(async (ctxt, next) => {
				ctxt.state.device = device(ctxt.req.headers['user-agent'] || '');
				await next();
			});

			// Step 1.7: Session
			const koaSession = require('koa-session');
			const KoaSessionStore = require('koa-redis');

			this.$config.session.config.store = new KoaSessionStore({
				'client': this.$dependencies.CacheService
			});

			this.$config.session.config.genid = function() {
				const uuid = require('uuid/v4');
				return `twyr!webapp!server!${uuid()}`;
			};

			this.$koa.use(koaSession(this.$config.session.config, this.$koa));

			// Step 1.8: The body parsers...
			const koaFormidable = require('koa2-formidable');
			this.$koa.use(koaFormidable());

			const koaBodyParser = require('koa-bodyparser');
			this.$koa.use(koaBodyParser({
				'formLimit': '10mb',
				'jsonLimit': '10mb',
				'textLimit': '10mb',
				'extendTypes': {
					'json': ['application/x-javascript'],
					'form': ['multipart/form-data']
				}
			}));

			// Step 1.9: Passport based authentication
			this.$koa.use(this.$dependencies.AuthService.initialize());
			this.$koa.use(this.$dependencies.AuthService.session());

			// Step 1.10: Compressor for large response payloads
			const compressor = require('koa-compress');
			this.$koa.use(compressor({
				'threshold': 4096
			}));

			// Step 1.11: Twyr Auditing & Logger for auditing...
			this.$koa.use(this._auditLog.bind(this));

			// Step 1.12: Static Assets / Favicon / etc.
			const koaFavicon = require('koa-favicon');
			const koaStatic = require('koa-static');

			this.$koa.use(this._serveTenantFavicon.bind(this));
			this.$koa.use(koaFavicon(path.join(path.dirname(path.dirname(require.main.filename)), 'static_assets/favicon.ico')));

			this.$koa.use(this._serveTenantStaticAssets.bind(this));
			this.$koa.use(koaStatic(path.join(path.dirname(path.dirname(require.main.filename)), 'static_assets')));

			// Step 1.13: Add in the router
			const Router = require('koa-router');
			this.$router = new Router();

			this.$koa.use(this.$router.routes());
			this.$koa.use(this.$router.allowedMethods());

			/**
			 * Step 2: Setup node.js Web Server
			 * @ignore
			 */

			// Step 2.1: Create the Server
			this.$config.protocol = this.$config.protocol || 'http';
			const protocol = require(this.$config.protocol || 'http');

			if(this.$config.protocol === 'http') { // eslint-disable-line curly
				this.$server = protocol.createServer(this.$koa.callback());
			}

			const filesystem = promises.promisifyAll(require('fs'));
			if((this.$config.protocol === 'https') || this.$config.protocol === 'spdy') {
				const secureKey = await filesystem.readFileAsync(path.isAbsolute(this.$config.secureProtocols[this.$config.protocol].key) ? this.$config.secureProtocols[this.$config.protocol].key : path.join(__dirname, this.$config.secureProtocols[this.$config.protocol].key));
				const secureCert = await filesystem.readFileAsync(path.isAbsolute(this.$config.secureProtocols[this.$config.protocol].cert) ? this.$config.secureProtocols[this.$config.protocol].cert : path.join(__dirname, this.$config.secureProtocols[this.$config.protocol].cert));

				this.$config.secureProtocols[this.$config.protocol].key = secureKey;
				this.$config.secureProtocols[this.$config.protocol].cert = secureCert;

				this.$server = protocol.createServer(this.$config.secureProtocols[this.$config.protocol], this.$koa.callback());
			}

			if(this.$config.protocol === 'http2') {
				const secureKey = await filesystem.readFileAsync(path.isAbsolute(this.$config.secureProtocols[this.$config.protocol].key) ? this.$config.secureProtocols[this.$config.protocol].key : path.join(__dirname, this.$config.secureProtocols[this.$config.protocol].key));
				const secureCert = await filesystem.readFileAsync(path.isAbsolute(this.$config.secureProtocols[this.$config.protocol].cert) ? this.$config.secureProtocols[this.$config.protocol].cert : path.join(__dirname, this.$config.secureProtocols[this.$config.protocol].cert));

				this.$config.secureProtocols[this.$config.protocol].key = secureKey;
				this.$config.secureProtocols[this.$config.protocol].cert = secureCert;

				this.$server = protocol.createSecureServer(this.$config.secureProtocols[this.$config.protocol], this.$koa.callback());
			}

			// Step 2.2: Add utility to force-stop server
			const serverDestroy = require('server-destroy');
			serverDestroy(this.$server);

			// Step 2.3: Start listening to events
			this.$server.on('connection', this._serverConnection.bind(this));

			this.$koa.on('error', this._handleKoaError.bind(this));
			this.$server.on('error', this._serverError.bind(this));

			// Step 2.4: Setup the server to listen to requests forwarded via Ringpop, just in case
			this.$dependencies.RingpopService.on('request', this._processRequestFromAnotherNode.bind(this));

			// Finally, Start listening...
			this.$server = promises.promisifyAll(this.$server);
			this.$parent.once('server-online', this._listenAndPrintNetworkInterfaces.bind(this));

			return null;
		}
		catch(err) {
			throw new TwyrSrvcError(`${this.name}::_setup error`, err);
		}
	}

	/**
	 * @async
	 * @function
	 * @override
	 * @instance
	 * @memberof WebserverService
	 * @name     _teardown
	 *
	 * @returns  {undefined} Nothing.
	 *
	 * @summary  Destroys the Koa Web Server.
	 */
	async _teardown() {
		try {
			this.$dependencies.RingpopService.off('request', this._processRequestFromAnotherNode.bind(this));

			if(!this.$server) return null;
			if(this.$server.listening) { // eslint-disable-line curly
				await this.$server.destroyAsync();
			}

			this.$server.off('connection', this._serverConnection.bind(this));

			this.$server.off('error', this._serverError.bind(this));
			this.$koa.off('error', this._handleKoaError.bind(this));

			this.$router.stack.length = 0;

			delete this.$router;
			delete this.$koa;
			delete this.$server;

			await super._teardown();
			return null;
		}
		catch(err) {
			throw new TwyrSrvcError(`${this.name}::_teardown error`, err);
		}
	}
	// #endregion

	// #region Configuration Change Handlers
	/**
	 * @async
	 * @function
	 * @override
	 * @instance
	 * @memberof WebserverService
	 * @name     _reconfigure
	 *
	 * @param    {Object} newConfig - The changed configuration.
	 *
	 * @returns  {undefined} Nothing.
	 *
	 * @summary  Changes the configuration of this module, and informs everyone interested.
	 */
	async _reconfigure(newConfig) {
		this.$parent.emit('server-offline');
		await super._reconfigure(newConfig);
		this.$parent.emit('server-online');

		return null;
	}
	// #endregion

	// #region Koa Middlewares
	/**
	 * @async
	 * @function
	 * @instance
	 * @memberof WebserverService
	 * @name     _setTenant
	 *
	 * @param    {Object} ctxt - Koa context.
	 * @param    {callback} next - Callback to pass the request on to the next route in the chain.
	 *
	 * @returns  {undefined} Nothing.
	 *
	 * @summary  Sets up the tenant context on each request so Ringpop knows which node in the cluster to route it to.
	 */
	async _setTenant(ctxt, next) {
		try {
			const cacheSrvc = this.$dependencies.CacheService,
				dbSrvc = this.$dependencies.DatabaseService.knex;

			let tenantSubDomain = (ctxt.subdomains.length === 1) ? ctxt.subdomains[0] : 'www';

			if(ctxt.subdomains.length > 1) {
				ctxt.subdomains.reverse();
				tenantSubDomain = ctxt.subdomains.join('.');
				ctxt.subdomains.reverse();
			}

			if(this.$config.subdomainMappings && this.$config.subdomainMappings[tenantSubDomain])
				tenantSubDomain = this.$config.subdomainMappings[tenantSubDomain];

			let tenant = null;
			if(ctxt.get['tenant']) { // eslint-disable-line curly
				tenant = JSON.parse(ctxt.get['tenant']);
			}

			if(!tenant) {
				tenant = await cacheSrvc.getAsync(`twyr!webapp!tenant!${tenantSubDomain}`);
				if(tenant) tenant = JSON.parse(tenant);
			}

			if(!tenant) {
				let parentModule = this.$parent;
				while(parentModule.$parent) parentModule = parentModule.$parent;

				const parentModuleId = await this.$dependencies.ConfigurationService.getModuleID(parentModule);

				tenant = await dbSrvc.raw('SELECT tenant_id, name, sub_domain FROM tenants WHERE sub_domain = ?', [tenantSubDomain]);
				if(!tenant.rows.length) {
					let redirectDomain = `${this.$config.protocol}://www.${this.$config.domain}`;
					if(this.$config.externalPort) redirectDomain = `${redirectDomain}:${this.$config.externalPort}`;

					ctxt.redirect(redirectDomain);
					return;
				}

				tenant = tenant.rows.shift();

				let template = await dbSrvc.raw(`SELECT * FROM fn_get_tenant_server_template(?, ?)`, [tenant.tenant_id, parentModuleId]);
				template = template.rows.shift();

				tenant['template'] = template;

				const tenantFeatures = await dbSrvc.raw(`SELECT * FROM fn_get_module_descendants(?) WHERE type = 'feature' AND module_id IN (SELECT module_id FROM tenants_features WHERE tenant_id = ?)`, [parentModuleId, tenant.tenant_id]);
				tenant['features'] = this._setupTenantFeatureTree(tenantFeatures.rows, parentModuleId);

				const cacheMulti = cacheSrvc.multi();
				cacheMulti.setAsync(`twyr!webapp!tenant!${tenantSubDomain}`, JSON.stringify(tenant));
				cacheMulti.expireAsync(`twyr!webapp!tenant!${tenantSubDomain}`, ((twyrEnv === 'development') ? 300 : 86400));

				await cacheMulti.execAsync();
			}

			ctxt.state.tenant = tenant;
			ctxt.request.headers['tenant'] = JSON.stringify(tenant);

			await next();
		}
		catch(err) {
			let error = err;

			// eslint-disable-next-line curly
			if(error && !(error instanceof TwyrSrvcError)) {
				error = new TwyrSrvcError(`${this.name}::_setTenant`, error);
			}

			throw error;
		}
	}

	/**
	 * @async
	 * @function
	 * @instance
	 * @memberof WebserverService
	 * @name     _auditLog
	 *
	 * @param    {Object} ctxt - Koa context.
	 * @param    {callback} next - Callback to pass the request on to the next route in the chain.
	 *
	 * @returns  {undefined} Nothing.
	 *
	 * @summary  Pushes the data from the request/response cycle to the Audit Service for publication.
	 */
	async _auditLog(ctxt, next) {
		try {
			const convertHRTime = require('convert-hrtime');
			const moment = require('moment');
			const statusCodes = require('http').STATUS_CODES;

			const reqHeaders = {};
			Object.keys(ctxt.request.headers).forEach((reqHeader) => {
				try {
					reqHeaders[reqHeader] = JSON.parse(JSON.stringify(ctxt.request.headers[reqHeader]));
				}
				catch(err) {
					// Do Nothing
				}
			});

			const logMsgMeta = {
				'id': ctxt.state.id,
				'start-time': moment().valueOf(),
				'duration': 0,

				'user': {
					'user_id': ctxt.state.user ? ctxt.state.user.user_id : 'ffffffff-ffff-4fff-ffff-ffffffffffff',
					'name': ctxt.state.user ? `${ctxt.state.user.first_name} ${ctxt.state.user.last_name}` : 'Public'
				},

				'tenant': {
					'tenant_id': ctxt.state.tenant ? ctxt.state.tenant.tenant_id : '00000000-0000-4000-0000-000000000000',
					'sub-domain': ctxt.state.tenant ? ctxt.state.tenant.sub_domain : '???',
					'name': ctxt.state.tenant ? ctxt.state.tenant.name : 'Unknown'
				},

				'request-meta': {
					'headers': reqHeaders || {},
					'method': ctxt.request.method,
					'url': ctxt.request.url,
					'ip': ctxt.request.ip,
					'ips': JSON.parse(JSON.stringify(ctxt.request.ips || []))
				},

				'response-meta': {
				},

				'query': JSON.parse(JSON.stringify(ctxt.query || {})),
				'params': JSON.parse(JSON.stringify(ctxt.params || {})),
				'body': JSON.parse(JSON.stringify(ctxt.request.body || {})),
				'payload': null,
				'error': null
			};

			const startTime = process.hrtime();
			if(next) await next();
			const duration = process.hrtime(startTime);

			logMsgMeta.duration = convertHRTime(duration).milliseconds;

			logMsgMeta['response-meta']['headers'] = JSON.parse(JSON.stringify(ctxt.response.headers));
			logMsgMeta['response-meta']['status'] = {
				'code': ctxt.status,
				'message': statusCodes[ctxt.status]
			};

			logMsgMeta['payload'] = (Buffer.isBuffer(ctxt.body)) ? 'BUFFER' : JSON.parse(JSON.stringify(ctxt.body || {}));
			logMsgMeta['error'] = ctxt.state.error || (ctxt.status >= 400);

			logMsgMeta['request-meta']['headers']['tenant'] = undefined;

			if(ctxt.request.url.indexOf('websockets') >= 0) {
				ctxt.status = 200;
				ctxt.type = 'application/json; charset=utf-8';
				ctxt.body = logMsgMeta;

				return;
			}

			await this.$dependencies.AuditService.publish(logMsgMeta);
		}
		catch(err) {
			let error = err;

			// eslint-disable-next-line curly
			if(error && !(error instanceof TwyrSrvcError)) {
				error = new TwyrSrvcError(`${this.name}::_auditLog`, error);
			}

			throw error;
		}
	}

	/**
	 * @async
	 * @function
	 * @instance
	 * @memberof WebserverService
	 * @name     _handleOrProxytoCluster
	 *
	 * @param    {Object} ctxt - Koa context.
	 * @param    {callback} next - Callback to pass the request on to the next route in the chain.
	 *
	 * @returns  {undefined} Nothing.
	 *
	 * @summary  Call Ringpop to decide whether to handle the request, or to forward it someplace else.
	 */
	async _handleOrProxytoCluster(ctxt, next) {
		await next();
		return;

		// try {
		// 	const ringpop = this.$dependencies.RingpopService;

		// 	const hostPort = [];
		// 	hostPort.push(ringpop.lookup(ctxt.state.tenant.tenant_id).split(':').shift());
		// 	hostPort.push(this.$config.internalPort || 9100);
		// 	// hostPort.push(this.$config.internalPort === 9100 ? 9101 : 9100);

		// 	const dest = `${this.$config.protocol}://${hostPort.join(':')}${ctxt.path}`;

		// 	if(ringpop.lookup(ctxt.state.tenant.tenant_id) === ringpop.whoami()) {
		// 		delete this.$proxies[dest];

		// 		await next();
		// 		return;
		// 	}

		// 	delete this.$serveFavicons[ctxt.state.tenant.sub_domain];
		// 	delete this.$serveStatics[ctxt.state.tenant.sub_domain];

		// 	if(!this.$proxies[dest]) {
		// 		const proxy = require('koa-better-http-proxy');
		// 		this.$proxies[dest] = proxy(dest, {
		// 			'preserveReqSession': true,
		// 			'preserveHostHdr': true
		// 		});
		// 	}

		// 	await this.$proxies[dest](ctxt, next);
		// }
		// catch(err) {
		// 	let error = err;

		// 	// eslint-disable-next-line curly
		// 	if(error && !(error instanceof TwyrSrvcError)) {
		// 		error = new TwyrSrvcError(`${this.name}::_handleOrProxytoCluster`, error);
		// 	}

		// 	throw error;
		// }
	}

	/**
	 * @async
	 * @function
	 * @instance
	 * @memberof WebserverService
	 * @name     _serveTenantFavicon
	 *
	 * @param    {Object} ctxt - Koa context.
	 * @param    {callback} next - Callback to pass the request on to the next route in the chain.
	 *
	 * @returns  {undefined} Nothing.
	 *
	 * @summary  Return the favicon set by the tenant.
	 */
	async _serveTenantFavicon(ctxt, next) {
		const path = require('path');
		const serveFavicon = require('koa-favicon');

		try {
			const tenantFaviconPath = path.join(path.dirname(path.dirname(require.main.filename)), 'static_assets', ctxt.state.tenant['sub_domain'], 'favicon.ico');
			if(!this.$serveFavicons[ctxt.state.tenant.sub_domain]) { // eslint-disable-line curly
				this.$serveFavicons[ctxt.state.tenant.sub_domain] = serveFavicon(tenantFaviconPath);
			}
		}
		catch(err) {
			await next();
			return;
		}

		await this.$serveFavicons[ctxt.state.tenant.sub_domain](ctxt, next);
	}

	/**
	 * @async
	 * @function
	 * @instance
	 * @memberof WebserverService
	 * @name     _serveTenantStaticAssets
	 *
	 * @param    {Object} ctxt - Koa context.
	 * @param    {callback} next - Callback to pass the request on to the next route in the chain.
	 *
	 * @returns  {undefined} Nothing.
	 *
	 * @summary  Serve up the static assets from the current tenants folder.
	 */
	async _serveTenantStaticAssets(ctxt, next) {
		const path = require('path');
		const serveStatic = require('koa-static');

		try {
			const tenantStaticAssetPath = path.join(path.dirname(path.dirname(require.main.filename)), 'static_assets', ctxt.state.tenant['sub_domain']);
			if(!this.$serveStatics[ctxt.state.tenant.sub_domain]) { // eslint-disable-line curly
				this.$serveStatics[ctxt.state.tenant.sub_domain] = serveStatic(tenantStaticAssetPath);
			}
		}
		catch(err) {
			await next();
			return;
		}

		await this.$serveStatics[ctxt.state.tenant.sub_domain](ctxt, next);
	}

	// #endregion

	// #region Miscellaneous
	/**
	 * @function
	 * @instance
	 * @memberof WebserverService
	 * @name     _processRequestFromAnotherNode
	 *
	 * @param    {Object} request - Request coming in from the outside world.
	 * @param    {Object} response - Response going out to the outside world.
	 *
	 * @returns  {undefined} Nothing.
	 *
	 * @summary  Returns a function that can handle the request coming in from another Ringpop node.
	 */
	_processRequestFromAnotherNode(request, response) {
		response.end();
	}

	/**
	 * @function
	 * @instance
	 * @memberof WebserverService
	 * @name     _setupTenantFeatureTree
	 *
	 * @param    {Array} tenantFeatures - List of features this tenant has access to in the database.
	 * @param    {string} parentModuleId - Parent Feature to be considered.
	 *
	 * @returns  {Object} Tree structure.
	 *
	 * @summary  Returns a sree structure of the features / sub-features that the tenant has access to.
	 */
	_setupTenantFeatureTree(tenantFeatures, parentModuleId) {
		const thisParentFeatures = {};
		tenantFeatures.forEach((tenantFeature) => {
			if(tenantFeature['parent_module_id'] !== parentModuleId)
				return;

			thisParentFeatures[tenantFeature.name] = this._setupTenantFeatureTree(tenantFeatures, tenantFeature['module_id']);
		});

		return thisParentFeatures;
	}
	// #endregion

	// #region Private Methods
	_serverConnection(socket) {
		socket.setTimeout(this.$config.connectionTimeout * 1000);
	}

	_serverError(err) {
		let error = err;

		// eslint-disable-next-line curly
		if(error && !(error instanceof TwyrSrvcError)) {
			error = new TwyrSrvcError(`${this.name}::_serverError`, error);
		}

		this.$dependencies.LoggerService.error(`${this.name}::_serverError\n\n${error.toString()}`);
	}

	_handleKoaError(err, ctxt) {
		let error = err;

		// eslint-disable-next-line curly
		if(error && !(error instanceof TwyrSrvcError)) {
			error = new TwyrSrvcError(`${this.name}::_handleKoaError`, error);
		}

		ctxt.state.error = error;
		this._auditLog(ctxt);
	}

	async _listenAndPrintNetworkInterfaces() {
		await snooze(1000);
		await this.$server.listenAsync(this.$config.internalPort || 9090);

		if(twyrEnv !== 'development' && twyrEnv !== 'test')
			return;

		const forPrint = [],
			networkInterfaces = require('os').networkInterfaces();

		Object.keys(networkInterfaces).forEach((networkInterfaceName) => {
			const networkInterfaceAddresses = networkInterfaces[networkInterfaceName];

			for(const address of networkInterfaceAddresses)
				forPrint.push({
					'Interface': networkInterfaceName,
					'Protocol': address.family,
					'Address': address.address,
					'Port': this.$config.internalPort || 9100
				});
		});

		console.table(forPrint);
	}
	// #endregion

	// #region Properties
	/**
	 * @override
	 */
	get Interface() {
		return {
			'App': this.$koa,
			'Router': this.$router,
			'Server': this.$server
		};
	}

	/**
	 * @override
	 */
	get dependencies() {
		return [
			'AuditService',
			'AuthService',
			'CacheService',
			'ConfigurationService',
			'DatabaseService',
			'LocalizationService',
			'LoggerService',
			'RingpopService'
		].concat(super.dependencies);
	}

	/**
	 * @override
	 */
	get basePath() {
		return __dirname;
	}
	// #endregion
}

exports.service = WebserverService;