Reference Source

js/servers/express_server.js

// NPM IMPORTS
import assert from 'assert'
import _ from 'lodash'
import express from 'express'
import http from 'http'
import socketio from 'socket.io'
import compression from 'compression'
// import helmet from 'helmet'
import favicon from 'express-favicon'
import path from 'path'

// COMMON IMPORTS
import T from 'devapt-core-common/dist/js/utils/types'

// SERVER IMPORTS
import runtime from '../base/runtime'
import RoutableServer from './routable_server'
import MetricsMiddleware from '../metrics/http/metrics_http_collector'


const context = 'server/servers/express_server'



/**
 * @file Express server class.
 * @author Luc BORIES
 * @license Apache-2.0
 */
export default class ExpressServer extends RoutableServer
{
	/**
	 * Create Express server instance.
	 * @extends RoutableServer
	 * 
	 * @param {string} arg_name - server name.
	 * @param {object} arg_settings - plugin settings map.
	 * @param {string} arg_log_context - trace context string.
	 * 
	 * @returns {nothing}
	 */
	constructor(arg_name, arg_settings, arg_log_context=context)
	{
		super(arg_name, 'ExpressServer', arg_settings, arg_log_context)
		
		this.is_express_server = true
	}
	

	
	/**
	 * Build private server instance.
	 * 
	 * @returns {nothing}
	 */
	build_server()
	{
		this.enter_group('build_server')
		
		// DEBUG
		// debugger
		
		assert( this.server_protocole == 'http' || this.server_protocole == 'https', context + ':bad protocole for express [' + this.server_protocole + ']')
		
		// CREATE SERVER
		this.info('build_server:create Express server')
		this.server = express()
		
		// USE COMPRESSED RESPONSE WITH GZIP
		this.info('build_server:use compression')
		this.server.use(compression())
		
		// USE SECURITY MIDDLEWARE (https://www.npmjs.com/package/helmet)
		this.info('build_server:use security')
		// this.server.use(helmet)
		this.server.disable('x-powered-by')
		
		
		// USE METRICS MIDDLEWARE
		this.info('build_server:use metrics')
		this.server.use( MetricsMiddleware.create_middleware(this) )
		
		
		// USE FAVICON MIDDLEWARE
		this.info('build_server:use favicon')
		const favicon_path = runtime.context.get_absolute_path('../../../public/favico.png')
		// console.log(favicon_path, 'favicon_path')
		this.server.use( favicon(favicon_path) )
		
		
		// BUILD SOCKETIO
		this.info('build_server:use socketio')
		const use_socketio = this.get_setting('use_socketio', false)

		// DEBUG
		this.debug('build_server:use socket io?', use_socketio)
		console.log(context + ':build_server:use socket io?', use_socketio)
		
		if (use_socketio)
		{
			// DEBUG
			this.debug('build_server:creating Express socket io')
			console.log(context + ':build_server:creating Express socket io')
			
			this.server_http = http.Server(this.server)
			this.serverio = socketio(this.server_http)
			
			runtime.add_socketio(this.get_name(), this.serverio)
		}

		
		// USE ALL MIDDLEWARES WITHOUT SECURITY
		this.info('build_server:use all middleware without security')
		this.services_without_security.forEach(
			(arg_record) => {
				this.info('build_server:activate service=' + arg_record.svc.get_name())
				arg_record.svc.activate_on_server(arg_record.app, this, arg_record.cfg)
			}
		)


		// USE AUTHENTICATION MIDDLEWARES
		this.info('build_server:apply authentication middlewares')
		this.authentication.apply_middlewares(this)
		
		
		// TODO: USE AUTHORIZATION MIDDLEWARE
		// this.server.use( this.authorization.create_middleware() )
		

		// USE ALL MIDDLEWARES WITH SECURITY
		this.info('build_server:use all middleware with security')
		this.services_with_security.forEach(
			(arg_record) => {
				arg_record.svc.activate_on_server(arg_record.app, this, arg_record.cfg)
			}
		)

		
		// DEFAULT VIEW ENGINE
		// this.server.use( express.bodyParser() )
		// this.server.set('views', runtime.context.get_absolute_path('jade'))
		this.server.set('views', path.join(runtime.context.get_base_dir(), '../../dist/jade') )
		this.server.set('view engine', 'jade')
		
		
		this.leave_group('build_server')
	}
	
	finaly()
	{
		// USE FILE NOT FOUND MIDDLEWARE
		this.server.use(
			function(req, res/*, next*/)
			{
				console.log('EXPRESS: FILE NOT FOUND', req.url)
				
				res.status(404)

				// SEND HTML RESPONSE
				if (req.accepts('html'))
				{
					res.render('404', { url: req.url })
					return
				}

				// SEND JSON RESPONSE
				if (req.accepts('json'))
				{
					res.send({ error: 'Not found' })
					return
				}

				// SEND PLAIN TEXT RESPONSE
				res.type('txt').send('URL Not found (no routes)')
			}
		)
		
		
		// USE BAD REQUEST MIDDLEWARE
		this.server.use(
			function(err, req, res, next)
			{
				// !!! RES COULD BE A http.ServerResponse AND NOT A Express.Response INSTANCE
				if (req.xhr)
				{
					res.status(500).send( { error: 'Something failed!' } )
				}
				else
				{
					next(err)
				}
			}
		)
		
		
		// USE ERROR MIDDLEWARE
		this.server.use(
			function(err, req, res/*, next*/)
			{
				console.log(req.url, 'request.url')
				console.error(err.stack)
				res.status(500)
				res.render('error', { error: err } )
			}
		)
		
	}
	
	
	
