Skip to content

Commit

Permalink
Core documentation and cleanup in CORS hook.
Browse files Browse the repository at this point in the history
  • Loading branch information
mikermcneil committed Jan 31, 2016
1 parent ab81886 commit e0b54da
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 129 deletions.
35 changes: 9 additions & 26 deletions lib/hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
64 changes: 64 additions & 0 deletions lib/hooks/cors/README.md
Original file line number Diff line number Diff line change
@@ -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!)
16 changes: 16 additions & 0 deletions lib/hooks/cors/clear-headers.js
Original file line number Diff line number Diff line change
@@ -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();

};
117 changes: 25 additions & 92 deletions lib/hooks/cors/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,26 @@ module.exports = function(sails) {
*/

var _ = require('lodash');
var clearHeaders = require('./clear-headers');
var prepareSendHeaders = require('./to-prepare-send-headers')(sails);


/**
* Expose hook definition
*/

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: '*',
Expand All @@ -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 {
Expand All @@ -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);
}
}

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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();

}
}//</initialize>

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();

}
};

};
Loading

0 comments on commit e0b54da

Please sign in to comment.