diff --git a/lib/hooks/README.md b/lib/hooks/README.md index 4ef3960a9..c5ff05a97 100644 --- a/lib/hooks/README.md +++ b/lib/hooks/README.md @@ -2,46 +2,29 @@ ## Status -> ##### Stability: [2](http://nodejs.org/api/documentation.html#documentation_stability_index) - Unstable +> ##### Stability: [2](https://github.com/balderdashy/sails-docs/blob/master/contributing/stability-index.md) - Stable -The API is in the process of settling, but has not yet had sufficient real-world testing to be considered stable. -Backwards-compatibility will be maintained if reasonable. +## Purpose Most of the non-essential Sails core has been pulled into hooks already. These hooks may eventually be pulled out into separate modules, or they may continue to live in the main Sails repo (like Connect middleware). -Custom hooks in userland are functional- specifiable as dependencies (`node_modules`) or by tossing them into a folder in your project. However, this process is not currently well-documented and backwards-compatibility is not guaranteed. Please check out the source for more details. - - - -## Purpose - -Hooks were introduced to Sails as part of major refactor designed to make the framework more modular and testable. -Their primary purpose for now is to pull all but the most minimal functionality of Sails into independent modules. -Eventually, this architecture will allow for built-in hooks to be overridden, and even new hooks to be mixed-in to projects (a proper plugin system). +Hooks were introduced to Sails as part of major refactor designed to make the framework more modular and testable. Their primary purpose was originally to pull all but the most minimal functionality of Sails into independent modules. +Today, most of the non-essential Sails core are hooks. These hooks may eventually be pulled out into separate modules, or they may continue to live in the main Sails repo (like Connect middleware). -**Original Proposal:** -https://gist.github.com/mikermcneil/5746660 +This architecture has allowed for built-in hooks to be overridden or disabled, and even for new hooks to be mixed-in to projects. +This gave way to hooks becoming a proper plugin system. Nowadays, the goal of hooks is to provide an API that is flexible and powerful enough for plugin developers or folks who need to hack Sails core, but also predictable, documented, and easy to install for end users. -## Custom Hooks = Plugins? +See http://sailsjs.org/documentation/concepts/extending-sails/hooks for more information. -Sort of! The goal is to make hooks powerful, and simple to work w/ for plugin developers, but also predictable, easy to distribute and install, and documented for end users. -**The hooks API is tentative**, and it is currently going through at least one more set of changes. We are quickly approaching the point where we can call this feature "Stable", prioritize backwards compatibilty, and limit API changes. +> **For historical purposes, here is the original proposal from the v0.9 days:** +> https://gist.github.com/mikermcneil/5746660 -That said, you _can_ write and distribute a custom hook today. If you're interested in the roadmap for the plugin system, or developing a plugin yourself, consider/check out the following tools at your disposal: -+ [Custom Generators](https://github.com/balderdashy/sails/blob/v0.10/bin/generators/README.md) :: coming in v0.10, useful for extending the Sails command-line interface (Stage 1 - Experimental) -+ [Custom Adapters](https://github.com/balderdashy/sails-docs/blob/0.9/api.adapter-interface.md) :: Since v0.8, useful for adding database support, API integrations, etc. (Stage 2 - Unstable, but approaching Stage 3) -+ [`sails` Core Events](https://gist.github.com/mikermcneil/5898598) :: Since v0.9, the `sails` object is an EventEmitter. (Stage 2 - Unstable, but approaching Stage 3) -+ Custom blueprint middlewares (coming in v0.10: Stage 1 - Experimental) -+ Custom API responses (coming in v0.10: Stage 2 - Unstable) -+ Custom route-level options (since v0.9, but changing in 0.10: Stage 2 - Unstable, but approaching Stage 3) -+ Custom configuration (since v0.7) -+ Custom "shadow routes" (since v0.7, merged in hooks in v0.9) ## FAQ diff --git a/lib/hooks/cors/README.md b/lib/hooks/cors/README.md new file mode 100644 index 000000000..640fa7ea5 --- /dev/null +++ b/lib/hooks/cors/README.md @@ -0,0 +1,64 @@ +# cors (Core Hook) + + +## Status + +> ##### Stability: [2](https://github.com/balderdashy/sails-docs/blob/master/contributing/stability-index.md) - Stable + + + +## Dependencies + +In order for this hook to load, the following other hooks must have already finished loading: + +- moduleloader +- userconfig + + +## Dependents + +If this hook is disabled, in order for Sails to load, the following other core hooks must also be disabled: + +_N/A_ + + +## Purpose + +This hook's responsibilities are: + + +##### Bind shadow routes to set appropriate CORS headers + +When Sails loads, this hook binds a `router:before` listener so that it can bind routes before the router binds explicit routes. Then it binds shadow routes for the appropriate endpoints based on `sails.config.cors` (also mixing in its implicit defaults). + + + +## Implicit Defaults + +This hook sets the following implicit default configuration on `sails.config`: + + +| Property | Type | Default | +|-----------------------------------------------|:-------------:|-----------------| +| `sails.config.cors.origin` | ((string)) | `'*'` +| `sails.config.cors.credentials` | ((boolean)) | `true` +| `sails.config.cors.methods` | ((string)) | `'GET, POST, PUT, DELETE, OPTIONS, HEAD'` +| `sails.config.cors.headers` | ((string)) | `'content-type'` +| `sails.config.cors.exposeHeaders` | ((string)) | `''` _(empty string)_ +| `sails.config.cors.securityLevel` | ((number)) | `0` + + + + +## Events + +##### `hook:cors:loaded` + +Emitted when this hook has been automatically loaded by Sails core, and triggered the callback in its `initialize` function. + + + + +## FAQ + +> If you have a question that isn't covered here, please feel free to send a PR adding it to this section (even if you don't have the answer!) diff --git a/lib/hooks/cors/clear-headers.js b/lib/hooks/cors/clear-headers.js new file mode 100644 index 000000000..6621e5bad --- /dev/null +++ b/lib/hooks/cors/clear-headers.js @@ -0,0 +1,16 @@ +module.exports = function (req, res, next) { + + // If we can set headers, do so. + // (Note: This is for backwards-compatibility. `res.set()` should always exist now. + // This check can be removed in a future version of Sails- just needs tests first.) + if (res.set) { + res.set('Access-Control-Allow-Origin', ''); + res.set('Access-Control-Allow-Credentials', ''); + res.set('Access-Control-Allow-Methods', ''); + res.set('Access-Control-Allow-Headers', ''); + res.set('Access-Control-Expose-Headers', ''); + } + + next(); + +}; diff --git a/lib/hooks/cors/index.js b/lib/hooks/cors/index.js index 2bec2b12b..e2b752703 100644 --- a/lib/hooks/cors/index.js +++ b/lib/hooks/cors/index.js @@ -5,6 +5,9 @@ module.exports = function(sails) { */ var _ = require('lodash'); + var clearHeaders = require('./clear-headers'); + var prepareSendHeaders = require('./to-prepare-send-headers')(sails); + /** * Expose hook definition @@ -12,10 +15,16 @@ module.exports = function(sails) { return { + // These constants are for private use within the hook. SECURITY_LEVEL_NORMAL: 0, SECURITY_LEVEL_HIGH: 1, SECURITY_LEVEL_VERYHIGH: 2, + + /** + * Implicit defaults + * @type {Dictionary} + */ defaults: { cors: { origin: '*', @@ -28,14 +37,20 @@ module.exports = function(sails) { }, + /** + * When this hook loads... + * @param {Function} cb + */ initialize: function(cb) { + // Once it's time to bind shadow routes, get to bindin'. sails.on('router:before', function () { + // (TODO: consider changing this ^^ to `sails.after()` for consistency) // If we're setting CORS on all routes by default, set up a universal route for it here. // CORS can still be turned off for specific routes by setting "cors:false" if (sails.config.cors.allRoutes === true) { - sails.router.bind('/*', sendHeaders(), 'all', {_middlewareType: 'CORS HOOK: sendHeaders'}); + sails.router.bind('/*', prepareSendHeaders(), 'all', {_middlewareType: 'CORS HOOK: sendHeaders'}); } // Otherwise clear all the headers by default else { @@ -61,7 +76,7 @@ module.exports = function(sails) { // Use the default CORS config for this path on an OPTIONS request optionsRouteConfigs[path][verb || "default"] = sails.config.cors; if (!sails.config.cors.allRoutes) { - sails.router.bind(route, sendHeaders(), null); + sails.router.bind(route, prepareSendHeaders(), null); } } @@ -77,7 +92,7 @@ module.exports = function(sails) { // Else if cors is set to a string, use that has the origin else if (typeof config.cors === "string") { optionsRouteConfigs[path][verb || "default"] = _.extend({origin: config.cors},{methods: verb}); - sails.router.bind(route, sendHeaders({origin:config.cors}), null); + sails.router.bind(route, prepareSendHeaders({origin:config.cors}), null); } // Else if cors is an object, use that as the config @@ -88,7 +103,7 @@ module.exports = function(sails) { config.cors.methods = verb; } optionsRouteConfigs[path][verb || "default"] = config.cors; - sails.router.bind(route, sendHeaders(config.cors), null); + sails.router.bind(route, prepareSendHeaders(config.cors), null); } // Otherwise throw a warning @@ -101,10 +116,10 @@ module.exports = function(sails) { }); _.each(optionsRouteConfigs, function(config, path) { - sails.router.bind("options "+path, sendHeaders(config, true), null, {_middlewareType: 'CORS HOOK: preflight'}); + sails.router.bind("options "+path, prepareSendHeaders(config, true), null, {_middlewareType: 'CORS HOOK: preflight'}); }); - // IF SECURITY_LEVEL > "normal"--don't process requests from disallowed origins + // IF SECURITY_LEVEL > "normal"--don't process requests from disallowed origins. // // We can't just rely on the browser implementing "access-control-allow-origin" correctly; // we need to make sure that if a request is made from an origin that isn't whitelisted, @@ -124,94 +139,12 @@ module.exports = function(sails) { }); - cb(); - - } - - }; - - function sendHeaders(_routeCorsConfig, isOptionsRoute) { - - if (!_routeCorsConfig) { - _routeCorsConfig = {}; - } - var _sendHeaders = function(req, res, next) { - var routeCorsConfig; - // If this is an options route handler, pull the config to use based on the method - // that would be used in the follow-on request - if (isOptionsRoute) { - var method = (req.headers['access-control-request-method'] || '').toLowerCase() || "default"; - routeCorsConfig = _routeCorsConfig[method]; - if (routeCorsConfig == 'clear') { - return clearHeaders(req, res, next); - } - } - // Otherwise just use the config that was passed down - else { - routeCorsConfig = _routeCorsConfig; - } - // If we have an origin header... - if (req.headers && req.headers.origin) { - - // Get the allowed origins - var origins = (routeCorsConfig.origin || sails.config.cors.origin).split(','); - - // Match the origin of the request against the allowed origins - var foundOrigin = false; - _.every(origins, function(origin) { - - origin = origin.trim(); - // If we find a whitelisted origin, send the Access-Control-Allow-Origin header - // to greenlight the request. - if (origin == req.headers.origin || origin == "*") { - res.set('Access-Control-Allow-Origin', req.headers.origin); - foundOrigin = true; - return false; - } - return true; - }); - - if (!foundOrigin) { - // For HTTP requests, set the Access-Control-Allow-Origin header to '', which the browser will - // interpret as, "no way Jose." - res.set('Access-Control-Allow-Origin', ''); - } - - // Determine whether or not to allow cookies to be passed cross-origin - res.set('Access-Control-Allow-Credentials', !_.isUndefined(routeCorsConfig.credentials) ? routeCorsConfig.credentials : sails.config.cors.credentials); - - // This header lets a server whitelist headers that browsers are allowed to access - res.set('Access-Control-Expose-Headers', !_.isUndefined(routeCorsConfig.exposeHeaders) ? routeCorsConfig.exposeHeaders : sails.config.cors.exposeHeaders); - - // Handle preflight requests - if (req.method == "OPTIONS") { - res.set('Access-Control-Allow-Methods', !_.isUndefined(routeCorsConfig.methods) ? routeCorsConfig.methods : sails.config.cors.methods); - res.set('Access-Control-Allow-Headers', !_.isUndefined(routeCorsConfig.headers) ? routeCorsConfig.headers : sails.config.cors.headers); - } - - } - - next(); - - }; - _sendHeaders._middlewareType = "CORS HOOK: sendHeaders"; - return _sendHeaders; + // Continue loading this Sails app. + return cb(); - } + }// - function clearHeaders(req, res, next) { - // If we can set headers (i.e. it's not a socket request), do so. - if (res.set) { - res.set('Access-Control-Allow-Origin', ''); - res.set('Access-Control-Allow-Credentials', ''); - res.set('Access-Control-Allow-Methods', ''); - res.set('Access-Control-Allow-Headers', ''); - res.set('Access-Control-Expose-Headers', ''); - } - - next(); - - } + }; }; diff --git a/lib/hooks/cors/to-prepare-send-headers.js b/lib/hooks/cors/to-prepare-send-headers.js new file mode 100644 index 000000000..f4d9c3d1f --- /dev/null +++ b/lib/hooks/cors/to-prepare-send-headers.js @@ -0,0 +1,99 @@ +/** + * Module dependencies. + */ + +var _ = require('lodash'); + + +/** + * toPrepareSendHeaders() + * + * @param {SailsApp} sails + * + * @return {Function} + * not-yet-configured middleware ("protoware") that can be called to get req/res/next mware function + */ +module.exports = function (sails) { + + + /** + * @optional {Dictionary} _routeCorsConfig + * + * @optional {Boolean} isOptionsRoute + * if set, use the `access-control-request-method` header + * as the method when looking up the route's CORS configuration + * + * @return {Function} + * A configured middleware function which sets the appropriate headers. + */ + return function toPrepareSendHeaders(_routeCorsConfig, isOptionsRoute) { + + if (!_routeCorsConfig) { + _routeCorsConfig = {}; + } + var _sendHeaders = function(req, res, next) { + var routeCorsConfig; + // If this is an options route handler, pull the config to use based on the method + // that would be used in the follow-on request + if (isOptionsRoute) { + var method = (req.headers['access-control-request-method'] || '').toLowerCase() || "default"; + routeCorsConfig = _routeCorsConfig[method]; + if (routeCorsConfig == 'clear') { + return clearHeaders(req, res, next); + } + } + // Otherwise just use the config that was passed down + else { + routeCorsConfig = _routeCorsConfig; + } + // If we have an origin header... + if (req.headers && req.headers.origin) { + + // Get the allowed origins + var origins = (routeCorsConfig.origin || sails.config.cors.origin).split(','); + + // Match the origin of the request against the allowed origins + var foundOrigin = false; + _.every(origins, function(origin) { + + origin = origin.trim(); + // If we find a whitelisted origin, send the Access-Control-Allow-Origin header + // to greenlight the request. + if (origin == req.headers.origin || origin == "*") { + res.set('Access-Control-Allow-Origin', req.headers.origin); + foundOrigin = true; + return false; + } + return true; + }); + + if (!foundOrigin) { + // For HTTP requests, set the Access-Control-Allow-Origin header to '', which the browser will + // interpret as, "no way Jose." + res.set('Access-Control-Allow-Origin', ''); + } + + // Determine whether or not to allow cookies to be passed cross-origin + res.set('Access-Control-Allow-Credentials', !_.isUndefined(routeCorsConfig.credentials) ? routeCorsConfig.credentials : sails.config.cors.credentials); + + // This header lets a server whitelist headers that browsers are allowed to access + res.set('Access-Control-Expose-Headers', !_.isUndefined(routeCorsConfig.exposeHeaders) ? routeCorsConfig.exposeHeaders : sails.config.cors.exposeHeaders); + + // Handle preflight requests + if (req.method == "OPTIONS") { + res.set('Access-Control-Allow-Methods', !_.isUndefined(routeCorsConfig.methods) ? routeCorsConfig.methods : sails.config.cors.methods); + res.set('Access-Control-Allow-Headers', !_.isUndefined(routeCorsConfig.headers) ? routeCorsConfig.headers : sails.config.cors.headers); + } + + } + + next(); + + }; + + _sendHeaders._middlewareType = "CORS HOOK: sendHeaders"; + return _sendHeaders; + + } + +}; diff --git a/lib/hooks/policies/README.md b/lib/hooks/policies/README.md index e9bdc31b1..1290ca811 100644 --- a/lib/hooks/policies/README.md +++ b/lib/hooks/policies/README.md @@ -2,11 +2,7 @@ ## Status -> ##### Stability: [2](http://nodejs.org/api/documentation.html#documentation_stability_index) - Unstable -> -> The API is in the process of settling, but has not yet had sufficient real-world testing to be considered stable. -> -> Backwards-compatibility will be maintained if reasonable. +> ##### Stability: [2](https://github.com/balderdashy/sails-docs/blob/master/contributing/stability-index.md) - Stable ## Purpose diff --git a/test/fixtures/middleware.js b/test/fixtures/middleware.js index 1a695e33b..8bf2c0c86 100644 --- a/test/fixtures/middleware.js +++ b/test/fixtures/middleware.js @@ -1,12 +1,30 @@ /** * Stub middleware/handlers for use in tests. * - * @type {Object} + * @type {Dictionary} */ module.exports = { - HELLO: function (req, res) { res.send('hello world!'); }, - GOODBYE: function (req, res) { res.send('goodbye world!'); }, - HELLO_500: function (req, res) { res.send(500, 'hello world!'); }, - JSON_HELLO: function (req, res) { res.json({ hello: 'world' }); }, - SOMETHING_THAT_THROWS: function (req, res) { throw 'oops'; }, + + HELLO: function(req, res) { + return res.send('hello world!'); + }, + + GOODBYE: function(req, res) { + return res.send('goodbye world!'); + }, + + HELLO_500: function(req, res) { + return res.send(500, 'hello world!'); + }, + + JSON_HELLO: function(req, res) { + return res.json({ + hello: 'world' + }); + }, + + SOMETHING_THAT_THROWS: function(req, res) { + throw 'oops'; + }, + };