/**
* MOST Web Framework
* A JavaScript Web Framework
* http://themost.io
*
* Copyright (c) 2014, Kyriakos Barbounakis k.barbounakis@gmail.com, Anthi Oikonomou anthioikonomou@gmail.com
*
* Released under the BSD3-Clause license
* Date: 2014-06-10
*/
'use strict';
/**
* @private
*/
var common = require('./common'),
files = require('./files'),
mvc = require('./http-mvc'),
html = require('./html'), util = require('util'), array = require('most-array'),
async = require('async'), path = require("path"), fs = require("fs"),
url = require('url'),
http = require('http'),
da = require('most-data'),
querystring = require('querystring'),
HttpContext= require('./http-context').HttpContext,
crypto = require('crypto');
/**
* @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();
*/
function ApplicationOptions() {
}
/**
* Represents a configuration file that is applicable to an application or service.
* @constructor
*/
function ApplicationConfig() {
/**
* Gets an array of data adapters.
* @type {Array}
*/
this.adapters = [];
/**
* Gets an array of HTTP view engines configuration
* @type {Array}
*/
this.engines = [];
/**
* Gets an array of all registered MIME types
* @type {Array}
*/
this.mimes = [];
/**
* Gets an array of all registered HTTP handlers.
* @type {Array}
*/
this.handlers = [];
/**
* Gets an array of all registered HTTP routes.
* @type {Array}
*/
this.routes = [];
/**
* Gets or sets a collection of data adapter types that are going to be use in data operation
* @type {Array}
*/
this.adapterTypes = null;
/**
* Gets or sets a collection of data types that are going to be use in data operation
* @type {Array}
*/
this.dataTypes = null;
/**
* Gets or sets an object that holds application settings
* @type {Array}
*/
this.settings = { };
/**
* Gets or sets an object that holds application locales
* @type {*}
*/
this.locales = { };
}
/**
* Abstract class that represents a data context
* @constructor
*/
function HttpDataContext() {
//
}
/**
* @returns {AbstractAdapter}
*/
HttpDataContext.prototype.db = function () {
return null;
};
/**
* @param {string} name
* @returns {DataModel}
*/
HttpDataContext.prototype.model = function (name) {
return null;
};
/**
* @param {string} type
* @returns {*}
*/
HttpDataContext.prototype.dataTypes = function (type) {
return null;
};
/**
* @classdesc An abstract class that represents an HTTP Handler
* @class HttpHandler
* @abstract
* @constructor
*/
function HttpHandler() {
//
}
/**
* @type {string[]}
* @private
*/
HttpHandler.Events = ['beginRequest', 'validateRequest', 'authenticateRequest',
'authorizeRequest', 'mapRequest', 'postMapRequest', 'preExecuteResult', 'postExecuteResult', 'endRequest'];
/**
* Occurs as the first event in the HTTP execution
* @param {HttpContext} context
* @param {Function} callback
*/
HttpHandler.prototype.beginRequest = function (context, callback) {
callback = callback || function () {
};
callback.call(context);
};
/**
* Occurs when a handler is going to validate current HTTP request.
* @param {HttpContext} context
* @param {Function} callback
*/
HttpHandler.prototype.validateRequest = function (context, callback) {
callback = callback || function () {
};
callback.call(context);
};
/**
* Occurs when a handler is going to set current user identity.
* @param {HttpContext} context
* @param {Function} callback
*/
HttpHandler.prototype.authenticateRequest = function (context, callback) {
callback = callback || function () {
};
callback.call(context);
};
/**
* Occurs when a handler has established the identity of the current user.
* @param {HttpContext} context
* @param {Function} callback
*/
/*HttpHandler.prototype.postAuthenticateRequest = function(context, callback) {
callback = callback || function() {};
callback.call(context);
};*/
/**
* Occurs when a handler has verified user authorization.
* @param {HttpContext} context
* @param {Function} callback
*/
HttpHandler.prototype.authorizeRequest = function (context, callback) {
callback = callback || function () {
};
callback.call(context);
};
/**
* Occurs when the handler is selected to respond to the request.
* @param {HttpContext} context
* @param {Function} callback
*/
HttpHandler.prototype.mapRequest = function (context, callback) {
callback = callback || function () {
};
callback.call(context);
};
/**
* Occurs when application has mapped the current request to the appropriate handler.
* @param {HttpContext} context
* @param {Function} callback
*/
HttpHandler.prototype.postMapRequest = function(context, callback) {
callback = callback || function() {};
callback.call(context);
};
/**
* Occurs just before application starts executing a handler.
* @param {HttpContext} context
* @param {Function} callback
*/
/*HttpHandler.prototype.preRequestHandlerExecute = function(context, callback) {
callback = callback || function() {};
callback.call(context);
};*/
/**
* Occurs when application starts processing current HTTP request.
* @param {HttpContext} context
* @param {Function} callback
*/
HttpHandler.prototype.processRequest = function (context, callback) {
callback = callback || function () {
};
callback.call(context);
};
/**
* Occurs when application starts executing an HTTP Result.
* @param {HttpContext} context
* @param {Function} callback
*/
HttpHandler.prototype.preExecuteResult = function (context, callback) {
callback = callback || function () {
};
callback.call(context);
};
/**
* Occurs when application was succesfully executes an HTTP Result.
* @param {HttpContext} context
* @param {Function} callback
*/
HttpHandler.prototype.postExecuteResult = function (context, callback) {
callback = callback || function () {
};
callback.call(context);
};
/**
* Occurs when the handler finishes execution.
* @param {HttpContext} context
* @param {Function} callback
*/
/*HttpHandler.prototype.postRequestHandlerExecute = function(context, callback) {
callback = callback || function() {};
callback.call(context);
};*/
/**
* Occurs as the last event in the HTTP execution
* @param {HttpContext} context
* @param {Function} callback
*/
HttpHandler.prototype.endRequest = function (context, callback) {
callback = callback || function () {
};
callback.call(context);
};
/**
* @class HttpApplication
* @constructor
* @augments EventEmitter
*/
function HttpApplication() {
this.executionPath = path.join(process.cwd(), 'app');
/**
* Gets the current application configuration path
* @type {*}
*/
this.configPath = path.join(process.cwd(), 'app');
/**
* Gets or sets application configuration settings
* @type {ApplicationConfig}
*/
this.config = null;
/**
* Gets or sets a collection of application handlers
* @type {Array}
*/
this.handlers = [];
//initialize angular server module
var ng = require('./angular-server-module');
/**
* @type {AngularServerModule}
*/
this.module = null;
//init module
ng.init(this);
//register auth service
var self = this;
self.module.service('$auth', function($context) {
try {
//ensure settings
self.config.settings.auth = self.config.settings.auth || { };
var providerPath = self.config.settings.auth.provider || './auth-service';
//get auth provider
if (providerPath.indexOf('/')==0)
providerPath = self.mapPath(providerPath);
var svc = require(providerPath);
if (typeof svc.createInstance !== 'function')
throw new Error('Invalid authentication provider module.');
return svc.createInstance($context);
}
catch (e) {
throw e;
}
});
/**
* @type {HttpCache}
*/
var $cache;
self.module.service('$cache', function() {
try {
return self.cache;
}
catch (e) {
throw e;
}
});
Object.defineProperty(self, 'cache', {
get: function () {
if (!web.common.isNullOrUndefined($cache))
return $cache;
var HttpCache = require( "./http-cache" );
/**
* @type {HttpCache|*}
*/
$cache = new HttpCache();
return $cache;
},
set: function(value) {
$cache = value;
},
configurable: false,
enumerable: false
});
/**
* 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);
}
util.inherits(HttpApplication, da.types.EventEmitter2);
/**
* Initializes application configuration.
* @return {HttpApplication}
*/
HttpApplication.prototype.init = function () {
/**
* Gets or sets application configuration settings
*/
//get node environment
var env = process.env['NODE_ENV'] || 'production', str;
//first of all try to load environment specific configuration
try {
common.log(util.format('Init: Loading environment specific configuration file (app.%s.json)', env));
str = path.join(process.cwd(), 'config', 'app.' + env + '.json');
/**
* @type {ApplicationConfig}
*/
this.config = require(str);
common.log(util.format('Init: Environment specific configuration file (app.%s.json) was succesfully loaded.', env));
}
catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
common.log(util.format('Init: Environment specific configuration file (app.%s.json) is missing.', env));
//try to load default configuration file
try {
common.log('Init: Loading environment default configuration file (app.json)');
str = path.join(process.cwd(), 'config', 'app.json');
/**
* @type {ApplicationConfig}
*/
this.config = require(str);
common.log('Init: Default configuration file (app.json) was succesfully loaded.');
}
catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
common.log('Init: An error occured while loading default configuration (app.json). Configuration cannot be found or is inaccesible.');
//load internal configuration file
/**
* @type {ApplicationConfig}
*/
this.config = require('./app.json');
this.config.settings.crypto = {
"algorithm": "aes256",
"key": common.randomHex(32)
};
common.log('Init: Internal configuration file (app.json) was succesfully loaded.');
}
else {
common.log('Init: An error occured while loading default configuration (app.json)');
throw e;
}
}
}
else {
common.log(util.format('Init: An error occured while loading application specific configuration (app).', env));
throw e;
}
}
//load routes (if empty)
if (web.common.isNullOrUndefined(this.config.routes)) {
try {
this.config.routes = require(path.join(process.cwd(),'config/routes.json'));
}
catch(e) {
if (e.code === 'MODULE_NOT_FOUND') {
//load internal default route file
web.common.log('Init: Application specific routes configuration cannot be found. The default routes configuration will be loaded instead.');
this.config.routes = require('./routes.json');
}
else {
web.common.log('Init: An error occured while trying to load application routes configuration.');
throw e;
}
}
}
//load data types (if empty)
if (web.common.isNullOrUndefined(this.config.dataTypes))
{
try {
this.config.dataTypes = da.cfg.current.dataTypes;
}
catch(e) {
web.common.log('Init: An error occured while trying to load application data types configuration.');
throw e;
}
}
//set settings default
this.config.settings = this.config.settings || {};
//initialize handlers list
//important note: Applications handlers are static classes (they will be initialized once),
//so they should not hold information about http context and execution lifecycle.
var self = this;
var handlers = self.config.handlers || [], defaultApplicationConfig = require('./app.json');
//default handlers
var defaultHandlers = defaultApplicationConfig.handlers;
for (var i = 0; i < defaultHandlers.length; i++) {
(function(item) {
if (typeof handlers.filter(function(x) { return x.name === item.name; })[0] === 'undefined') {
handlers.push(item);
}
})(defaultHandlers[i]);
}
array(handlers).each(function (h) {
try {
var handlerPath = h.type;
if (handlerPath.indexOf('/')==0)
handlerPath = self.mapPath(handlerPath);
var handlerModule = require(handlerPath), handler = null;
if (handlerModule) {
if (typeof handlerModule.createInstance != 'function') {
console.log(util.format('The specified handler (%s) cannot be instantiated. The module does not export createInstance() function.', h.name));
return;
}
handler = handlerModule.createInstance();
if (handler)
self.handlers.push(handler);
}
}
catch (e) {
throw new Error(util.format('The specified handler (%s) cannot be loaded. %s', h.name, e.message));
}
});
//initialize basic directives collection
var directives = require("./angular-server-directives");
directives.apply(this);
return this;
};
/**
* Returns the path of a physical file based on a given URL.
*/
HttpApplication.prototype.mapPath = function (s) {
var uri = url.parse(s).pathname;
return path.join(this.executionPath, uri);
};
/**
* Resolves ETag header for the given file. If the specifed 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);
}
});
};
/**
* @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 (e) {
console.log(e);
callback(null, false);
}
};
/**
* @param request {String|IncomingMessage}
* */
HttpApplication.prototype.resolveMime = function (request) {
if (typeof request=== 'string') {
//get file extension
var extensionName = path.extname(request);
var arr = this.config.mimes.filter(function(x) {
return (x.extension == extensionName);
});
if (arr.length>0)
return arr[0];
return null;
}
else if (typeof request=== 'object') {
//get file extension
var extensionName = path.extname(request.url);
var arr = this.config.mimes.filter(function(x) {
return (x.extension == extensionName);
});
if (arr.length>0)
return arr[0];
return null;
}
};
/**
* Encrypts the given data
* */
HttpApplication.prototype.encypt = function (data)
{
if (typeof data === 'undefined' || data==null)
return null;
//validate settings
if (!this.config.settings.crypto)
throw new Error('Data encryption configuration section is missing. The operation cannot be completed');
if (!this.config.settings.crypto.algorithm)
throw new Error('Data encryption algorithm is missing. The operation cannot be completed');
if (!this.config.settings.crypto.key)
throw new Error('Data encryption key is missing. The operation cannot be completed');
//encrypt
var cipher = crypto.createCipher(this.config.settings.crypto.algorithm, this.config.settings.crypto.key);
return cipher.update(data, 'utf8', 'hex') + cipher.final('hex');
};
/**
* Decrypts the given data.
* */
HttpApplication.prototype.decrypt = function (data)
{
if (typeof data === 'undefined' || data==null)
return null;
//validate settings
if (!this.config.settings.crypto)
throw new Error('Data encryption configuration section is missing. The operation cannot be completed');
if (!this.config.settings.crypto.algorithm)
throw new Error('Data encryption algorithm is missing. The operation cannot be completed');
if (!this.config.settings.crypto.key)
throw new Error('Data encryption key is missing. The operation cannot be completed');
//decrypt
var decipher = crypto.createDecipher(this.config.settings.crypto.algorithm, this.config.settings.crypto.key);
return decipher.update(data, 'hex', 'utf8') + decipher.final('utf8');
};
/**
* Sets the authentication cookie that is associated with the given user.
* @param {HttpContext} context
* @param {String} username
* @param {*=} options
*/
HttpApplication.prototype.setAuthCookie = function (context, username, options)
{
var defaultOptions = { user:username, dateCreated:new Date()}, value;
if (typeof options === 'object') {
value = JSON.stringify(util._extend(options, defaultOptions));
}
else {
value = JSON.stringify(defaultOptions);
}
var settings = this.config.settings ? (this.config.settings.auth || { }) : { } ;
settings.name = settings.name || '.MAUTH';
context.response.setHeader('Set-Cookie',settings.name.concat('=', this.encypt(value)) + ';path=/');
};
/**
* Sets the authentication cookie that is associated with the given user.
* @param {HttpContext} context
* @param {String} username
*/
HttpApplication.prototype.getAuthCookie = function (context)
{
try {
var settings = this.config.settings ? (this.config.settings.auth || { }) : { } ;
settings.name = settings.name || '.MAUTH';
var cookie = context.cookie(settings.name);
if (cookie) {
return this.decrypt(cookie);
}
return null;
}
catch(e) {
console.log('GetAuthCookie failed.');
console.log(e.message);
return null;
}
};
/**
*
* @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 {
callback.call(context, new common.HttpNotFoundException());
}
}
}
});
}
});
}
});
}
});
}
});
}
});
}
};
/**
* Gets the default data context based on the current configuration
* @returns {AbstractAdapter}
*/
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 = array(this.config.adapters).firstOrDefault(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().')
}
};
/**
* Creates an instance of HttpContext class.
* @param {ClientRequest} request
* @param {ServerResponse} response
* @returns {HttpContext}
*/
HttpApplication.prototype.createContext = function (request, response) {
var httpContext = require('./http-context'),
context = httpContext.createInstance(request, response);
//set context application
context.application = this;
//set handler events
for (var i = 0; i < HttpHandler.Events.length; i++) {
var ev = HttpHandler.Events[i];
for (var j = 0; j < this.handlers.length; j++) {
var handler = this.handlers[j];
if (typeof handler[ev] === 'function') {
context.on(ev, handler[ev]);
}
}
}
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(HttpContext)} fn
*/
HttpApplication.prototype.unattended = function (fn) {
//create context
var request = createRequestInternal.call(this), context = this.createContext(request, createResponseInternal.call(this,request));
//get unattended account
/**
* @type {{unattendedExecutionAccount:string}|*}
*/
this.config.settings.auth = this.config.settings.auth || {};
var account = this.config.settings.auth.unattendedExecutionAccount;
//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(util.format('/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') {
util._extend(opts, { url:options });
}
else {
util._extend(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 hexademical 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 hexademical 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 (util.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
* @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)
util._extend(context.params, querystring.parse(request.url.substring(request.url.indexOf('?') + 1)));
//add form
if (request.form)
util._extend(context.params, request.form);
//add files
if (request.files)
util._extend(context.params, request.files);
self.processRequest(context, function (err) {
if (err) {
if (self.listeners('error').length == 0) {
self.onError(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-US,en;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 (common.isNullOrUndefined(context)) {
callback(err);
return;
}
var request = context.request, response = context.response, ejs = require('ejs');
if (common.isNullOrUndefined(request) || common.isNullOrUndefined(response)) {
callback(err);
return;
}
//HTML custom errors
if (/text\/html/g.test(request.headers.accept)) {
fs.readFile(path.join(__dirname, './http-error.html.ejs'), 'utf8', function (readErr, data) {
if (readErr) {
//log process error
common.log(readErr);
//continue error execution
callback(err);
return;
}
//compile data
var str;
try {
if (err instanceof common.HttpException) {
str = ejs.render(data, { error:err });
}
else {
var httpErr = new common.HttpException(500, null, err.message);
httpErr.stack = err.stack;
str = ejs.render(data, {error: httpErr});
}
}
catch (e) {
common.log(e);
//continue error execution
callback(err);
return;
}
//write status header
response.writeHead(err.status || 500 , { "Content-Type": "text/html" });
response.write(str);
response.end();
callback();
});
}
else {
callback(err);
}
}
catch (e) {
//log process error
web.common.log(e);
//and continue execution
callback(err);
}
}
/**
*
* @param {HttpContext} context
* @param {Error|HttpException} err
* @param {function()} callback
*/
HttpApplication.prototype.onError = function (context, err, callback) {
callback = callback || function () { };
try {
if (err instanceof Error) {
//always log error
common.log(err);
//get response object
var response = context.response, ejs = require('ejs');
if (common.isNullOrUndefined(response)) {
callback.call(this);
}
if (response._headerSent) {
callback.call(this);
return;
}
onHtmlError(context, err, function(err) {
if (err) {
//send plain text
response.writeHead(err.status || 500, {"Content-Type": "text/plain"});
//if error is an HTTP Exception
if (err instanceof common.HttpException) {
response.write(err.status + ' ' + 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 (!common.isEmptyString(err.innerMessage)) {
response.write(err.innerMessage + "\n");
}
if (!common.isEmptyString(err.stack)) {
response.write(err.stack + "\n");
}
}
}
callback.call(this);
});
}
else {
callback.call(this);
}
}
catch (e) {
common.log(e);
if (context.response) {
context.response.writeHead(500, {"Content-Type": "text/plain"});
context.response.write("500 Internal Server Error");
callback.call(this);
}
}
};
/**
* @private
* @type {string}
*/
var HTTP_SERVER_DEFAULT_BIND = '127.0.0.1';
/**
* @private
* @type {number}
*/
var HTTP_SERVER_DEFAULT_PORT = 3000;
/**
* @private
* @param {ApplicationOptions|*} options
*/
function startInternal(options) {
var self = this;
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
util._extend(opts, options);
http.createServer(function (request, response) {
var context = self.createContext(request, response);
//begin request processing
self.processRequest(context, function (err) {
if (err) {
if (self.listeners('error').length == 0) {
self.onError(context, err, function () {
if (typeof context === 'undefined' || 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 (typeof context === 'undefined' || context == null) { return; }
context.finalize(function() {
if (context.response) { context.response.end(); }
});
});
}
}
else {
if (typeof context === 'undefined' || context == null) { return; }
context.finalize(function() {
if (context.response) { context.response.end(); }
});
}
});
}).listen(opts.port, opts.bind);
web.common.log(util.format('Web application is running at http://%s:%s/', opts.bind, opts.port));
} catch (e) {
console.log(e);
}
}
/**
*
* @param {ApplicationOptions|*} options
*/
HttpApplication.prototype.start = function (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 = common.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.call(this,options);
}
}
else {
startInternal.call(this,options);
}
}
else {
startInternal.call(this,options);
}
};
/**
* @param {string} name
* @param {function=} ctor - The class constructor associated with this controller
* @returns {HttpApplication|function()}
*/
HttpApplication.prototype.service = function(name, ctor) {
if (typeof ctor === 'undefined')
return this.module.service(name);
this.module.service(name, ctor);
return this;
};
/**
* @param {string} name
* @param {function} ctor - The class constructor associated with this controller
* @returns {HttpApplication|function()}
*/
HttpApplication.prototype.directive = function(name, ctor) {
this.module.directive(name, ctor);
return this;
};
/**
* Get or sets an HTTP controller
* @param {string} name
* @param {Function|*} ctor
* @returns {*}
*/
HttpApplication.prototype.controller = function(name, ctor) {
this.config.controllers = this.config.controllers || {};
var er;
if (typeof ctor === 'undefined') {
var c = this.config.controllers[name];
if (typeof c === 'string') {
return require(c);
}
else if (typeof c === 'function') {
return c;
}
else {
er = new Error('Invalid HTTP Controller constructor. Expected string or function.'); er.code='EARG';
throw er;
}
}
//if ctor is not a function (constructor) throw invalid argument exception
if (typeof ctor !== 'function') {
er = new Error('Invalid HTTP Controller constructor. Expected function.'); er.code='EARG';
throw er;
}
//append controller to application constroller (or override an already existing controller)
this.config.controllers[name] = ctor;
return this;
};
/**
* @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 () { };
onHtmlError(context, error, function(err) {
callback.call(self, err);
});
},
text: function(context, error, callback) {
callback = callback || function () { };
/**
* @type {ServerResponse}
*/
var response = context.response;
if (error) {
//send plain text
response.writeHead(error.status || 500, {"Content-Type": "text/plain"});
//if error is an HTTP Exception
if (error instanceof common.HttpException) {
response.write(error.status + ' ' + 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 (!common.isEmptyString(error.innerMessage)) {
response.write(error.innerMessage + "\n");
}
if (!common.isEmptyString(error.stack)) {
response.write(error.stack + "\n");
}
}
}
callback.call(this);
},
json: function(context, error, callback) {
callback = callback || function () { };
context.request.headers = context.request.headers || { };
if (/application\/json/g.test(context.request.headers.accept)) {
//prepare JSON result
var result;
if ((err instanceof common.HttpException) || (typeof err.status !== 'undefined')) {
result = new mvc.HttpJsonResult({ status:error.status, code:error.code, message:error.message, innerMessage: error.innerMessage });
}
else if (process.env.NODE_ENV === 'development') {
result = new mvc.HttpJsonResult(err);
}
else {
result = new mvc.HttpJsonResult(new common.HttpServerError());
}
//execute redirect result
result.execute(context, function(err) {
callback.call(self, err);
});
return;
}
//go to next error if any
callback.call(self, error);
},
unauthorized: function(context, error, callback) {
if (common.isNullOrUndefined(context) || common.isNullOrUndefined(context)) {
return callback.call(self);
}
if (error.status != 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;
}
}
}
//go to next error if any
callback.call(self, error);
}
}
}
/**
* @module most-web
*/
var web = {
HttpApplication: HttpApplication,
HttpContext: HttpContext,
/**
* @type HttpApplication
* */
current: undefined,
/**
* Most Web Framework Express Parser
* @param {Object=} options
*/
runtime: function(options) {
var self = this;
return function runtimeParser(req, res, next) {
//create context
var ctx = self.current.createContext(req,res);
ctx.request.on('close', function() {
//client was disconnected abnormally
//finalize data context
if (typeof ctx !== 'undefined' && ctx !=null) {
ctx.finalize(function() {
if (ctx.response) {
//if response is alive
if (ctx.response.finished == false)
//end response
ctx.response.end();
}
});
}
});
//process request
self.current.processRequest(ctx, function(err) {
if (err) {
ctx.finalize(function() {
next(err);
});
}
else {
ctx.finalize(function() {
ctx.response.end();
});
}
});
};
},
/**
* Expression handler for Access Denied HTTP errors (401).
* @param {Object=} options
*/
unauthorized: function(options) {
return function(err, req, res, next)
{
try {
if (err.status==401) {
if (/text\/html/g.test(req.get('accept'))) {
if (web.current.config.settings) {
if (web.current.config.settings.auth) {
var page = web.current.config.settings.auth.loginPage || '/login.html';
res.set('Location', page.concat('?returnUrl=', encodeURIComponent(req.url)));
res.status(302).end();
return;
}
}
}
}
next(err);
}
catch(e) {
console.log(e);
next(err);
}
};
},
/**
* Expression handler for HTTP errors.
* @param {Object=} options
*/
error: function() {
return function(err, request, response, next)
{
try {
var ejs = require('ejs');
if (common.isNullOrUndefined(response) || common.isNullOrUndefined(request)) {
next(err);
}
if (!/text\/html/g.test(request.get('accept'))) {
next(err);
}
else {
if (response._headerSent) {
next(err);
return;
}
fs.readFile(path.join(__dirname, './http-error.html.ejs'), 'utf8', function (readErr, data) {
if (readErr) {
//log process error
common.log(readErr);
next(err);
return;
}
//compile data
var str;
try {
if (err instanceof common.HttpException) {
str = ejs.render(data, { error:err });
}
else {
var httpErr = new common.HttpException(500, null, err.message);
httpErr.stack = err.stack;
str = ejs.render(data, {error: httpErr});
}
}
catch (e) {
common.log(e);
next(err);
return;
}
//write status header
response.writeHead(err.status || 500 , { "Content-Type": "text/html" });
response.write(str);
response.end();
});
}
}
catch(e) {
console.log(e);
next(err);
}
};
},
/**
* @namespace
* @memberOf module:most-web
*/
controllers: {
HttpController: mvc.HttpController,
HttpBaseController: require('./base-controller'),
HttpDataController: require('./data-controller'),
HttpLookupController: require('./lookup-controller')
},
views: {
/**
* Creates an empty HTTP response.
* @returns {HttpEmptyResult}
*/
createEmptyResult: function () {
return new mvc.HttpEmptyResult();
},
/**
* Creates a basic HTTP response with the data provided
* @param s {string}
* @returns {HttpContentResult}
*/
createContentResult: function (s) {
return new mvc.HttpContentResult(s);
},
/**
* Creates a new HTTP view context that is going to be used in view controllers
* @param context {HttpContext=} - The current HTTP context
* @returns {HttpViewContext} - The newly create HTTP view context
*/
createViewContext: function (context) {
return new mvc.HttpViewContext(context);
},
/**
* Creates a JSON response with the given data
* @param data
* @returns {HttpJsonResult}
*/
createJsonResult: function (data) {
return new mvc.HttpJsonResult(data);
},
/**
* Creates a HTTP redirect to given url.
* @param url
* @returns {HttpRedirectResult}
*/
createRedirectResult: function (url) {
return new mvc.HttpRedirectResult(url);
},
/**
* Creates an XML response with the data provided.
* @param data
* @returns {HttpXmlResult}
*/
createXmlResult: function (data) {
return new mvc.HttpXmlResult(data);
},
/**
* Creates an HTML response with the data provided.
* @param data
* @returns {HttpViewResult}
*/
createViewResult: function (name, data) {
return new mvc.HttpViewResult(name, data);
},
/**
* Inherit the prototype methods from HttpController into the given class
* @param {function} ctor Constructor function which needs to inherit the HttpController
*/
inheritsController: function (ctor) {
util.inherits(ctor, mvc.HttpController);
},
HttpController: mvc.HttpController,
HttpViewContext:mvc.HttpViewContext
},
html: html,
mvc: mvc,
common: common,
files: files
};
/**
* @type HttpApplication
* @private
*/
var __current__ = null;
if (typeof global !== 'undefined' && global!=null) {
//set current application as global property (globals.application)
Object.defineProperty(global, 'application', {
get: function () {
return web.current;
},
configurable: false,
enumerable: false
});
}
Object.defineProperty(web, 'current', {
get: function () {
if (__current__ != null)
return __current__;
//instantiate HTTP application
__current__ = new HttpApplication();
//initialize current application
if (__current__.config == null)
__current__.init();
//extend current application
__current__.extend();
//and finally return it
return __current__;
},
configurable: false,
enumerable: false
});
if (typeof exports !== 'undefined') {
/**
* @see web
*/
module.exports = web;
}