Skip to content

Commit

Permalink
Add config to allow cors for specific origins (#26)
Browse files Browse the repository at this point in the history
The default node version was also updated to 10.12.0.
  • Loading branch information
AlphaHydrae committed Mar 27, 2019
1 parent 999e43f commit 4466daa
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 11 deletions.
2 changes: 1 addition & 1 deletion .node-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
10.1.0
10.12.0
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ configuration file.
| :--- | :--- | :--- | :--- |
| `$BCRYPT_COST` | `bcryptCost` | 10 | bcrypt cost parameter (should be at least 10; see [bcrypt][bcrypt]) |
| `$CONFIG` | | `config/local.js` | Path to the local configuration file to load |
| `$CORS` | `cors` | `false` | Whether to enable Cross-Origin Request Sharing (CORS) |
| `$CORS` | `cors.enabled` | `false` | Whether to enable Cross-Origin Request Sharing (CORS) |
| `$CORS_ORIGIN` | `cors.origin` | | Comma-delimited whitelist of origins that are allowed to use CORS (setting this enables CORS by default) |
| `$DATABASE_URL` | `db` | `postgres://localhost/biopocket` | PostgreSQL database URL to connect to |
| `$IMAGES_BASE_URL` | `imagesBaseUrl` | | Base URL where theme, action and task images are stored. |
| `$INTERFACE_DATABASE_URL` | `interfaceDb` | | PostgreSQL database URL for the data collection interface database (see [Synchronization](DEVELOPMENT.md#synchronization)) |
Expand Down
19 changes: 14 additions & 5 deletions config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ const fixedConfig = {
// Configuration from environment variables
const configFromEnvironment = {
bcryptCost: parseConfigInt(getEnvVar('BCRYPT_COST')),
cors: parseConfigBoolean(getEnvVar('CORS')),
cors: {
enabled: parseConfigBoolean(getEnvVar('CORS')),
origin: getEnvVar('CORS_ORIGIN')
},
db: getDatabaseUrl(),
imagesBaseUrl: getEnvVar('IMAGES_BASE_URL'),
interfaceDb: getDatabaseUrl('INTERFACE_DATABASE_', 'biopocket_interface'),
Expand All @@ -49,15 +52,17 @@ if (localConfigFile !== joinProjectPath('config', 'local.js') && !fs.existsSync(
} else if (fs.existsSync(localConfigFile)) {
const localConfig = require(localConfigFile);
configFromLocalFile = _.pick(localConfig,
'bcryptCost', 'cors', 'db', 'defaultPaginationLimit',
'bcryptCost', 'cors.enabled', 'cors.origin', 'db', 'defaultPaginationLimit',
'docs.browser', 'docs.host', 'docs.open', 'docs.port',
'env', 'imagesBaseUrl', 'interfaceDb', 'logLevel', 'port', 'sessionSecret');
}

// Default configuration
const defaultConfig = {
bcryptCost: 10,
cors: false,
cors: {
enabled: _.get(configFromEnvironment, 'cors.origin') !== undefined || _.get(configFromLocalFile, 'cors.origin') !== undefined
},
db: 'postgres://localhost/biopocket',
defaultPaginationLimit: 100,
docs: {
Expand Down Expand Up @@ -280,8 +285,12 @@ function parseConfigInt(value, defaultValue) {
function validate(conf) {
if (!_.isInteger(conf.bcryptCost) || conf.bcryptCost < 1) {
throw new Error(`Unsupported bcrypt cost "${conf.bcryptCost}" (type ${typeof conf.bcryptCost}); must be an integer greater than or equal to 1`);
} else if (!_.isBoolean(conf.cors)) {
throw new Error(`Unsupported CORS value "${conf.cors}" (type ${typeof conf.cors}); must be a boolean`);
} else if (!_.isPlainObject(conf.cors)) {
throw new Error(`Unsupported CORS value "${conf.cors}" (type ${typeof conf.cors}); must be an object`);
} else if (!_.isBoolean(conf.cors.enabled)) {
throw new Error(`Unsupported CORS enabled value "${conf.cors.enabled}" (type ${typeof conf.cors.enabled}); must be a boolean`);
} else if (config.cors.origin !== undefined && !_.isString(conf.cors.origin)) {
throw new Error(`Unsupported CORS origin value "${conf.cors.origin}" (type ${typeof conf.cors.origin}); must be a string`);
} else if (!_.isString(conf.db) || !conf.db.match(/^postgres:\/\//)) {
throw new Error(`Unsupported database URL "${conf.db}" (type ${typeof conf.db}); must be a string starting with "postgres://"`);
} else if (!_.isInteger(conf.defaultPaginationLimit) || conf.defaultPaginationLimit < 1) {
Expand Down
6 changes: 5 additions & 1 deletion config/local.sample.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// Cross-Origin Resource Sharing (CORS)
// It is disabled by default.
exports.cors = false;
exports.cors = {
enabled: false
// Origin whitelist (comma-separated list).
// origin: 'http://example.com,http://api.example.com'
};

// Database URL
// The full format is "postgres://username:password@host:port/dbname"
Expand Down
6 changes: 3 additions & 3 deletions server/app.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
const bodyParser = require('body-parser');
const cors = require('cors');
const express = require('express');

const config = require('../config');
const api = require('./api');
const db = require('./db');
const cors = require('./utils/cors');
const { logger: expressLogger } = require('./utils/express');

const logger = config.logger('app');
Expand All @@ -17,8 +17,8 @@ app.set('env', config.env);
app.use(expressLogger);
app.use(bodyParser.json());

logger.debug(`CORS is ${config.cors ? 'enabled' : 'disabled'} (change with $CORS or config.cors)`);
if (config.cors) {
logger.debug(`CORS is ${config.cors.enabled ? 'enabled' : 'disabled'} (change with $CORS or config.cors.enabled)`);
if (config.cors.enabled) {
app.use(cors());
}

Expand Down
64 changes: 64 additions & 0 deletions server/utils/cors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const cors = require('cors');
const { merge } = require('lodash');

const config = require('../../config');

let cachedOriginWhitelist;

module.exports = options => cors(corsOptionsDelegateFactory(options));

/**
* Returns an asynchronous function that can be used as a delegate to
* configure the CORS middleware.
*
* See https://www.npmjs.com/package/cors#configuring-cors-asynchronously
*
* @param {Object} options - Options to pass to the middleware.
* @returns {Function} A CORS options delegate.
*/
function corsOptionsDelegateFactory(options) {
return function(req, callback) {
Promise
.resolve()
.then(() => getCorsOptions(req, options))
.then(result => callback(undefined, result))
.catch(callback);
};
}

/**
* Constructs options for the CORS middleware based on the request.
*
* An origin whitelist is set based on the `cors.origin` configuration property by default.
*
* @param {Request} req - The Express request object.
* @param {Object} options - Additional options (which may override the default options).
* @returns {Object} Options for the CORS middleware.
*/
function getCorsOptions(req, options) {

const corsOptions = {};

const originWhitelist = getOriginWhitelist();
if (originWhitelist) {
corsOptions.origin = originWhitelist.indexOf(req.get('Origin')) !== -1;
}

return merge(corsOptions, options);
}

/**
* Returns a whitelist of origins allowed to use CORS.
*
* The list is based on the `cors.origin` configuration property.
*
* @returns {boolean|string[]} An array of allowed origins, or false if none are allowed.
*/
function getOriginWhitelist() {
if (cachedOriginWhitelist === undefined) {
const origin = config.cors.origin;
cachedOriginWhitelist = origin ? origin.split(',') : false;
}

return cachedOriginWhitelist;
}

0 comments on commit 4466daa

Please sign in to comment.