UNPKG

@barchart/common-node-js

Version:

Common classes, utilities, and functions for building Node.js servers

1,179 lines (915 loc) 37 kB
const bodyParser = require('body-parser'), clientSessions = require('client-sessions'), express = require('express'), expressHandlebars = require('express-handlebars'), http = require('http'), https = require('https'), log4js = require('log4js'), multer = require('multer'), path = require('path'), proxy = require('express-http-proxy'), querystring = require('querystring'), socketIO = require('socket.io'); const assert = require('@barchart/common-js/lang/assert'), CommandHandler = require('@barchart/common-js/commands/CommandHandler'), Disposable = require('@barchart/common-js/lang/Disposable'), DisposableStack = require('@barchart/common-js/collections/specialized/DisposableStack'), Event = require('@barchart/common-js/messaging/Event'), is = require('@barchart/common-js/lang/is'), promise = require('@barchart/common-js/lang/promise'); const Container = require('./../endpoints/Container'), PageContainer = require('./../endpoints/html/PageContainer'), RelayContainer = require('./../endpoints/html/RelayContainer'), RestContainer = require('./../endpoints/rest/RestContainer'), ServerFactory = require('./../ServerFactory'), SocketRequestContainer = require('./../endpoints/socket/specialized/SocketRequestContainer'), SocketEmitterContainer = require('./../endpoints/socket/specialized/SocketEmitterContainer'), SocketSubscriptionContainer = require('./../endpoints/socket/specialized/SocketSubscriptionContainer'), Verb = require('./../../http/Verb'); const S3Provider = require('./../../../aws/S3Provider'); module.exports = (() => { 'use strict'; const logger = log4js.getLogger('common-node/network/server/express/ExpressServerFactory'); class ExpressServerFactory extends ServerFactory { constructor() { super(); } _build(containers, staticPaths, templatePath) { const serverContainer = new ExpressServerContainer(staticPaths, templatePath); const containerBindingStrategies = ContainerBindingStrategy.getStrategies(); return Promise.all(containers.map((container) => { const containerBindingStrategy = containerBindingStrategies.find((candidate) => { return candidate.canBind(container); }); let bindingPromise; if (containerBindingStrategy) { bindingPromise = containerBindingStrategy.bind(container, serverContainer); } else { logger.warn('Unable to find appropriate binding strategy for container'); bindingPromise = Promise.resolve(null); } return bindingPromise; })).then((ignored) => { return serverContainer.start(); }); } toString() { return '[ExpressServerFactory]'; } } class ExpressServer { constructor(port, secure, staticPaths, templatePath) { assert.argumentIsRequired(port, 'port', Number); assert.argumentIsRequired(secure, 'secure', Boolean); assert.argumentIsOptional(staticPaths, 'staticPaths', Object); assert.argumentIsOptional(templatePath, 'templatePath', String); this._port = port; this._secure = secure; this._useSessions = false; this._staticPaths = staticPaths; this._templatePath = templatePath; this._pageMap = {}; this._relayMap = {}; this._serviceMap = {}; this._socketRequestMap = {}; this._socketSubscriptionMap = {}; this._socketEmitters = [ ]; this._started = false; } getPort() { return this._port; } getIsSecure() { return this._secure; } addPage(basePath, pagePath, template, verb, command, cache, useSession, acceptFile, secureRedirect) { assert.argumentIsRequired(basePath, 'basePath', String); assert.argumentIsRequired(pagePath, 'pagePath', String); assert.argumentIsRequired(template, 'template', String); assert.argumentIsRequired(verb, 'verb', Verb, 'Verb'); assert.argumentIsRequired(command, 'command', CommandHandler, 'CommandHandler'); assert.argumentIsRequired(cache, 'cache', Boolean); assert.argumentIsRequired(useSession, 'useSession', Boolean); assert.argumentIsRequired(useSession, 'acceptFile', Boolean); assert.argumentIsRequired(secureRedirect, 'secureRedirect', Boolean); this._useSessions = this._useSessions || useSession; if (!this._pageMap.hasOwnProperty(basePath)) { this._pageMap[basePath] = { path: basePath, handlers: [] }; } const handlerData = { verb: verb, path: pagePath, template: template, handlers: buildPageHandlers(verb, basePath, pagePath, template, command, cache, useSession, acceptFile, secureRedirect) }; this._pageMap[basePath].handlers.push(handlerData); } addRelay(basePath, acceptPath, forwardHost, forwardPath, verb, headerOverrides, parameterOverrides) { assert.argumentIsRequired(basePath, 'basePath', String); assert.argumentIsRequired(acceptPath, 'acceptPath', String); assert.argumentIsRequired(forwardHost, 'forwardHost', String); assert.argumentIsRequired(forwardPath, 'forwardPath', String); assert.argumentIsRequired(verb, 'verb', Verb, 'Verb'); assert.argumentIsRequired(headerOverrides, 'headerOverrides', Object); assert.argumentIsRequired(parameterOverrides, 'parameterOverrides', Object); if (!this._relayMap.hasOwnProperty(basePath)) { this._relayMap[basePath] = { path: basePath, relays: [ ] }; } this._relayMap[basePath].relays.push({ verb: verb, acceptPath: acceptPath, forwardHost: forwardHost, forwardPath: forwardPath, handler: buildRelayHandler(basePath, acceptPath, forwardHost, forwardPath, verb, headerOverrides, parameterOverrides) }); } addService(basePath, routePath, verb, command, validationCommand) { assert.argumentIsRequired(basePath, 'basePath', String); assert.argumentIsRequired(routePath, 'routePath', String); assert.argumentIsRequired(verb, 'verb', Verb, 'Verb'); assert.argumentIsRequired(command, 'command', CommandHandler, 'CommandHandler'); assert.argumentIsRequired(validationCommand, 'validationCommand', CommandHandler, 'CommandHandler'); if (this._started) { throw new Error('Unable to add route, the server has already been started.'); } if (!this._serviceMap.hasOwnProperty(basePath)) { this._serviceMap[basePath] = { path: basePath, handlers: [] }; } const handlerData = { verb: verb, path: routePath, handler: buildRestHandler(verb, basePath, routePath, command, validationCommand) }; this._serviceMap[basePath].handlers.push(handlerData); } addChannel(path, channel, executionCommand, validationCommand) { assert.argumentIsRequired(path, 'path', String); assert.argumentIsRequired(channel, 'channel', String); assert.argumentIsRequired(executionCommand, 'executionCommand', CommandHandler, 'CommandHandler'); assert.argumentIsRequired(validationCommand, 'validationCommand', CommandHandler, 'CommandHandler'); if (this._started) { throw new Error('Unable to add request handler for socket.io channel, the server has already been started.'); } const completePath = 'request' + path + channel; if (this._socketRequestMap.hasOwnProperty(completePath)) { throw new Error('Unable to add handler for socket.io channel, another handler is already using this channel.'); } this._socketRequestMap[completePath] = { commands: { execution: executionCommand, validation: validationCommand } }; } addEmitter(path, channel, event, eventType, roomCommand) { assert.argumentIsRequired(path, 'path', String); assert.argumentIsRequired(channel, 'channel', String); assert.argumentIsRequired(event, 'event', Event, 'Event'); assert.argumentIsRequired(eventType, 'eventType', String); assert.argumentIsRequired(roomCommand, 'roomCommand', CommandHandler, 'CommandHandler'); if (this._started) { throw new Error('Unable to add emitter for socket.io channel, the server has already been started.'); } this._socketEmitters.push({ room: { base: path + channel, command: roomCommand }, event: event, eventType: eventType }); } addSubscription(path, channel, roomsCommand, responseCommand, responseEventType, validationCommand) { assert.argumentIsRequired(path, 'path', String); assert.argumentIsRequired(channel, 'channel', String); assert.argumentIsRequired(roomsCommand, 'roomsCommand', CommandHandler, 'CommandHandler'); assert.argumentIsRequired(responseCommand, 'responseCommand', CommandHandler, 'CommandHandler'); assert.argumentIsRequired(responseEventType, 'responseEventType', String); assert.argumentIsRequired(validationCommand, 'validationCommand', CommandHandler, 'CommandHandler'); if (this._started) { throw new Error('Unable to add subscription handler for socket.io channel, the server has already been started.'); } const completePath = 'subscribe' + path + channel; if (this._socketSubscriptionMap.hasOwnProperty(completePath)) { throw new Error('Unable to add subscription handler for socket.io channel, another handler is already using this channel.'); } const subscriptionInfo = { commands: { rooms: roomsCommand, response: responseCommand, validation: validationCommand }, room: { base: path + channel }, response: { eventType: responseEventType } }; this._socketSubscriptionMap[completePath] = subscriptionInfo; } start() { if (this._started) { throw new Error('Unable to start server, the has already been started.'); } this._started = true; let startPromise = Promise.resolve(); const startStack = new DisposableStack(); const secure = this.getIsSecure(); const port = this.getPort(); const app = new express(); app.use(bodyParser.urlencoded({extended: true, limit: '1mb'})); app.use(bodyParser.json({limit: '1mb'})); app.use((req, res, next) => { logger.debug('Applying HTTP headers for ' + req.originalUrl); res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Headers', 'Accept,Access-Control-Allow-Headers,Access-Control-Request-Method,Access-Control-Request-Headers,Access-Control-Allow-Origin,Content-Type,Authorization,Origin,X-Requested-With'); res.header('Access-Control-Allow-Methods', 'PUT,GET,POST,DELETE,OPTIONS'); next(); }); if (this._useSessions) { app.use(clientSessions({ cookieName: 'session', secret: 'barchart-session-secret-1234567890', duration: 24 * 60 * 60 * 1000 })); } if (this._staticPaths !== null) { Object.keys(this._staticPaths).forEach((serverPath) => { const staticPathItem = this._staticPaths[serverPath]; if (staticPathItem.type === 'local') { logger.info('Bound static path', serverPath, 'on', (secure ? 'HTTPS' : 'HTTP'), 'port', port, 'to file system at', staticPathItem.filePath); app.use(serverPath, express.static(staticPathItem.filePath)); } else if (staticPathItem.type === 's3') { startPromise = startPromise .then(() => { const s3 = new S3Provider(staticPathItem.s3); return s3.start() .then(() => { logger.info('Bound static path', serverPath, 'on', (secure ? 'HTTPS' : 'HTTP'), 'port', port, 'to s3 bucket', staticPathItem.s3.bucket); const router = express.Router(); router.get(new RegExp('^[\\/\]*' + serverPath + '(.*)$'), (request, response) => { const requestPath = request.params[0]; if (is.string(requestPath) && requestPath.length > 0) { return s3.download(staticPathItem.keyPrefix + requestPath) .then((data) => { response.send(data); }).catch((e) => { response.status(404); response.json(generateRestResponse('file not found')); }); } else { response.status(404); response.json(generateRestResponse('no data')); } }); app.use(router); }); }); } else { logger.warn('Unable to configure static path', staticPathItem); } }); } const routeBindingStrategies = ExpressRouteBindingStrategy.getStrategies(); const pageKeys = Object.keys(this._pageMap); if (is.string(this._templatePath) && pageKeys.some(() => true)) { app.set('views', this._templatePath); app.engine('.hbs', expressHandlebars({extname: '.hbs'})); app.set('view engine', '.hbs'); pageKeys.forEach((key) => { const pageData = this._pageMap[key]; const basePath = pageData.path; const router = express.Router(); pageData.handlers.forEach((handlerData) => { const verb = handlerData.verb; const handlers = handlerData.handlers; const pagePath = handlerData.path; const template = handlerData.template; const routeBindingStrategy = routeBindingStrategies.find((candidate) => { return candidate.canBind(verb); }); if (routeBindingStrategy) { routeBindingStrategy.bind(router, verb, pagePath, handlers); logger.info('Bound page handler for', (secure ? 'HTTPS' : 'HTTP'), verb.getCode(), 'on port', port, 'at', path.join(basePath, pagePath), 'to', template + '.hbs'); } else { logger.warn('Unable to find appropriate binding strategy for endpoint using HTTP verb (' + verb.getCode() + ')'); } }); app.use(basePath, router); }); } const relayKeys = Object.keys(this._relayMap); relayKeys.forEach((key) => { const rootData = this._relayMap[key]; const basePath = rootData.path; const router = express.Router(); rootData.relays.forEach((relayData) => { const verb = relayData.verb; const handler = relayData.handler; const acceptPath = relayData.acceptPath; const forwardHost = relayData.forwardHost; const forwardPath = relayData.forwardPath; const routeBindingStrategy = routeBindingStrategies.find((candidate) => { return candidate.canBind(verb); }); if (routeBindingStrategy) { routeBindingStrategy.bind(router, verb, acceptPath, [ handler ]); logger.info('Bound relay for', (secure ? 'HTTPS' : 'HTTP'), verb.getCode(), 'on port', port, 'at', path.join(basePath, acceptPath), 'to', path.join(forwardHost, forwardPath)); } else { logger.warn('Unable to find appropriate binding strategy for endpoint using HTTP verb (' + verb.getCode() + ')'); } }); app.use(basePath, router); }); const serviceKeys = Object.keys(this._serviceMap); serviceKeys.forEach((key) => { const routeData = this._serviceMap[key]; const basePath = routeData.path; const router = express.Router(); routeData.handlers.forEach((handlerData) => { const verb = handlerData.verb; const handler = handlerData.handler; const routePath = handlerData.path; const routeBindingStrategy = routeBindingStrategies.find((candidate) => { return candidate.canBind(verb); }); if (routeBindingStrategy) { routeBindingStrategy.bind(router, verb, routePath, [ handler ]); logger.info('Bound REST handler for', (secure ? 'HTTPS' : 'HTTP'), verb.getCode(), 'on port', port, 'at', path.join(basePath, routePath)); } else { logger.warn('Unable to find appropriate binding strategy for endpoint using HTTP verb (' + verb.getCode() + ')'); } }); app.use(basePath, router); }); let server; if (secure) { server = https.createServer(app); } else { server = http.createServer(app); } const socketRequestKeys = Object.keys(this._socketRequestMap); const socketSubscriptionKeys = Object.keys(this._socketSubscriptionMap); if (socketRequestKeys.some(() => true) || socketSubscriptionKeys.some(() => true) || this._socketEmitters.some(() => true)) { const io = socketIO.listen(server); this._socketEmitters.forEach((emitterData) => { startStack.push( emitterData.event.register((data) => { Promise.resolve() .then(() => { return emitterData.room.command.process(data); }).then((qualifier) => { let room = emitterData.room.base; if (qualifier) { room = room + qualifier; } logger.debug('Socket.io emitter on port', port, 'emitting to', room); io.to(room).emit(emitterData.eventType, data); }); }) ); logger.info('Bound socket.io emitter on port', port, 'for base room', emitterData.room.base); }); socketRequestKeys.forEach((channel) => { logger.info('Bound socket.io request handler on port', port, 'to channel', channel); }); socketSubscriptionKeys.forEach((channel) => { logger.info('Bound socket.io subscription handler port', port, 'to channel', channel); }); io.on('connection', (socket) => { if (logger.isInfoEnabled()) { logger.info('Socket.io client [', socket.id, '] at', socket.conn.remoteAddress, 'connected on port', port); logger.info('Socket.io now has', Object.keys(socket.adapter.sids).length, 'connections'); } socket.on('disconnect', () => { if (logger.isInfoEnabled()) { logger.info('Socket.io client [', socket.id, '] at', socket.conn.remoteAddress, 'on port', port, 'disconnected'); logger.info('Socket.io now has', Object.keys(socket.adapter.sids).length, 'connections'); } }); socketRequestKeys.forEach((channel) => { const requestInfo = this._socketRequestMap[channel]; socket.on(channel, buildSocketRequestHandler(channel, requestInfo, socket)); }); socketSubscriptionKeys.forEach((channel) => { const subscriptionInfo = this._socketSubscriptionMap[channel]; socket.on(channel, buildSocketSubscriptionHandler(channel, subscriptionInfo, socket)); }); logger.info('Socket.io client [', socket.id, '] at', socket.conn.remoteAddress, 'on port', port, 'is ready to accept messages'); }); } server.listen(port); startStack.push(Disposable.fromAction(() => { server.close(); })); return startPromise.then(() => { return startStack; }); } } class ExpressServerContainer { constructor(staticPaths, templatePath) { this._serverMap = {}; this._staticPaths = staticPaths || null; this._templatePath = templatePath || null; this._started = false; } getServer(port, secure) { if (this._started) { throw new Error('Unable to manipulate servers, the server container has already started.'); } if (!this._serverMap.hasOwnProperty(port)) { this._serverMap[port] = new ExpressServer(port, secure, this._staticPaths, this._templatePath); } const returnRef = this._serverMap[port]; if (returnRef.getIsSecure() !== secure) { throw new Error('Unable to bind HTTP and HTTPS protocol to the same port (' + port + ').'); } return returnRef; } start() { if (this._started) { throw new Error('Unable to start servers, the server container has already started.'); } this._started = true; Promise.all( Object.keys(this._serverMap).map((port) => { const server = this._serverMap[port]; logger.info('Starting new ' + (server.getIsSecure() ? 'secure ' : '') + 'server on port ' + server.getPort()); return server.start(); }) ).then((disposables) => { return disposables.reduce((stack, disposable) => { stack.push(disposable); return stack; }, new DisposableStack()); }); } } class ExpressRouteBindingStrategy { constructor(verb, action) { assert.argumentIsRequired(verb, 'verb', Verb, 'Verb'); assert.argumentIsRequired(action, 'action', Function); this._verb = verb; this._action = action; } canBind(verb) { return this._verb === verb; } bind(router, verb, path, handlers) { assert.argumentIsRequired(router, router); assert.argumentIsRequired(verb, 'verb', Verb, 'Verb'); assert.argumentIsRequired(path, 'path', String); assert.argumentIsArray(handlers, 'handlers', Function, 'Function'); if (!this.canBind(verb)) { logger.warn('Unable to bind endpoint. The strategy does not support the HTTP verb (' + verb.getCode() + ')'); } return this._action(router, path, handlers); } } ExpressRouteBindingStrategy.getStrategies = () => { return [ new ExpressRouteBindingStrategy(Verb.GET, (router, path, handlers) => { router.get.apply(router, [path].concat(handlers)); }), new ExpressRouteBindingStrategy(Verb.POST, (router, path, handlers) => { router.post.apply(router, [path].concat(handlers)); }), new ExpressRouteBindingStrategy(Verb.PUT, (router, path, handlers) => { router.put.apply(router, [path].concat(handlers)); }), new ExpressRouteBindingStrategy(Verb.DELETE, (router, path, handlers) => { router.delete.apply(router, [path].concat(handlers)); }) ]; }; class ExpressArgumentExtractionStrategy { constructor(verb, action) { assert.argumentIsRequired(verb, 'verb', Verb, 'Verb'); assert.argumentIsRequired(action, 'action', Function); this._verb = verb; this._action = action; } canProcess(verb) { return this._verb === verb; } getCommandArguments(verb, request, useSession, acceptFile) { assert.argumentIsRequired(request, 'request'); if (!this.canProcess(verb)) { logger.warn('Unable to extract arguments from HTTP request.'); } const returnRef = this._action(request); if (useSession) { returnRef.session = request.session || { }; } if (acceptFile) { returnRef.file = request.file; } return returnRef; } } ExpressArgumentExtractionStrategy.getStrategies = () => { return [ new ExpressArgumentExtractionStrategy(Verb.GET, (req) => { return Object.assign({}, req.query || {}, req.params || {}); }), new ExpressArgumentExtractionStrategy(Verb.POST, (req) => { return Object.assign({}, req.params || {}, req.body || {}); }), new ExpressArgumentExtractionStrategy(Verb.PUT, (req) => { return Object.assign({}, req.params || {}, req.body || {}); }), new ExpressArgumentExtractionStrategy(Verb.DELETE, (req) => { return Object.assign({}, req.query || {}, req.params || {}); }) ]; }; class ContainerBindingStrategy { constructor() { } canBind(container) { return this._canBind(container); } _canBind(container) { return false; } bind(container, serverContainer) { assert.argumentIsRequired(container, 'container', Container, 'Container'); assert.argumentIsRequired(serverContainer, 'serverContainer', ExpressServerContainer, 'ExpressServerContainer'); if (!this.canBind(container)) { throw new Error('Unable to bind container, the strategy does not support the container.'); } return Promise.resolve(this._bind(container, serverContainer)); } _bind(container, serverContainer) { return false; } } class RestContainerBindingStrategy extends ContainerBindingStrategy { constructor() { super(); } _canBind(container) { return container instanceof RestContainer; } _bind(container, serverContainer) { const endpoints = container.getEndpoints(); const server = serverContainer.getServer(container.getPort(), container.getIsSecure(), false); endpoints.forEach((endpoint) => { server.addService(container.getPath(), endpoint.getPath(), endpoint.getRestAction().getVerb(), endpoint.getExecutionCommand(), endpoint.getValidationCommand()); }); return true; } } class SocketRequestContainerBindingStrategy extends ContainerBindingStrategy { constructor() { super(); } _canBind(container) { return container instanceof SocketRequestContainer; } _bind(container, serverContainer) { const endpoints = container.getEndpoints(); const server = serverContainer.getServer(container.getPort(), container.getIsSecure()); endpoints.forEach((endpoint) => { server.addChannel(container.getPath(), endpoint.getChannel(), endpoint.getExecutionCommand(), endpoint.getValidationCommand()); }); return true; } } class SocketEmitterContainerBindingStrategy extends ContainerBindingStrategy { constructor() { super(); } _canBind(container) { return container instanceof SocketEmitterContainer; } _bind(container, serverContainer) { const endpoints = container.getEndpoints(); const server = serverContainer.getServer(container.getPort(), container.getIsSecure()); endpoints.forEach((endpoint) => { server.addEmitter(container.getPath(), endpoint.getChannel(), endpoint.getEvent(), endpoint.getEventType(), endpoint.getRoomCommand()); }); return true; } } class SocketSubscriptionContainerBindingStrategy extends ContainerBindingStrategy { constructor() { super(); } _canBind(container) { return container instanceof SocketSubscriptionContainer; } _bind(container, serverContainer) { const endpoints = container.getEndpoints(); const server = serverContainer.getServer(container.getPort(), container.getIsSecure()); endpoints.forEach((endpoint) => { server.addSubscription(container.getPath(), endpoint.getChannel(), endpoint.getRoomsCommand(), endpoint.getResponseCommand(), endpoint.getResponseEventType(), endpoint.getValidationCommand()); }); return true; } } class HtmlContainerBindingStrategy extends ContainerBindingStrategy { constructor() { super(); } _canBind(container) { return container instanceof PageContainer; } _bind(container, serverContainer) { const endpoints = container.getEndpoints(); const server = serverContainer.getServer(container.getPort(), container.getIsSecure()); endpoints.forEach((endpoint) => { server.addPage(container.getPath(), endpoint.getPath(), endpoint.getTemplate(), endpoint.getVerb(), endpoint.getExecutionCommand(), endpoint.getCache(), container.getUsesSession(), endpoint.getAcceptFile(), container.getSecureRedirect() || endpoint.getSecureRedirect()); }); return true; } } class RelayContainerBindingStrategy extends ContainerBindingStrategy { constructor() { super(); } _canBind(container) { return container instanceof RelayContainer; } _bind(container, serverContainer) { const endpoints = container.getEndpoints(); const server = serverContainer.getServer(container.getPort(), container.getIsSecure()); endpoints.forEach((endpoint) => { server.addRelay(container.getPath(), endpoint.getAcceptPath(), endpoint.getForwardHost(), endpoint.getForwardPath(), endpoint.getVerb(), endpoint.getHeaderOverrides(), endpoint.getParameterOverrides()); }); return true; } } ContainerBindingStrategy.getStrategies = () => { return [ new RestContainerBindingStrategy(), new SocketRequestContainerBindingStrategy(), new SocketEmitterContainerBindingStrategy(), new SocketSubscriptionContainerBindingStrategy(), new HtmlContainerBindingStrategy(), new RelayContainerBindingStrategy() ]; }; function buildPageHandlers(verb, basePath, routePath, template, command, cache, useSession, acceptFile, secureRedirect) { const handlers = [ ]; if (acceptFile) { const uploader = multer({ storage: multer.memoryStorage(), limits: { files: 1, fileSize: 10485760 } }); handlers.push(uploader.single('file')); } let sequencer = 0; let argumentExtractionStrategy = ExpressArgumentExtractionStrategy.getStrategies().find((candidate) => { return candidate.canProcess(verb); }); if (!argumentExtractionStrategy) { logger.warn('Unable to find appropriate argument extraction strategy for HTTP ' + verb.getCode() + ' requests'); argumentExtractionStrategy = () => { return {}; }; } handlers.push((request, response) => { const sequence = sequencer++; logger.debug('Processing starting for', verb.getCode(), 'at', path.join(basePath, routePath), '(' + sequence + ')'); let handlerPromise; if (secureRedirect && request.headers['x-forwarded-proto'] === 'http') { handlerPromise = promise.build((resolveCallback, rejectCallback) => { if (verb === Verb.GET) { logger.warn('Redirecting HTTP ', verb.getCode(), 'at', path.join(basePath, routePath), ' to HTTPS (' + sequence + ')'); response.redirect('https://' + request.headers.host + request.url); resolveCallback(); } else { logger.error('Unable to redirect HTTP ', verb.getCode(), 'at', path.join(basePath, routePath), ' to HTTPS (' + sequence + ')'); rejectCallback('Unable to redirect HTTP ', verb.getCode(), 'at', path.join(basePath, routePath), ' to HTTPS (' + sequence + ')'); } }); } else { handlerPromise = Promise.resolve() .then(() => { const commandArguments = argumentExtractionStrategy.getCommandArguments(verb, request, useSession, acceptFile); return command.process(commandArguments); }).then((result) => { if (!cache) { response.setHeader('Cache-Control', 'private, max-age=0, no-cache'); } response.render(template, result); logger.debug('Processing completed for', verb.getCode(), 'at', path.join(basePath + routePath), '(' + sequence + ')'); }); } return handlerPromise .catch((error) => { logger.error('Processing failed for', verb.getCode(), 'at', path.join(basePath, routePath), '(' + sequence + ')'); logger.error(error); response.status(500); response.json(generateRestResponse(error.message || error.toString() || 'internal server error')); }); }); return handlers; } function buildRelayHandler(basePath, acceptPath, forwardHost, forwardPath, verb, headerOverrides, parameterOverrides) { return proxy(forwardHost, { filter: (request, response) => { return request.method == verb.getCode(); }, forwardPath: (request, response) => { let returnRef = forwardPath; if (Verb.GET === verb) { Object.assign(request.query || { }, parameterOverrides); if (Object.keys(request.query).some(() => true)) { returnRef = returnRef + '?' + querystring.stringify(request.query); } } return returnRef; }, decorateRequest: (request) => { Object.assign(request.headers, headerOverrides); if (Verb.GET !== verb) { Object.assign(request.body || { }, parameterOverrides); } return request; } }); } let sequencer = 0; function buildRestHandler(verb, basePath, routePath, command, validationCommand) { let argumentExtractionStrategy = ExpressArgumentExtractionStrategy.getStrategies().find((candidate) => { return candidate.canProcess(verb); }); if (!argumentExtractionStrategy) { logger.warn('Unable to find appropriate argument extraction strategy for HTTP ' + verb.getCode() + ' requests'); argumentExtractionStrategy = () => { return {}; }; } return (request, response) => { const sequence = sequencer++; logger.debug('Processing starting for', verb.getCode(), 'at', path.join(basePath, routePath), '(' + sequence + ')'); return Promise.resolve() .then(() => { const validationData = { payload: argumentExtractionStrategy.getCommandArguments(verb, request) || { } }; const authorization = request.get('authorization'); if (is.string(authorization) && authorization.length > 0) { validationData.context = { }; validationData.context.token = request.headers.authorization; } else { validationData.context = null; } logger.trace('Validating command (' + sequence + ') with the following arguments:', validationData); return Promise.resolve(validationCommand.process(validationData)) .then((result) => { if (result) { logger.trace('Validated command (' + sequence + ')'); return validationData.payload; } else { logger.info('Validate failed (' + sequence + ')'); return null; } }).catch((e) => { logger.error('Validate error (' + sequence + ')', e); return null; }); }).then((commandArguments) => { if (commandArguments === null) { response.status(401); response.json(generateRestResponse('unauthorized')); } else { logger.trace('Processing command (' + sequence + ') with the following arguments:', commandArguments); return Promise.resolve(command.process(commandArguments)) .then((result) => { if (is.object(result) || is.array(result)) { response.json(result); } else if (verb === Verb.GET && (is.null(result) || is.undefined(result))) { response.status(404); response.json(generateRestResponse('no data')); } else { response.status(200); response.json(generateRestResponse('success')); } logger.debug('Processing completed for', verb.getCode(), 'at', path.join(basePath, routePath), '(' + sequence + ')'); }); } }).catch((error) => { logger.error('Processing failed for', verb.getCode(), 'at', path.join(basePath, routePath), '(' + sequence + ')'); logger.error(error); response.status(500); response.json(generateRestResponse(error.message || error.toString() || 'internal server error')); }); }; } function buildSocketRequestHandler(channel, requestInfo, socket) { return (request) => { const sequence = sequencer++; const requestId = request.requestId; if (!is.string(requestId)) { throw new Error('Unable to process socket.io request. A "requestId" property is expected.'); } logger.debug('Processing starting for socket.io request from [', socket.id ,'] on', channel, '(', sequence, ')'); return Promise.resolve() .then(() => { let validationData; if (request.context) { validationData = { context: request.context, payload: request.request }; } else { validationData = null; } return requestInfo.commands.validation.process(validationData); }).then((valid) => { if (!valid) { throw new Error('Unable to process request, validation failed.'); } return requestInfo.commands.execution.process(request.request); }).then((result) => { const envelope = { requestId: request.requestId, response: result || {} }; socket.emit('response', envelope); logger.debug('Processing completed for socket.io request on', channel, '(', sequence, ')'); }).catch((error) => { logger.error('Processing failed for socket.io request on', channel, '(', sequence, ')'); logger.error(error); }); }; } function buildSocketSubscriptionHandler(channel, subscriptionInfo, socket) { return (request) => { const sequence = sequencer++; logger.debug('Processing starting for socket.io subscription request from [', socket.id ,'] on', channel, '(', sequence, ')'); return Promise.resolve() .then(() => { let validationData; if (request.context) { const context = request.context; const payload = request; delete request.context; validationData = { context: context, payload: payload }; } else { validationData = null; } return subscriptionInfo.commands.validation.process(validationData); }).then((valid) => { if (!valid) { throw new Error('Unable to process subscription, validation failed.'); } return subscriptionInfo.commands.rooms.process(request); }).then((qualifiers) => { let qualifiersToJoin; if (is.array(qualifiers)) { qualifiersToJoin = qualifiers; } else if (is.string(qualifiers)) { qualifiersToJoin = [ qualifiers ]; } else { qualifiersToJoin = [ ]; } const roomsToJoin = qualifiersToJoin.map((qualifierToJoin) => { return subscriptionInfo.room.base + qualifierToJoin; }); roomsToJoin.forEach((roomToJoin) => { socket.join(roomToJoin); }); logger.debug('Socket.io client [', socket.id, '] joined [', roomsToJoin.join(','), ']'); let responsePromise; if (subscriptionInfo.response.eventType) { responsePromise = subscriptionInfo.commands.response.process(request) .then((response) => { if (response) { socket.emit(subscriptionInfo.response.eventType, response); logger.debug('Socket.io client [', socket.id, '] sent immediate response after joining [', roomsToJoin.join(','), ']'); } }); } else { responsePromise = Promise.resolve(); } return responsePromise; }).then(() => { logger.debug('Processing completed for socket.io subscription request on', channel, '(', sequence, ')'); }).catch((error) => { logger.error('Processing failed for socket.io subscription request on', channel, '(', sequence, ')'); logger.error(error); }); }; } function generateRestResponse(message) { return { message: message }; } return ExpressServerFactory; })();