	/**
	 * Get server middleware for static route.
	 * 
     * @param {object} arg_cfg_route - plain object route configuration.
	 * 
	 * @returns {middleware} - middleware function as f(req, res, next)
	 */
	get_middleware_for_static_route(arg_cfg_route)
	{
		const self = this

		// SEARCH ASSETS DIRECTORY
		let dir_path = undefined
		if ( path.isAbsolute(arg_cfg_route.directory) )
		{
			dir_path = arg_cfg_route.directory
		}
		if ( ! dir_path && T.isNotEmptyString(arg_cfg_route.pkg_base_dir) )
		{
			dir_path = runtime.context.get_absolute_path(arg_cfg_route.pkg_base_dir, arg_cfg_route.directory)
		}
		if ( ! dir_path && T.isNotEmptyString(arg_cfg_route.app_base_dir) )
		{
			dir_path = runtime.context.get_absolute_path(arg_cfg_route.app_base_dir, '../public', arg_cfg_route.directory)
		}
		if ( ! dir_path )
		{
			dir_path = runtime.context.get_absolute_public_path(arg_cfg_route.directory)
		}

		// DEBUG
		// console.log(context + ':get_middleware_for_static_route:express static route', arg_cfg_route)
		// console.log(context + ':ROUTE FOR ASSETS IN DIRECTORY MODE => dir_path=', dir_path)

		// STATIC OPTIONS
		const one_day = 86400000
		const static_cfg = {
			maxAge:one_day,
			redirect:false,
			fallthrough:true, // TRUE to run next middlewares to search not found fil on another route or to call call an error handler.
			setHeaders:undefined, // function
			extensions:T.isNotEmptyArray(arg_cfg_route.extensions) ? arg_cfg_route.extensions : false,
			index:arg_cfg_route.default
		}

		this.debug('get_middleware_for_static_route:route [' + arg_cfg_route.directory + ']')
		this.debug('get_middleware_for_static_route:dir_path [' + dir_path + ']')
		this.debug('get_middleware_for_static_route:route cfg [' + JSON.stringify(arg_cfg_route) + ']')

		return (req, res, next)=>{
			const asset_path = req._parsedUrl.pathname

			// DEBUG
			self.debug('get_middleware_for_static_route:mw callback:asset_path=[' + asset_path + '] directory=[' + arg_cfg_route.directory + ']')
			// console.log(context + ':get_middleware_for_static_route:express static middleware', arg_cfg_route.directory, req._parsedUrl, dir_path, asset_path)
			// debugger
			
			// TEST REQUIRED PREFIXES
			if ( T.isNotEmptyArray(arg_cfg_route.required_prefixes) )
			{
				let match = false
				_.forEach(arg_cfg_route.required_prefixes,
					(value)=>{
						if ( asset_path.startsWith(value) )
						{
							match = true
						}
					}
				)

				if (! match)
				{
					this.warn('get_middleware_for_static_route:not matching route prefix for ' + asset_path)
					console.warn(context + ':get_middleware_for_static_route:not matching route prefix for ' + asset_path)

					return next()
				}
			}
			
			// TEST REQUIRED SUFFIXES
			if ( T.isNotEmptyArray(arg_cfg_route.required_suffixes) )
			{
				let match = false
				_.forEach(arg_cfg_route.required_suffixes,
					(value)=>{
						if ( asset_path.endsWith(value) )
						{
							match = true
						}
					}
				)

				if (! match)
				{
					this.warn('get_middleware_for_static_route:not matching route suffix for ' + asset_path)
					console.warn(context + ':get_middleware_for_static_route:not matching route suffix for ' + asset_path)

					return next()
				}
			}

			req.url = req.url == '/' ? req.baseUrl : req.url
			
			return express.static(dir_path, static_cfg)(req, res, next)
		}
	}
	
	
	
	/**
	 * Get server middleware for directory route.
	 * 
     * @param {object}   arg_cfg_route - plain object route configuration.
	 * @param {function} arg_callback - route handler callback.
	 * 
	 * @returns {boolean} - success or failure.
	 */
	add_get_route(arg_cfg_route, arg_callback)
	{
		this.enter_group('add_get_route')

		const route = arg_cfg_route.route_regexp ? arg_cfg_route.route_regexp : arg_cfg_route.full_route
		this.debug('add_get_route:route [' + route + ']')

		assert( T.isObject(arg_cfg_route),  this.get_context() + ':add_get_route:bad route config object')
		assert( T.isFunction(arg_callback), this.get_context() + ':add_get_route:bad callback function for route [' + route + ']')
		assert( T.isString(route) || T.isRegExp(route), this.get_context() + ':add_get_route:bad route string|RegExp for config [' + JSON.stringify(arg_cfg_route) + ']')

		// CHECK EXPRESS SERVER
		if ( ! this.server || ! T.isFunction(this.server.use) )
		{
			this.leave_group('add_get_route:bad server error for route config [' + JSON.stringify(arg_cfg_route) + ']')
			return false
		}
		
		const safe_callback = (req, res, next)=>{
			try{
				console.log(this.get_context() + ':add_get_route:route cfg=' + JSON.stringify(arg_cfg_route))
				return arg_callback(req, res, next)
			}
			catch(err)
			{
				console.error(this.get_context() + ':add_get_route:error=' + err + ':route cfg=' + JSON.stringify(arg_cfg_route))
				return next(err)
			}
		}
		this.server.use(route, safe_callback)

		this.leave_group('add_get_route:[' + route + ']')
		return true
	}
}