UNPKG

@themost/web

Version:

MOST Web Framework 2.0 - Web Server Module

1,419 lines (1,350 loc) 50.3 kB
/** * @license * MOST Web Framework 2.0 Codename Blueshift * Copyright (c) 2017, THEMOST LP All rights reserved * * Use of this source code is governed by an BSD-3-Clause license that can be * found in the LICENSE file at https://themost.io/license */ /// var HttpError = require('@themost/common/errors').HttpError; var HttpServerError = require('@themost/common/errors').HttpServerError; var HttpNotFoundError = require('@themost/common/errors').HttpNotFoundError; var Args = require('@themost/common/utils').Args; var TraceUtils = require('@themost/common/utils').TraceUtils; var sprintf = require('sprintf').sprintf; var _ = require('lodash'); var mvc = require('./mvc'); var LangUtils = require('@themost/common/utils').LangUtils; var path = require("path"); var fs = require("fs"); var ejs = require('ejs'); var url = require('url'); var http = require('http'); var SequentialEventEmitter = require('@themost/common/emitter').SequentialEventEmitter; var DataConfigurationStrategy = require('@themost/data/data-configuration').DataConfigurationStrategy; var querystring = require('querystring'); var crypto = require('crypto'); var Symbol = require('symbol'); var HttpHandler = require('./types').HttpHandler; var AuthStrategy = require('./handlers/auth').AuthStrategy; var DefaultAuthStrategy = require('./handlers/auth').DefaultAuthStrategy; var EncryptionStrategy = require('./handlers/auth').EncryptionStrategy; var DefaultEncryptionStrategy = require('./handlers/auth').DefaultEncryptionStrategy; var CacheStrategy = require('./cache').CacheStrategy; var DefaultCacheStrategy = require('./cache').DefaultCacheStrategy; var LocalizationStrategy = require('./localization').LocalizationStrategy; var DefaulLocalizationStrategy = require('./localization').DefaultLocalizationStrategy; var HttpConfiguration = require('./config').HttpConfiguration; var HttpApplicationService = require('./types').HttpApplicationService; var HttpContext = require('./context').HttpContext; var StaticHandler = require('./handlers/static').StaticHandler; var executionPathProperty = Symbol('executionPath'); var configPathProperty = Symbol('configPath'); var configProperty = Symbol('config'); var currentProperty = Symbol('current'); var servicesProperty = Symbol('services'); var DEFAULT_HTML_ERROR = fs.readFileSync(path.resolve(__dirname, 'http-error.html.ejs'), 'utf8'); /** * @classdesc ApplicationOptions class describes the startup options of a MOST Web Framework application. * @class * @constructor * @property {number} port - The HTTP binding port number. * The default value is either PORT environment variable or 3000. * @property {string} bind - The HTTP binding ip address or hostname. * The default value is either IP environment variable or 127.0.0.1. * @property {number|string} cluster - A number which represents the number of clustered applications. * The default value is zero (no clustering). If cluster is 'auto' then the number of clustered applications * depends on hardware capabilities (number of CPUs). @example //load module var web = require("most-web"); //start server web.current.start({ port:80, bind:"0.0.0.0",cluster:'auto' }); @example //Environment variables already set: IP=198.51.100.0 PORT=80 var web = require("most-web"); web.current.start(); */ // eslint-disable-next-line no-unused-vars function ApplicationOptions() { } /** * Abstract class that represents a data context * @constructor */ function HttpDataContext() { // } /** * @returns {DataAdapter} */ HttpDataContext.prototype.db = function () { return null; }; /** * @param {string} name * @returns {DataModel} */ // eslint-disable-next-line no-unused-vars HttpDataContext.prototype.model = function (name) { return null; }; /** * @param {string} type * @returns {*} */ // eslint-disable-next-line no-unused-vars HttpDataContext.prototype.dataTypes = function (type) { return null; }; /** * * @param {HttpApplication} app * @constructor */ function HttpContextProvider(app) { HttpContextProvider.super_.bind(this)(app); } LangUtils.inherits(HttpContextProvider,HttpApplicationService); /** * @returns {HttpContext} */ HttpContextProvider.prototype.create = function(req,res) { var context = new HttpContext(req,res); //set context application context.application = this.getApplication(); return context; }; /** * @class * @constructor * @param {string=} executionPath * @augments SequentialEventEmitter * @augments IApplication */ function HttpApplication(executionPath) { //Sets the current execution path this[executionPathProperty] = _.isNil(executionPath) ? path.join(process.cwd()) : path.resolve(executionPath); //Gets the current application configuration path this[configPathProperty] = _.isNil(executionPath) ? path.join(process.cwd(), 'config') : path.resolve(executionPath, 'config'); //initialize services this[servicesProperty] = { }; //set configuration this[configProperty] = new HttpConfiguration(this[configPathProperty]); /** * Gets or sets a collection of application handlers * @type {Array} */ this.handlers = []; var self = this; //initialize handlers collection var configurationHandlers = this.getConfiguration().handlers; var defaultHandlers = require('./resources/app.json').handlers; for (var i = 0; i < defaultHandlers.length; i++) { (function(item) { if (typeof configurationHandlers.filter(function(x) { return x.name === item.name; })[0] === 'undefined') { configurationHandlers.push(item); } })(defaultHandlers[i]); } var reModule = /^@themost\/web\//i; _.forEach(configurationHandlers, function (handlerConfiguration) { try { var handlerPath = handlerConfiguration.type; if (reModule.test(handlerPath)) { handlerPath = handlerPath.replace(reModule,'./'); } else if (/^\//.test(handlerPath)) { handlerPath = self.mapPath(handlerPath); } var handlerModule = require(handlerPath), handler = null; if (handlerModule) { //if module exports a constructor if (typeof handlerModule === 'function') { self.handlers.push(new handlerModule()); } //else if module exports a method called createInstance() else if (typeof handlerModule.createInstance === 'function') { //call createInstance handler = handlerModule.createInstance(); if (handler) { self.handlers.push(handler); } } else { TraceUtils.log('The specified handler (%s) cannot be instantiated. The module does not export a class constructor or createInstance() function.', handlerConfiguration.name); } } } catch (err) { throw new Error(sprintf('The specified handler (%s) cannot be loaded. %s', handlerConfiguration.name, err.message)); } }); //set default context provider self.useService(HttpContextProvider); //set authentication strategy self.useStrategy(AuthStrategy, DefaultAuthStrategy); //set cache strategy self.useStrategy(CacheStrategy, DefaultCacheStrategy); //set encryption strategy self.useStrategy(EncryptionStrategy, DefaultEncryptionStrategy); //set localization strategy self.useStrategy(LocalizationStrategy, DefaulLocalizationStrategy); //set authentication strategy self.getConfiguration().useStrategy(DataConfigurationStrategy, DataConfigurationStrategy); /** * Gets or sets a boolean that indicates whether the application is in development mode * @type {string} */ this.development = (process.env.NODE_ENV === 'development'); /** * * @type {{html, text, json, unauthorized}|*} */ this.errors = httpApplicationErrors(this); } LangUtils.inherits(HttpApplication, SequentialEventEmitter); /** * @returns {HttpApplication} */ HttpApplication.getCurrent = function() { if (typeof HttpApplication[currentProperty] === 'object') { return HttpApplication[currentProperty]; } HttpApplication[currentProperty] = new HttpApplication(); return HttpApplication[currentProperty]; }; /** * @returns {HttpConfiguration} */ HttpApplication.prototype.getConfiguration = function() { return this[configProperty]; }; /** * @returns {EncryptionStrategy} */ HttpApplication.prototype.getEncryptionStrategy = function() { return this.getStrategy(EncryptionStrategy); }; /** * @returns {AuthStrategy} */ HttpApplication.prototype.getAuthStrategy = function() { return this.getStrategy(AuthStrategy); }; /** * @returns {LocalizationStrategy} */ HttpApplication.prototype.getLocalizationStrategy = function() { return this.getStrategy(LocalizationStrategy); }; HttpApplication.prototype.getExecutionPath = function() { return this[executionPathProperty]; }; /** * Resolves the given path * @param {string} arg */ HttpApplication.prototype.mapExecutionPath = function(arg) { Args.check(_.isString(arg),'Path must be a string'); return path.resolve(this.getExecutionPath(), arg); }; /** * Sets static content root directory * @param {string} rootDir */ HttpApplication.prototype.useStaticContent = function(rootDir) { /** * @type {StaticHandler} */ var staticHandler = _.find(this.handlers, function(x) { return x.constructor === StaticHandler; }); if (typeof staticHandler === 'undefined') { throw new Error('An instance of StaticHandler class cannot be found in application handlers'); } staticHandler.rootDir = rootDir; return this; }; HttpApplication.prototype.getConfigurationPath = function() { return this[configPathProperty]; }; /** * Initializes application configuration. * @return {HttpApplication} */ HttpApplication.prototype.init = function () { //initialize basic directives collection var directives = require("./angular/directives"); directives.apply(this); return this; }; /** * Returns the path of a physical file based on a given URL. * @param {string} s */ HttpApplication.prototype.mapPath = function (s) { var uri = url.parse(s).pathname; return path.join(this[executionPathProperty], uri); }; /** * Converts an application URL into one that is usable on the requesting client. A valid application relative URL always start with "~/". * If the relativeUrl parameter contains an absolute URL, the URL is returned unchanged. * Note: An HTTP application base path may be set in settings/app/base configuration section. The default value is "/". * @param {string} appRelativeUrl - A string which represents an application relative URL like ~/login */ HttpApplication.prototype.resolveUrl = function (appRelativeUrl) { if (/^~\//.test(appRelativeUrl)) { var base = this.getConfiguration().getSourceAt("settings/app/base") || "/"; base += /\/$/.test(base) ? '' : '/'; return appRelativeUrl.replace(/^~\//, base); } return appRelativeUrl; }; /** * Resolves ETag header for the given file. If the specified does not exist or is invalid returns null. * @param {string=} file - A string that represents the file we want to query * @param {function(Error,string=)} callback */ HttpApplication.prototype.resolveETag = function(file, callback) { fs.exists(file, function(exists) { try { if (exists) { fs.stat(file, function(err, stats) { if (err) { callback(err); } else { if (!stats.isFile()) { callback(null); } else { //validate if-none-match var md5 = crypto.createHash('md5'); md5.update(stats.mtime.toString()); var result = md5.digest('base64'); callback(null, result); } } }); } else { callback(null); } } catch (e) { callback(null); } }); }; // noinspection JSUnusedGlobalSymbols /** * @param {HttpContext} context * @param {string} executionPath * @param {function(Error, Boolean)} callback */ HttpApplication.prototype.unmodifiedRequest = function(context, executionPath, callback) { try { var requestETag = context.request.headers['if-none-match']; if (typeof requestETag === 'undefined' || requestETag == null) { callback(null, false); return; } HttpApplication.prototype.resolveETag(executionPath, function(err, result) { callback(null, (requestETag===result)); }); } catch (err) { TraceUtils.error(err); callback(null, false); } }; /** * @param request {string|IncomingMessage} * @returns {*} * */ HttpApplication.prototype.resolveMime = function (request) { var extensionName; if (typeof request=== 'string') { //get file extension extensionName = path.extname(request); } else if (typeof request=== 'object') { //get file extension extensionName = path.extname(request.url); } else { return; } return _.find(this.getConfiguration().mimes, function(x) { return (x.extension === extensionName); }); }; /** * * @param {HttpContext} context * @param {Function} callback */ HttpApplication.prototype.processRequest = function (context, callback) { var self = this; if (typeof context === 'undefined' || context == null) { callback.call(self); } else { //1. beginRequest context.emit('beginRequest', context, function (err) { if (err) { callback.call(context, err); } else { //2. validateRequest context.emit('validateRequest', context, function (err) { if (err) { callback.call(context, err); } else { //3. authenticateRequest context.emit('authenticateRequest', context, function (err) { if (err) { callback.call(context, err); } else { //4. authorizeRequest context.emit('authorizeRequest', context, function (err) { if (err) { callback.call(context, err); } else { //5. mapRequest context.emit('mapRequest', context, function (err) { if (err) { callback.call(context, err); } else { //5b. postMapRequest context.emit('postMapRequest', context, function(err) { if (err) { callback.call(context, err); } else { //process HEAD request if (context.request.method==='HEAD') { //7. endRequest context.emit('endRequest', context, function (err) { callback.call(context, err); }); } else { //6. processRequest if (context.request.currentHandler != null) context.request.currentHandler.processRequest(context, function (err) { if (err) { callback.call(context, err); } else { //7. endRequest context.emit('endRequest', context, function (err) { callback.call(context, err); }); } }); else { var er = new HttpNotFoundError(); if (context.request && context.request.url) { er.resource = context.request.url; } callback.call(context, er); } } } }); } }); } }); } }); } }); } }); } }; /** * Gets the default data context based on the current configuration * @returns {DataAdapter} */ HttpApplication.prototype.db = function () { if ((this.config.adapters === null) || (this.config.adapters.length === 0)) throw new Error('Data adapters configuration settings are missing or cannot be accessed.'); var adapter = null; if (this.config.adapters.length === 1) { //there is only one adapter so try to instantiate it adapter = this.config.adapters[0]; } else { adapter = _.find(this.config.adapters,function (x) { return x.default; }); } if (adapter === null) throw new Error('There is no default data adapter or the configuration is incorrect.'); //try to instantiate adapter if (!adapter.invariantName) throw new Error('The default data adapter has no invariant name.'); var adapterType = this.config.adapterTypes[adapter.invariantName]; if (adapterType == null) throw new Error('The default data adapter type cannot be found.'); if (typeof adapterType.createInstance === 'function') { return adapterType.createInstance(adapter.options); } else if (adapterType.require) { var m = require(adapterType.require); if (typeof m.createInstance === 'function') { return m.createInstance(adapter.options); } throw new Error('The default data adapter cannot be instantiated. The module provided does not export a function called createInstance().') } }; /** * @returns {HttpContextProvider} */ HttpApplication.prototype.getContextProvider = function() { return this.getService(HttpContextProvider); }; /** * Creates an instance of HttpContext class. * @param {ClientRequest} request * @param {ServerResponse} response * @returns {HttpContext} */ HttpApplication.prototype.createContext = function (request, response) { var context = this.getContextProvider().create(request, response); //set context application context.application = this; //set handler events for (var i = 0; i < HttpHandler.Events.length; i++) { var eventName = HttpHandler.Events[i]; for (var j = 0; j < this.handlers.length; j++) { var handler = this.handlers[j]; if (typeof handler[eventName] === 'function') { context.on(eventName, handler[eventName].bind(handler)); } } } return context; }; /** * @param {*} options * @param {*} data * @param {Function} callback */ HttpApplication.prototype.executeExternalRequest = function(options,data, callback) { //make request var https = require('https'), opts = (typeof options==='string') ? url.parse(options) : options, httpModule = (opts.protocol === 'https:') ? https : http; var req = httpModule.request(opts, function(res) { res.setEncoding('utf8'); var data = ''; res.on('data', function (chunk) { data += chunk; }); res.on('end', function(){ var result = { statusCode: res.statusCode, headers: res.headers, body:data, encoding:'utf8' }; /** * destroy sockets (manually close an unused socket) ? */ callback(null, result); }); }); req.on('error', function(e) { //return error callback(e); }); if(data) { if (typeof data ==="object" ) req.write(JSON.stringify(data)); else req.write(data.toString()); } req.end(); }; /** * Executes an internal process * @param {Function(HttpContext)} fn */ HttpApplication.prototype.execute = function (fn) { var request = createRequestInternal.call(this); fn.call(this, this.createContext(request, createResponseInternal.call(this,request))); }; /** * Executes an unattended internal process * @param {Function} fn */ HttpApplication.prototype.unattended = function (fn) { //create context var request = createRequestInternal.call(this), context = this.createContext(request, createResponseInternal.call(this,request)); //get unattended account var account = this.getAuthStrategy().getUnattendedExecutionAccount(); //set unattended execution account if (typeof account !== 'undefined' || account!==null) { context.user = { name: account, authenticationType: 'Basic'}; } //execute internal process fn.call(this, context); }; /** * Load application extension */ HttpApplication.prototype.extend = function (extension) { if (typeof extension === 'undefined') { //register all application extensions var extensionFolder = this.mapPath('/extensions'); if (fs.existsSync(extensionFolder)) { var arr = fs.readdirSync(extensionFolder); for (var i = 0; i < arr.length; i++) { if (path.extname(arr[i])==='.js') require(path.join(extensionFolder, arr[i])); } } } else { //register the specified extension if (typeof extension === 'string') { var extensionPath = this.mapPath(sprintf('/extensions/%s.js', extension)); if (fs.existsSync(extensionPath)) { //load extension require(extensionPath); } } } return this; }; /** * * @param {*|string} options * @param {Function} callback */ HttpApplication.prototype.executeRequest = function (options, callback) { var opts = { }; if (typeof options === 'string') { _.assign(opts, { url:options }); } else { _.assign(opts, options); } var request = createRequestInternal.call(this,opts), response = createResponseInternal.call(this,request); if (!opts.url) { callback(new Error('Internal request url cannot be empty at this context.')); return; } if (opts.url.indexOf('/') !== 0) { var uri = url.parse(opts.url); opts.host = uri.host; opts.hostname = uri.hostname; opts.path = uri.path; opts.port = uri.port; //execute external request this.executeExternalRequest(opts,null, callback); } else { //todo::set cookie header (for internal requests) /* IMPORTANT: set response Content-Length to -1 in order to force the default HTTP response format. if the content length is unknown (server response does not have this header) in earlier version of node.js <0.11.9 the response contains by default a hexadecimal number that represents the content length. This number appears exactly after response headers and before response body. If the content length is defined the operation omits this hexadecimal value e.g. the wrong or custom formatted response HTTP 1.1 Status OK Content-Type: text/html ... Connection: keep-alive 6b8 <html><body> ... </body></html> e.g. the standard format HTTP 1.1 Status OK Content-Type: text/html ... Connection: keep-alive <html><body> ... </body></html> */ response.setHeader('Content-Length',-1); handleRequestInternal.call(this, request, response, function(err) { if (err) { callback(err); } else { try { //get statusCode var statusCode = response.statusCode; //get headers var headers = {}; if (response._header) { var arr = response._header.split('\r\n'); for (var i = 0; i < arr.length; i++) { var header = arr[i]; if (header) { var k = header.indexOf(':'); if (k>0) { headers[header.substr(0,k)] = header.substr(k+1); } } } } //get body var body = null; var encoding = null; if (_.isArray(response.output)) { if (response.output.length>0) { body = response.output[0].substr(response._header.length); encoding = response.outputEncodings[0]; } } //build result (something like ServerResponse) var result = { statusCode: statusCode, headers: headers, body:body, encoding:encoding }; callback(null, result); } catch (e) { callback(e); } } }); } }; /** * @private * @this HttpApplication * @param {ClientRequest} request * @param {ServerResponse} response * @param callback */ function handleRequestInternal(request, response, callback) { var self = this, context = self.createContext(request, response); //add query string if (request.url.indexOf('?') > 0) _.assign(context.params, querystring.parse(request.url.substring(request.url.indexOf('?') + 1))); //add form if (request.form) _.assign(context.params, request.form); //add files if (request.files) _.assign(context.params, request.files); self.processRequest(context, function (err) { if (err) { if (self.listeners('error').length === 0) { onError.bind(self)(context, err, function () { response.end(); callback(); }); } else { //raise application error event self.emit('error', { context:context, error:err } , function () { response.end(); callback(); }); } } else { context.finalize(function() { response.end(); callback(); }); } }); } /** * @private * @param {*} options */ function createRequestInternal(options) { var opt = options ? options : {}; var request = new http.IncomingMessage(); request.method = (opt.method) ? opt.method : 'GET'; request.url = (opt.url) ? opt.url : '/'; request.httpVersion = '1.1'; request.headers = (opt.headers) ? opt.headers : { host: 'localhost', 'user-agent': 'Mozilla/5.0 (X11; Linux i686; rv:10.0) Gecko/20100101 Firefox/22.0', accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'accept-language': 'en,en-US;q=0.5', 'accept-encoding': 'gzip, deflate', connection: 'keep-alive', 'cache-control': 'max-age=0' }; if (opt.cookie) request.headers.cookie = opt.cookie; request.cookies = (opt.cookies) ? opt.cookies : {}; request.session = (opt.session) ? opt.session : {}; request.params = (opt.params) ? opt.params : {}; request.query = (opt.query) ? opt.query : {}; request.form = (opt.form) ? opt.form : {}; request.body = (opt.body) ? opt.body : {}; request.files = (opt.files) ? opt.files : {}; return request; } /** * Creates a mock-up server response * @param {ClientRequest} req * @returns {ServerResponse|*} * @private */ function createResponseInternal(req) { return new http.ServerResponse(req); } /** * * @param {HttpContext} context * @param {Error|*} err * @param {function(Error=)} callback * @private */ function onHtmlError(context, err, callback) { try { if (context == null) { return callback(err); } // get request and response var request = context.request; var response = context.response; // validate request if ((request == null) || (response == null)) { return callback(err); } //HTML custom errors var str; if (err instanceof HttpError) { str = ejs.render(DEFAULT_HTML_ERROR, { model:err, html: { resolveUrl: context.resolveUrl.bind(context) } }); } else { // convert error to http error var finalErr = new HttpError(500, null, err.message); finalErr.stack = err.stack; str = ejs.render(DEFAULT_HTML_ERROR, { model: finalErr, html: { resolveUrl: context.resolveUrl.bind(context) } }); } //write status header response.writeHead(err.statusCode || 500 , { "Content-Type": "text/html" }); response.write(str); response.end(); return callback(); } catch (err) { //log process error TraceUtils.error(err); //and continue execution callback(err); } } /** * @private * @this HttpApplication * @param {HttpContext} context * @param {Error|*} err * @param {Function} callback */ function onError(context, err, callback) { callback = callback || function () { }; try { if (err == null) { return callback(); } // log request if (context.request) { TraceUtils.error(context.request.method + ' ' + ((context.user && context.user.name) || 'unknwon') + ' ' + context.request.url); } //log error TraceUtils.error(err); //get response object var response = context.response; // if response is null exit if (response == null) { return callback(); } // if response headers have been sent exit if (response._headerSent) { return callback(); } if (context.format) { /** * try to find an error handler based on current request * @type Function */ var errorHandler = this.errors[context.format]; if (typeof errorHandler === 'function') { return errorHandler(context, err, function(err) { if (err) { TraceUtils.error('An error occurred while handling request error'); TraceUtils.error(err); } return callback(); }); } } onHtmlError(context, err, function(err) { if (err) { //send plain text response.writeHead(err.statusCode || 500, {"Content-Type": "text/plain"}); //if error is an HTTP Exception if (err instanceof HttpError) { response.write(err.statusCode + ' ' + err.message + "\n"); } else { //otherwise send status 500 response.write('500 ' + err.message + "\n"); } //send extra data (on development) if (process.env.NODE_ENV === 'development') { if (err.innerMessage) { response.write(err.innerMessage + "\n"); } if (err.stack) { response.write(err.stack + "\n"); } } } return callback(); }); } catch (err) { TraceUtils.log(err); if (context.response) { context.response.writeHead(500, {"Content-Type": "text/plain"}); context.response.write("500 Internal Server Error"); return callback.bind(this)(); } } } /** * @private * @type {string} */ var HTTP_SERVER_DEFAULT_BIND = '127.0.0.1'; /** * @private * @type {number} */ var HTTP_SERVER_DEFAULT_PORT = 3000; /** * @private * @param {Function=} callback * @param {ApplicationOptions|*} options */ function startInternal(options, callback) { var self = this; callback = callback || function() { }; try { //validate options if (self.config === null) self.init(); /** * @memberof process.env * @property {number} PORT * @property {string} IP * @property {string} NODE_ENV */ var opts = { bind:(process.env.IP || HTTP_SERVER_DEFAULT_BIND), port:(process.env.PORT ? process.env.PORT: HTTP_SERVER_DEFAULT_PORT) }; //extend options _.assign(opts, options); var server_ = http.createServer(function (request, response) { var context = self.createContext(request, response); //begin request processing self.processRequest(context, function (err) { if (err) { //handle context error event if (context.listeners('error').length > 0) { return context.emit('error', { error:err }, function() { return context.finalize(function() { if (context.response) { context.response.end(); } }); }); } if (self.listeners('error').length === 0) { onError.bind(self)(context, err, function () { if (context == null) { return; } return context.finalize(function() { if (context.response) { context.response.end(); } }); }); } else { //raise application error event return self.emit('error', { context:context, error:err }, function() { if (context == null) { return; } context.finalize(function() { if (context.response) { context.response.end(); } }); }); } } else { if (context == null) { return; } return context.finalize(function() { if (context.response) { context.response.end(); } }); } }); }); /** * @name HttpApplication#getServer * @type {Function} * @returns {Server|*} */ self.getServer = function() { return server_; }; //start listening server_.listen(opts.port, opts.bind); TraceUtils.log('Web application is running at http://%s:%s/', opts.bind, opts.port); //do callback callback.call(self); } catch (err) { TraceUtils.error(err); } } /** * @param {ApplicationOptions|*=} options * @param {Function=} callback */ HttpApplication.prototype.start = function (options, callback) { callback = callback || function() { }; options = options || { }; if (options.cluster) { var clusters = 1; //check if options.cluster="auto" if (/^auto$/i.test(options.cluster)) { clusters = require('os').cpus().length; } else { //get cluster number clusters = LangUtils.parseInt(options.cluster); } if (clusters>1) { var cluster = require('cluster'); if (cluster.isMaster) { //get debug argument (if any) var debug = process.execArgv.filter(function(x) { return /^--debug(-brk)?=\d+$/.test(x); })[0], debugPort; if (debug) { //get debug port debugPort = parseInt(/^--debug(-brk)?=(\d+)$/.exec(debug)[2]); cluster.setupMaster({ execArgv: process.execArgv.filter(function(x) { return !/^--debug(-brk)?=\d+$/.test(x); }) }); } for (var i = 0; i < clusters; i++) { if (debug) { if (/^--debug-brk=/.test(debug)) cluster.settings.execArgv.push('--debug-brk=' + (debugPort + i)); else cluster.settings.execArgv.push('--debug=' + (debugPort + i)); } cluster.fork(); if (debug) cluster.settings.execArgv.pop(); } } else { startInternal.bind(this)(options, callback); } } else { startInternal.bind(this)(options, callback); } } else { startInternal.bind(this)(options, callback); } }; /** * Registers HttpApplication as express framework middleware */ HttpApplication.prototype.runtime = function() { var self = this; function nextError(context, err) { //handle context error event if (context.listeners('error').length > 0) { return context.emit('error', { error:err }, function() { context.finalize(function() { if (context.response) { context.response.end(); } }); }); } if (self.listeners('error').length === 0) { onError.bind(self)(context, err, function () { if (context == null) { return; } context.finalize(function() { if (context.response) { context.response.end(); } }); }); } else { //raise application error event self.emit('error', { context:context, error:err }, function() { if (context == null) { return; } context.finalize(function() { if (context.response) { context.response.end(); } }); }); } } return function runtimeParser(req, res, next) { //create context var context = self.createContext(req,res); context.request.on('close', function() { //finalize data context if (_.isObject(context)) { context.finalize(function() { if (context.response) { //if response is alive if (context.response.finished === false) { //end response context.response.end(); } } }); } }); //process request self.processRequest(context, function(err) { if (err) { if (typeof next === 'function') { return context.finalize(function() { return next(err); }); } return nextError(context, err); } return context.finalize(function() { context.response.end(); }); }); }; }; /** * Registers an application controller * @param {string} name * @param {Function|HttpControllerConfiguration} controllerCtor * @returns HttpApplication */ HttpApplication.prototype.useController = function(name, controllerCtor) { Args.notString(name,"Controller Name"); Args.notFunction(controllerCtor,"Controller constructor"); //get application controllers or default var controllers = this.getConfiguration().getSourceAt('controllers') || { }; //set application controller controllers[name] = controllerCtor; if (typeof controllerCtor.configure === 'function') { controllerCtor.configure(this); } //apply changes this.getConfiguration().setSourceAt('controllers', controllers); return this; }; /** * Registers an application strategy e.g. an singleton service which to be used in application contextr * @param {Function} serviceCtor * @param {Function} strategyCtor * @returns HttpApplication */ HttpApplication.prototype.useStrategy = function(serviceCtor, strategyCtor) { Args.notFunction(strategyCtor,"Service constructor"); Args.notFunction(strategyCtor,"Strategy constructor"); this[servicesProperty][serviceCtor.name] = new strategyCtor(this); return this; }; /** * Register a service type in application services * @param {Function} serviceCtor * @returns HttpApplication */ HttpApplication.prototype.useService = function(serviceCtor) { Args.notFunction(serviceCtor,"Service constructor"); this[servicesProperty][serviceCtor.name] = new serviceCtor(this); return this; }; /** * @param {Function} serviceCtor * @returns {boolean} */ HttpApplication.prototype.hasStrategy = function(serviceCtor) { Args.notFunction(serviceCtor,"Service constructor"); return this[servicesProperty].hasOwnProperty(serviceCtor.name); }; /** * @param {Function} serviceCtor * @returns {boolean} */ HttpApplication.prototype.hasService = function(serviceCtor) { Args.notFunction(serviceCtor,"Service constructor"); return this[servicesProperty].hasOwnProperty(serviceCtor.name); }; /** * Gets an application strategy based on the given base service type * @param {Function} serviceCtor * @return {*} */ HttpApplication.prototype.getStrategy = function(serviceCtor) { Args.notFunction(serviceCtor,"Service constructor"); return this[servicesProperty][serviceCtor.name]; }; /** * Gets an application service based on the given base service type * @param {Function} serviceCtor * @return {*} */ HttpApplication.prototype.getService = function(serviceCtor) { Args.notFunction(serviceCtor,"Service constructor"); return this[servicesProperty][serviceCtor.name]; }; /** * @param {HttpApplication} application * @returns {{html: Function, text: Function, json: Function, unauthorized: Function}} * @private */ function httpApplicationErrors(application) { var self = application; return { html: function(context, error, callback) { callback = callback || function () { }; if (_.isNil(error)) { return callback(); } onHtmlError(context, error, function(err) { callback.call(self, err); }); }, text: function(context, error, callback) { callback = callback || function () { }; if (_.isNil(error)) { return callback(); } /** * @type {ServerResponse} */ var response = context.response; if (error) { //send plain text response.writeHead(error.statusCode || 500, {"Content-Type": "text/plain"}); //if error is an HTTP Exception if (error instanceof HttpError) { response.write(error.statusCode + ' ' + error.message + "\n"); } else { //otherwise send status 500 response.write('500 ' + error.message + "\n"); } //send extra data (on development) if (process.env.NODE_ENV === 'development') { if (!_.isEmpty(error.innerMessage)) { response.write(error.innerMessage + "\n"); } if (!_.isEmpty(error.stack)) { response.write(error.stack + "\n"); } } } return callback.bind(self)(); }, json: function(context, error, callback) { callback = callback || function () { }; if (_.isNil(error)) { return callback(); } context.request.headers = context.request.headers || { }; if (/application\/json/g.test(context.request.headers.accept) || (context.format === 'json')) { var result; if (error instanceof HttpError) { result = new mvc.HttpJsonResult(error); result.responseStatus = error.statusCode; } else if (process.env.NODE_ENV === 'development') { result = new mvc.HttpJsonResult(error); result.responseStatus = error.statusCode || 500; } else { result = new mvc.HttpJsonResult(new HttpServerError()); result.responseStatus = 500; } //execute redirect result return result.execute(context, function(err) { return callback(err); }); } //go to next error if any callback.bind(self)(error); }, unauthorized: function(context, error, callback) { callback = callback || function () { }; if (_.isNil(error)) { return callback(); } if (_.isNil(context)) { return callback.call(self); } if (error.statusCode !== 401) { //go to next error if any return callback.call(self, error); } context.request.headers = context.request.headers || { }; if (/text\/html/g.test(context.request.headers.accept)) { if (self.config.settings) { if (self.config.settings.auth) { //get login page from configuration var page = self.config.settings.auth.loginPage || '/login.html'; //prepare redirect result var result = new mvc.HttpRedirectResult(page.concat('?returnUrl=', encodeURIComponent(context.request.url))); //execute redirect result result.execute(context, function(err) { callback.call(self, err); }); return; }