UNPKG

@themost/web

Version:

MOST Web Framework 2.0 - Web Server Module

652 lines (622 loc) 24.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 url = require('url'); var sprintf = require('sprintf').sprintf; var async = require('async'); var fs = require('fs'); var route = require('../http-route'); var LangUtils = require('@themost/common/utils').LangUtils; var TraceUtils = require('@themost/common/utils').TraceUtils; var HttpError = require('@themost/common/errors').HttpError; var path = require('path'); var _ = require('lodash'); var HttpConsumer = require('../consumers').HttpConsumer; var HttpResult = require('../mvc').HttpResult; var Q = require('q'); var accepts = require('accepts'); var STR_CONTROLLER_FILE = './%s-controller.js'; var STR_CONTROLLER_RELPATH = '/controllers/%s-controller.js'; if (process.execArgv.indexOf('ts-node/register')>=0) { //change controller resolution to typescript STR_CONTROLLER_FILE = './%s-controller.ts'; STR_CONTROLLER_RELPATH = '/controllers/%s-controller.ts'; } /** * * @param s * @returns {*} * @private */ function _dasherize(s) { if (_.isString(s)) return _.trim(s).replace(/[_\s]+/g, '-').replace(/([A-Z])/g, '-$1').replace(/-+/g, '-').replace(/^-/,'').toLowerCase(); return s; } /** * @method dasherize * @memberOf _ */ if (typeof _.dasherize !== 'function') { _.mixin({'dasherize' : _dasherize}); } function _isPromise(f) { if (typeof f !== 'object') { return false; } return (typeof f.then === 'function') && (typeof f.catch === 'function'); } /** * @method isPromise * @memberOf _ */ if (typeof _.isPromise !== 'function') { _.mixin({'isPromise' : _isPromise}); } /** * @class * @constructor * @implements AuthorizeRequestHandler * @implements MapRequestHandler * @implements PostMapRequestHandler * @implements ProcessRequestHandler */ function ViewHandler() { // } /** * * @param ctor * @param superCtor */ Object.inherits = function (ctor, superCtor) { if (!ctor.super_) { ctor.super_ = superCtor; while (superCtor) { var superProto = superCtor.prototype; var keys = Object.keys(superProto); for (var i = 0; i < keys.length; i++) { var key = keys[i]; if (typeof ctor.prototype[key] === 'undefined') ctor.prototype[key] = superProto[key]; } superCtor = superCtor.super_; } } }; /** * * @param {string} controllerName * @param {HttpContext} context * @param {Function} callback */ ViewHandler.queryControllerClass = function(controllerName, context, callback) { if (typeof controllerName === 'undefined' || controllerName===null) { callback(); } else { //get controller class path and model (if any) var controllerPath = context.getApplication().mapPath(sprintf(STR_CONTROLLER_RELPATH, _.dasherize(controllerName))), controllerModel = context.model(controllerName); //if controller does not exists fs.exists(controllerPath, function(exists){ try { //if controller class file does not exist in /controllers/ folder if (!exists) { //try to find if current controller has a model defined if (controllerModel) { var controllerType = controllerModel.type || 'data'; if (controllerModel.hidden || controllerModel.abstract) { controllerType = 'hidden'; } //try to find controller based on the model's type in controllers folder (e.g. /library-controller.js) controllerPath = context.getApplication().mapPath(sprintf(STR_CONTROLLER_RELPATH, controllerType)); fs.exists(controllerPath, function(exists) { if (!exists) { //get controller path according to related model's type (e.g ./data-controller) controllerPath = sprintf(STR_CONTROLLER_FILE, controllerType); //if controller does not exist controllerPath = path.join(__dirname, controllerPath); fs.exists(controllerPath, function(exists) { if (!exists) callback(null, require('../controllers/base')); else callback(null, require(controllerPath)); }); } else { callback(null, require(controllerPath)); } }); } else { var ControllerCtor = context.getApplication().getConfiguration().controllers[controllerName] || require('../controllers/base'); callback(null, ControllerCtor); } } else { //return controller class callback(null, require(controllerPath)); } } catch (err) { callback(err); } }); } }; ViewHandler.RestrictedLocations = [ { "path":"^/controllers/", "description":"Most web framework server controllers" }, { "path":"^/models/", "description":"Most web framework server models" }, { "path":"^/extensions/", "description":"Most web framework server extensions" }, { "path":"^/handlers/", "description":"Most web framework server handlers" }, { "path":"^/views/", "description":"Most web framework server views" } ]; ViewHandler.prototype.authorizeRequest = function (context, callback) { try { var uri = url.parse(context.request.url); for (var i = 0; i < ViewHandler.RestrictedLocations.length; i++) { /** * @type {*|LocationSetting} */ var location = ViewHandler.RestrictedLocations[i], /** * @type {RegExp} */ re = new RegExp(location.path,'ig'); if (re.test(uri.pathname)) { callback(new HttpError(403, 'Forbidden')); return; } } callback(); } catch(e) { callback(e); } }; /** * @param {HttpContext} context * @param {Function} callback */ ViewHandler.validateMediaType = function(context, callback) { if (typeof context === 'undefined' || context === null) { return callback(); } //validate mime type and route format let accept = accepts(context.request); if (context.request.route && context.request.route.format) { if (accept.type(context.request.route.format)) { return callback(); } return callback(new HttpError(415)); } return callback(); }; /** * @param {HttpContext} context * @param {Function} callback */ ViewHandler.prototype.mapRequest = function (context, callback) { callback = callback || function () { }; //try to map request try { //first of all check if a request handler is already defined if (typeof context.request.currentHandler !== 'undefined') { //do nothing (exit mapping) return callback(); } var requestUri = url.parse(context.request.url); /** * find route by querying application routes * @type {HttpRoute} */ var currentRoute = queryRoute(requestUri, context); if (typeof currentRoute === 'undefined' || currentRoute === null) { return callback(); } //query controller var controllerName = currentRoute["controller"] || currentRoute.routeData["controller"] || queryController(requestUri); if (typeof controllerName === 'undefined' || controllerName === null) { return callback(); } //try to find controller class ViewHandler.queryControllerClass(controllerName, context, function(err, ControllerClass) { if (err) { return callback(err); } try { //initialize controller var controller = new ControllerClass(); //set controller's name controller.name = controllerName.toLowerCase(); //set controller's context controller.context = context; //set request handler var handler = new ViewHandler(); handler.controller = controller; context.request.currentHandler = handler; //set route data context.request.route = _.assign({ },currentRoute.route); context.request.routeData = currentRoute.routeData; //set route data as params for(var prop in currentRoute.routeData) { if (currentRoute.routeData.hasOwnProperty(prop)) { context.params[prop] = currentRoute.routeData[prop]; } } return ViewHandler.validateMediaType(context, function(err) { return callback(err); }); } catch(err) { return callback(err); } }); } catch (e) { callback(e); } }; /** * @param {HttpContext} context * @param {Function} callback */ ViewHandler.prototype.postMapRequest = function (context, callback) { try { ViewHandler.prototype.preflightRequest.call(this, context, function(err) { if (err) { return callback(err); } var obj; if (context.is('POST')) { if (context.format==='json') { if (typeof context.request.body === 'string') { //parse json data try { obj = JSON.parse(context.request.body); //set context data context.params.data = obj; } catch(err) { TraceUtils.log(err); return callback(new Error('Invalid JSON data.')); } } } } return callback(); }); } catch(e) { callback(e); } }; ViewHandler.prototype.preflightRequest = function (context, callback) { try { if (context && (context.request.currentHandler instanceof ViewHandler)) { //set the default origin (with wildcard) var allowCredentials = true, allowOrigin="*", allowHeaders = "Origin, X-Requested-With, Content-Type, Content-Language, Accept, Accept-Language, Authorization", allowMethods = "GET, OPTIONS, PUT, POST, PATCH, DELETE"; /** * @private * @type {{allowOrigin:string,allowHeaders:string,allowCredentials:Boolean,allowMethods:string,allow:string}|*} */ var route = context.request.route; if (route) { if (typeof route.allowOrigin !== 'undefined') allowOrigin = route.allowOrigin; if (typeof route.allowHeaders !== 'undefined') allowHeaders = route.allowHeaders; if (typeof route.allowCredentials !== 'undefined') allowCredentials = route.allowCredentials; if ((typeof route.allowMethods !== 'undefined') || (typeof route.allow !== 'undefined')) allowMethods = route.allow || route.allowMethods; } //ensure header names var headerNames = context.response["_headerNames"] || { }; //1. Access-Control-Allow-Origin if (typeof headerNames["access-control-allow-origin"] === 'undefined') { //if request contains origin header if (context.request.headers.origin) { if (allowOrigin === "*") { //set access-control-allow-origin header equal to request origin header context.response.setHeader("Access-Control-Allow-Origin", context.request.headers.origin); } else if (allowOrigin.indexOf(context.request.headers.origin)>-1) { context.response.setHeader("Access-Control-Allow-Origin", context.request.headers.origin); } } else { //set access-control-allow-origin header equal to the predefined origin header context.response.setHeader("Access-Control-Allow-Origin", "*"); } } //2. Access-Control-Allow-Origin if (typeof headerNames["access-control-allow-credentials"] === 'undefined') { context.response.setHeader("Access-Control-Allow-Credentials", allowCredentials); } //3. Access-Control-Allow-Headers if (typeof headerNames["access-control-allow-headers"] === 'undefined') { context.response.setHeader("Access-Control-Allow-Headers", allowHeaders); } //4. Access-Control-Allow-Methods if (typeof headerNames["access-control-allow-methods"] === 'undefined') { context.response.setHeader("Access-Control-Allow-Methods", allowMethods); } } if (typeof callback === 'undefined') { return; } return callback(); } catch(e) { if (typeof callback === 'undefined') { throw e; } callback(e); } }; /** * @param {HttpContext} context * @param {Function} callback */ ViewHandler.prototype.processRequest = function (context, callback) { var self = this; callback = callback || function () { }; try { if (context.is('OPTIONS')) { //do nothing return callback(); } //validate request controller var controller = self.controller; if (controller) { /** * try to find action * @type {String} */ var action = context.request.routeData["action"]; if (action) { //execute action var fn, useHttpMethodNamingConvention = false; if (controller.constructor['httpController']) { fn = queryControllerAction(controller, action); if (typeof fn === 'function') { useHttpMethodNamingConvention = true; } } else { fn = controller[action]; if (typeof fn !== 'function') { fn = controller[_.camelCase(action)]; } } if (typeof fn !== 'function') { fn = controller.action; } //enumerate params var functionParams = LangUtils.getFunctionParams(fn), params =[]; if (functionParams.length>0) { if (!useHttpMethodNamingConvention) { //remove last parameter (the traditional callback function) functionParams.pop(); } } //execute action handler decorators var actionConsumers = _.filter(_.keys(fn), function(x) { return (fn[x] instanceof HttpConsumer); }); return async.eachSeries(actionConsumers, function(actionConsumer, cb) { try { var source = fn[actionConsumer].run(context); if (!_.isPromise(source)) { return cb(new Error("Invalid type. Action consumer result must be a promise.")); } return source.then(function() { return cb(); }).catch(function(err) { return cb(err); }); } catch(err) { return cb(err); } }, function(err) { if (err) { return callback(err); } try { if (functionParams.length>0) { var k = 0; while (k < functionParams.length) { if (typeof context.getParam === 'function') { params.push(context.getParam(functionParams[k])); } else { params.push(context.params[functionParams[k]]); } k+=1; } } if (useHttpMethodNamingConvention) { var source = fn.apply(controller, params); //if action result is an instance of HttpResult if (source instanceof HttpResult) { //execute http result return source.execute(context, callback); } var finalSource = _.isPromise(source) ? source : Q.resolve(source); //if action result is a promise return finalSource.then(function(result) { if (result instanceof HttpResult) { //execute http result return result.execute(context, callback); } else { //convert result (any result) to an instance HttpResult if (typeof controller.result === 'function') { var httpResult = controller.result(result); //and finally execute result return httpResult.execute(context, callback); } else { return callback(new TypeError('Invalid controller prototype.')); } } }).catch(function(err) { return callback.bind(context)(err); }); } else { params.push(function (err, result) { if (err) { //throw error callback.call(context, err); } else { //execute http result return result.execute(context, callback); } }); //invoke controller method return fn.apply(controller, params); } } catch(err) { return callback(err); } }); } } else { return callback(); } } catch (error) { callback(error); } }; /** * * @param {string|*} requestUri * @param {HttpContext} context * @returns {HttpRoute} * @private */ function queryRoute(requestUri,context) { /** * @type Array * */ var routes = context.getApplication().getConfiguration().routes; //enumerate registered routes var httpRoute = route.createInstance(); for (var i = 0; i < routes.length; i++) { httpRoute.route = routes[i]; //if uri path is matched if (httpRoute.isMatch(requestUri.pathname)) { return httpRoute; } } } /** * @function * @private * @param {HttpController|*} controller * @param {string} action * @returns {boolean} */ function isValidControllerAction(controller, action) { var httpMethodDecorator = _.camelCase('http-' + controller.context.request.method); if (typeof controller[action] === 'function') { //get httpAction decorator if ((typeof controller[action].httpAction === 'undefined') || (controller[action].httpAction===action)) { //and supports current request method (see http decorators) if (controller[action][httpMethodDecorator]) { //return this action return true; } } } return false; } function getControllerPropertyNames_(obj) { if (typeof obj === 'undefined' || obj === null) { return []; } var ownPropertyNames = []; //get object methods var proto = obj; while(proto) { ownPropertyNames = ownPropertyNames.concat(Object.getOwnPropertyNames(proto).filter( function(x) { return ownPropertyNames.indexOf(x)<0; })); proto = Object.getPrototypeOf(proto); } return ownPropertyNames; } /** * @function * @private * @param {HttpController|*} controller * @param {string} action * @returns {Function} */ function queryControllerAction(controller, action) { var httpMethodDecorator = _.camelCase('http-' + controller.context.request.method), method = _.camelCase(action); var controllerPrototype = Object.getPrototypeOf(controller); var controllerPropertyNames = getControllerPropertyNames_(controllerPrototype); if (controllerPrototype) { //query controller methods that support current http request var protoActionMethods = _.filter(controllerPropertyNames, function(x) { return (typeof controller[x] === 'function') && (controller[x].httpAction === action) && controller[x][httpMethodDecorator]; }); //if an action was found for the given criteria if (protoActionMethods.length===1) { return controller[protoActionMethods[0]]; } } //if an action with the given name is a method of current controller if (isValidControllerAction(controller, action)) { return controller[action]; } //if a camel cased action with the given name is a method of current controller if (isValidControllerAction(controller, method)) { return controller[method]; } } /** * Gets the controller of the given url * @param {string|*} requestUri - A string that represents the url we want to parse. * @private * */ function queryController(requestUri) { try { if (requestUri === undefined) return null; //split path var segments = requestUri.pathname.split('/'); //put an exception for root controller //maybe this is unnecessary exception but we need to search for root controller e.g. /index.html, /about.html if (segments.length === 2) return 'root'; else //e.g /pages/about where segments are ['','pages','about'] //and the controller of course is always the second segment. return segments[1]; } catch (e) { throw e; } } if (typeof exports !== 'undefined') { module.exports.ViewHandler = ViewHandler; module.exports.createInstance = function() { return new ViewHandler(); }; }