UNPKG

sails

Version:

API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)

609 lines (476 loc) 18.4 kB
/** * Module dependencies. */ var Readable = require('stream').Readable; var QS = require('querystring'); var _ = require('@sailshq/lodash'); var router = require('@sailshq/router'); var flaverr = require('flaverr'); var sortRouteAddresses = require('sort-route-addresses'); var buildReq = require('./req'); var buildRes = require('./res'); var defaultHandlers = require('./bindDefaultHandlers'); var detectVerb = require('../util/detect-verb'); // Private var to hold sorted route addresses var sortedRouteAddresses = []; /** * Expose new instance of `Router` * * @api private */ module.exports = function(sails) { return new Router({sails: sails}); }; /** * Initialize a new `Router` * * @param {Object} options * @api private */ function Router(options) { options = options || {}; this.sails = options.sails; this.defaultHandlers = defaultHandlers(this.sails); // Expose router on `sails` object this.sails.router = this; // Instantiate the private router as an instance of `router. this._privateRouter = router(); // Return the array of sorted route addresses, cloned for our protection. this.getSortedRouteAddresses = function() { return _.clone(sortedRouteAddresses); }; // Bind the context of all instance methods this.load = _.bind(this.load, this); this.bind = _.bind(this.bind, this); this.unbind = _.bind(this.unbind, this); this.reset = _.bind(this.reset, this); this.flush = _.bind(this.flush, this); this.route = _.bind(this.route, this); this.getActionIdentityForTarget = _.bind(this.getActionIdentityForTarget, this); } /** * _privateRouter * * This internal "private" instance of an Express app object * is used only for routing. (i.e. it will not be used for * listening to actual HTTP requests; instead, one or more * delegate servers can be attached- see the `http` or * `sockets` hooks for examples of attaching a server to * Sails) * * NOTE: Requires calling `load()` before use in order to * provide access to the proper NODE_ENV, since Express * uses that to determine its environment (development vs. * production.) */ // Router.prototype._privateRouter; /** * `sails.router.load()` * * Expose the router, create the Express private router, * then call flush(), which will bind configured routes * and emit the appropriate events. * * Note the `results, cb` signature, which is necessary * because this function is called from an async.auto() * where it has dependencies. * * @api public */ Router.prototype.load = function(results, cb) { var sails = this.sails; sails.log.silly('Loading router...'); // Maintain a reference to the static route config this.explicitRoutes = sails.config.routes; // Save reference to sails logger this.log = sails.log; var sessionSecret = sails.config.session && sails.config.session.secret; // If a session store is configured, hook it up as `req.session` by passing // it down to the session middleware if (!sails.hooks.session) { // If available, Sails uses the configured session secret for signing cookies. if (sessionSecret) { // Ensure secret is a string. This check happens in the session hook as well, // but sails.config.session.secret may still be provided even if the session hook // is turned off, so to be extra anal we'll check here as well. if (!_.isString(sessionSecret)) { return cb(new Error('If provided, sails.config.session.secret should be a string.')); } } } if (sessionSecret) { sails._privateCpMware = require('cookie-parser')(sessionSecret); } else { sails._privateCpMware = require('cookie-parser')(); } // Wipe any existing routes and bind them anew try { this.flush(); } // Catch any errors thrown by code handling the router:before and router:after events. catch(e) { return cb(e); } // Listen for requests sails.on('router:request', this.route); // Listen for unhandled errors and unmatched routes sails.on('router:request:500', this.defaultHandlers[500]); sails.on('router:request:404', this.defaultHandlers[404]); cb(); }; /** * `sails.router.route(partialReq, partialRes)` * * Interpret the specified (usually partial) request and response objects into * streams with all of the expected methods, then routes the fully-formed request * using the built-in private router. Useful for creating virtual request/response * streams from non-HTTP sources, like Socket.io or unit tests. * * This method is not always helpful-- it is not called for HTTP requests, for instance, * since the true HTTP req/res streams already exist. In that case, at lift-time, Sails * calls `router:bind`, which loads Sails' routes as normal middleware/routes in the http hook. * stack will run as usual. * * On the other hand, Socket.io needs to use this method (i.e. the `router:request` event) * to simulate a connect-style router since it can't bind dynamic routes ahead of time. * * Keep in mind that, if `route` is not used, the implementing server is responsible * for routing to Sails' default `next(foo)` handler. * * @param {Request} req * @param {Response} res * @api private */ Router.prototype.route = function(req, res) { var sails = this.sails; var _privateRouter = this._privateRouter; // If sails is `_exiting`, ignore the request. if (sails._exiting) { return; } // Provide access to SailsApp instance as `req._sails`. req._sails = req._sails || sails; // Note that, at this point, `req` and `res` are just dictionaries containing // the properties of each object that have been built up _so far_. // // Use base req and res definitions to ensure the specified // objects are at least ducktype-able as standard node HTTP // req and req streams. // // Make sure request and response objects have reasonable defaults // (will use the supplied definitions if possible) req = buildReq(req); res = buildRes(req, res); // Default to 200 status code for OPTIONS requests. // The built-in Express OPTIONS handler just calls `res.end()` (rather // than `res.send()`), so no status code gets set and our mock res.writeHead // method complains. if (req.method === 'OPTIONS' && !res.statusCode) { res.status(200); } // console.log('\n\n\n\n=======================\nReceived request to %s %s\nwith req.body:\n',req.method,req.url, req.body); // Run some basic middleware sails.log.silly('Handling virtual request :: Running virtual querystring parser...'); qsParser(req,res, function (err) { if (err) { return res.status(400).send(err && err.stack); } // Parse cookies parseCookies(req, res, function(err){ if (err) { return res.status(400).send(err && err.stack); } // console.log('Ran cookie parser'); // console.log('res.writeHead= ',res.writeHead); // Load session (if relevant) loadSession(req, res, function (err) { if (err) { return res.status(400).send(err && err.stack); } // console.log('res is now:\n',res); // console.log('\n\n'); // console.log('Ran session middleware'); // console.log('req.sessionID= ',req.sessionID); // console.log('The loaded req.session= ',req.session); sails.log.silly('Handling virtual request :: Running virtual body parser...'); bodyParser(req,res, function (err) { if (err) { return res.status(400).send(err && err.stack); } // Use our private router to route the request _privateRouter(req, res, function handleUnmatchedNext(err) { // // In the event of an unmatched `next()`, `next('foo')`, // or `next('foo', errorCode)`... // // Use the default server error handler if (err) { sails.log.silly('Handling virtual request :: Running final "error" handler...'); sails.emit('router:request:500', err, req, res); return; } // Or the default not found handler sails.log.silly('Handling virtual request :: Running final "not found" handler...'); sails.emit('router:request:404', req, res); return; }); }); }); }); }); }; /** * `sails.router.bind()` * * Bind new route(s) * * @param {String|RegExp} path * @param {String|Object|Array|Function} bindTo * @param {String} verb * @api private */ Router.prototype.bind = require('./bind'); /** * `sails.router.unbind()` * * Unbind existing route * * @param {Object} route * @api private */ Router.prototype.unbind = function(routeToRemove) { var sails = this.sails; // Inform attached servers that route should be unbound sails.emit('router:unbind', routeToRemove); // Remove any route which matches the path and verb of the argument _.remove(this._privateRouter.stack, function(layer) { return (layer.route.path === routeToRemove.path && layer.route.methods[routeToRemove.verb] === true); }); }; /** * `sails.router.reset()` * * Unbind all routes currently attached to the router * * @api private */ Router.prototype.reset = function() { var sails = this.sails; // Make sure that all the routes are deleted this._privateRouter.stack = []; // Emit reset event to allow attached servers to // unbind all of their routes as well sails.emit('router:reset'); }; /** * `sails.router.flush()` * * Unbind all current routes, then re-bind everything, re-emitting the routing * lifecycle events (e.g. `router:before` and `router:after`) * * @param {Object} routes - (optional) * If specified, replaces `this.explicitRoutes` before flushing. * * @api private */ Router.prototype.flush = function(routes) { var self = this; var sails = this.sails; // Wipe routes this.reset(); // Fired before static routes are bound sails.emit('router:before'); // If specified, replace `this.explicitRoutes` if (routes) { this.explicitRoutes = routes; } // Updated the sorted route address cache sortedRouteAddresses = sortRouteAddresses(_.keys(this.explicitRoutes)); // Iterate over each address and bind the route that the address is for. _.each(sortedRouteAddresses, function(address) { var target = self.explicitRoutes[address]; var verb = detectVerb(address).verb; // If the route address ends in a pattern var (e.g. /:id) or a wildcard (i.e. /*) // and it declares a method that could be used to request an asset, and the route // doesn't explicitly declare `skipAssets` true or false, then it should! var shouldDeclareSkipAssets = ( _.isUndefined(target.skipAssets) && (address.match(/\/\*\/?$/) || address.match(/^r\|/)) && (!verb || _.contains(['all', 'get', 'head', 'options'], verb)) ); if (shouldDeclareSkipAssets) { sails.log.warn('Warning: route `' + address + '` should explicitly declare `skipAssets: true` or `skipAssets: false` to ensure correct handling of assets!'); sails.log.warn('See http://sailsjs.com/docs/concepts/routes/url-slugs for more info.'); console.log(); } self.bind(address, target); }); // Fired after static routes are bound sails.emit('router:after'); }; /** * Given a route target configuration, return an action identity for that target. * @param {Dictionary|String} target The route target to get an action identity for * @return {String} An action identity like `user/find` */ Router.prototype.getActionIdentityForTarget = function getActionIdentityForTarget(target) { var actionIdentity; // Unwrap { target: '...' } targets. if (target && target.target) { target = target.target; } // Handle dictionary targets: // {controller: 'UserController', action: 'create'} // - or - // {action: 'user.create'} if (_.isObject(target) && !_.isArray(target) && !_.isFunction(target)) { // Attempt to handle `{controller: 'UserController', action: 'create'}` target. if (target.controller) { if (!target.action) { throw flaverr({name: 'userError', code: 'E_NOT_ACTION_TARGET'}, new Error('If `controller` is specified, `action` must be also!')); } actionIdentity = target.controller.replace('Controller', '') + '/' + target.action; } // Attempt to handle `{action: 'user.create'}` target. else if (target.action) { // Get the action identity by lowercasing the value of the `action` property. actionIdentity = target.action; } else { throw flaverr({name: 'userError', code: 'E_NOT_ACTION_TARGET'}, new Error('If target is a dictionary, it must contain an `action` property!')); } // Bail if the action contains characters other than letters, numbers, dashes and forward slashes. if (!actionIdentity.match(/^[a-zA-Z_\$]+[a-zA-Z0-9_\/\-\$]*$/)) { // If the action didn't contain weird characters, make a suggestion by removing "Controller" and // replacing dots with slashes. var didYouMean = ''; var RX_DOESNT_HAVE_ANY_WEIRD_CHARS = /[^a-zA-Z0-9.\/\-\$]/; if (!actionIdentity.match(RX_DOESNT_HAVE_ANY_WEIRD_CHARS)) { didYouMean = ' Did you mean `' + actionIdentity.replace('Controller', '').replace(/\./g,'/') + '`?'; } throw flaverr({name: 'userError', code: 'E_NOT_ACTION_TARGET'}, new Error( '\nCould not parse invalid action `' + actionIdentity + '`.' + didYouMean + '\n\n' + 'See http://sailsjs.com/docs/concepts/routes/custom-routes#?controller-action-target-syntax\n'+ 'for more info on controller/action and standalone action route syntax.\n' )); } } // Handle string targets: // 'UserController.create' // - or - // 'user.create' // - or - // 'user/create' else if (_.isString(target)) { // Normalize the action identity by removing `Controller` and replacing `.` with `/` actionIdentity = target.replace(/Controller/,'').replace(/\./g,'/'); // If the result contains anything other than letters, numbers, dashes, underscores or forward-slashes, bail. if (!actionIdentity.match(/^[a-zA-Z_\$]+[a-zA-Z0-9_\/\-\$]*$/)) { throw flaverr({name: 'userError', code: 'E_NOT_ACTION_TARGET'}, new Error( '\nCould not parse invalid action `' + target + '`.\n'+ 'See http://sailsjs.com/docs/concepts/routes/custom-routes#?controller-action-target-syntax\n'+ 'for more info on controller/action and standalone action route syntax.\n' )); } } else if (_.isArray(target)) { actionIdentity = (function(){ var actionTarget = _.find(target, function(targetComponent) { return targetComponent.action; }); if (actionTarget) { return getActionIdentityForTarget(actionTarget); } throw flaverr({name: 'userError', code: 'E_NOT_ACTION_TARGET'}, new Error('Target was an array without any items containing an action!')); })(); } else { throw flaverr({name: 'userError', code: 'E_NOT_ACTION_TARGET'}, new Error('Target must be a dictionary or string!')); } // Replace all dots with slashes, and ensure lowercase. actionIdentity = actionIdentity.replace(/\./g, '/').toLowerCase(); return actionIdentity; }; //////////////////////////////////////////////////////////////////////////////////////////////////// // // || Private functions // \/ // //////////////////////////////////////////////////////////////////////////////////////////////////// // Extremely simple query string parser (`req.query`) function qsParser(req,res,next) { var queryStringPos = req.url.indexOf('?'); if (queryStringPos !== -1) { req.query = _.merge(req.query, QS.parse(req.url.substr(queryStringPos + 1))); } else { req.query = req.query || {}; } next(); } // Extremely simple body parser (`req.body`) function bodyParser (req, res, next) { // Set up a mock `req.file()` clarifying that req.file() is not available // outside of the context of Skipper (i.e. in this case, most commonly from // socket.io virtual requests). req.file = function fileUploadsNotAvailable(){ return res.status(500).send('Streaming file uploads via `req.file()` are only available over HTTP with Skipper.'); }; var bodyBuffer=''; if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'DELETE'){ req.body = _.extend({}, req.body); return next(); } // Ensure that `req` is a readable stream at this point if ( !(req instanceof Readable) ) { return next(new Error('Sails Internal Error: `req` should be a Readable stream by the time `route()` is called')); } req.on('readable', function() { var chunk; while (null !== (chunk = req.read())) { bodyBuffer += chunk; } }); req.on('end', function() { var parsedBody; try { parsedBody = JSON.parse(bodyBuffer); } catch (unusedErr) {} // TODO -- replace _.merge() with a call to merge-dictionaries module? req.body = _.merge(req.body, parsedBody); next(); }); } /** * [parseCookies description] * @param {[type]} req [description] * @param {[type]} res [description] * @param {Function} next [description] * @return {[type]} [description] */ function parseCookies (req, res, next){ // req._sails.log.verbose('Parsing cookie:',req.headers.cookie); if (req._sails._privateCpMware) { // Run the middleware return req._sails._privateCpMware(req, res, next); } // Otherwise don't even worry about it. return next(); } /** * [loadSession description] * @param {[type]} req [description] * @param {[type]} res [description] * @param {Function} next [description] * @return {[type]} [description] */ function loadSession (req, res, next){ // If a session store is configured, and we haven't deliberately disabled // session support for this request by setting the "nosession" header, // hook up the store up as `req.session` by passing it down to the // session middleware. if (req._sails._privateSessionMiddleware && !req.headers.nosession) { // Access store preconfigured session middleware as a private property on the app instance. return req._sails._privateSessionMiddleware(req, res, next); } // Otherwise don't even worry about it. return next(); }