Skip to content
This repository has been archived by the owner on Jan 7, 2018. It is now read-only.

Commit

Permalink
Merge pull request #10 from NREL/https-requirements
Browse files Browse the repository at this point in the history
Implement new HTTPS requirement behavior
  • Loading branch information
GUI committed Mar 29, 2015
2 parents f090da8 + fce2eb5 commit f58c5ee
Show file tree
Hide file tree
Showing 9 changed files with 521 additions and 25 deletions.
5 changes: 5 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ strip_cookies:
- ^_ga$
- ^is_returning$
apiSettings:
require_https: required_return_error
rate_limits:
- duration: 1000
accuracy: 500
Expand Down Expand Up @@ -112,3 +113,7 @@ apiSettings:
status_code: 500
code: INTERNAL_SERVER_ERROR
message: An unexpected error has occurred. Try again later or contact us at {{baseUrl}}/contact for assistance
https_required:
status_code: 400
code: HTTPS_REQUIRED
message: "Requests must be made over HTTPS. Try accessing the API at: {{httpsUrl}}"
1 change: 1 addition & 0 deletions lib/gatekeeper/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module.exports.basicAuth = require('./middleware/basic_auth');
module.exports.apiKeyValidator = require('./middleware/api_key_validator');
module.exports.apiMatcher = require('./middleware/api_matcher');
module.exports.apiSettings = require('./middleware/api_settings');
module.exports.httpsRequirements = require('./middleware/https_requirements');
module.exports.roleValdiator = require('./middleware/role_validator');
module.exports.ipValidator = require('./middleware/ip_validator');
module.exports.refererValidator = require('./middleware/referer_validator');
Expand Down
109 changes: 109 additions & 0 deletions lib/gatekeeper/middleware/https_requirements.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
'use strict';

var _ = require('lodash'),
config = require('api-umbrella-config').global(),
url = require('url'),
utils = require('../utils');

var HttpsRequirements = function() {
this.initialize.apply(this, arguments);
};

_.extend(HttpsRequirements.prototype, {
initialize: function() {
this.httpsPort = config.get('https_port');
},

handleRequest: function(request, response, next) {
if(request.base.substring(0, 8).toLowerCase() === 'https://') {
// https requests are always okay, so continue.
next();
} else if(request.base.substring(0, 7).toLowerCase() !== 'http://') {
// If this isn't an http request, then we don't know how to handle it, so
// continue.
next();
} else {
var mode = request.apiUmbrellaGatekeeper.settings.require_https;
if(mode === 'optional') {
// Continue if https isn't required.
next();
} else {
this.handleHttpRequest(mode, request, response, next);
}
}
},

handleHttpRequest: function(mode, request, response, next) {
var httpUrl = request.base;
if(request.apiUmbrellaGatekeeper && request.apiUmbrellaGatekeeper.originalUrl) {
httpUrl += request.apiUmbrellaGatekeeper.originalUrl;
} else {
httpUrl += request.url;
}

var urlParts = url.parse(httpUrl);
urlParts.protocol = 'https:';
delete urlParts.port;
delete urlParts.host;
if(this.httpsPort && this.httpsPort !== 443) {
urlParts.port = this.httpsPort;
}
var httpsUrl = url.format(urlParts);

if(mode === 'transition_return_error' || mode === 'transition_return_redirect') {
var transitionStartAt = request.apiUmbrellaGatekeeper.settings.require_https_transition_start_at;
var user = request.apiUmbrellaGatekeeper.user;

// If there is no user, or the user existed prior to the HTTPS transition
// starting, then continue on, allowing http.
if(!user || !user.created_at || user.created_at < transitionStartAt) {
return next();
}
}

if(mode === 'required_return_redirect' || mode === 'transition_return_redirect') {
// Return a 301 Moved Permanently redirect for GET requests.
var statusCode = 301;

// For non-GET requests, return a 307 Temporary Redirect, which instructs
// the request to be made again with the same method (eg, a POST should
// be retried as another POST). Ideally we would return a 308 Permanent
// Redirect for permanent semantics, but that's an experimental RFC and
// some libraries do something else currently with 308s (Resume
// Incomplete): http://stackoverflow.com/q/14144664
//
// Also for general reference, see Curl's current handling of 301, 302,
// and 303s: http://curl.haxx.se/docs/manpage.html#-L
if(request.method !== 'GET') {
statusCode = 307;
}

var headers = {
'Access-Control-Allow-Origin': '*',
'Location': httpsUrl,
};

var body;
if(request.method !== 'HEAD') {
body = 'Redirecting to ' + httpsUrl;
headers['Content-Type'] = 'text/plain';
headers['Content-Length'] = Buffer.byteLength(body);
}

response.writeHead(statusCode, headers);
response.end(body);
} else {
utils.errorHandler(request, response, 'https_required', {
httpsUrl: httpsUrl,
});
}
},
});

module.exports = function httpsRequirements(proxy) {
var middleware = new HttpsRequirements(proxy);

return function(request, response, next) {
middleware.handleRequest(request, response, next);
};
};
16 changes: 12 additions & 4 deletions lib/gatekeeper/utils.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
'use strict';

var cloneDeep = require('clone'),
var _ = require('lodash'),
config = require('api-umbrella-config').global(),
csv = require('csv-string'),
handlebars = require('handlebars'),
logger = require('../logger'),
mime = require('mime'),
Negotiator = require('negotiator'),
url = require('url');

exports.errorHandler = function(request, response, error) {
exports.errorHandler = function(request, response, error, data) {
var availableMediaTypes = ['application/json', 'application/xml', 'text/csv', 'text/html'];

// Prefer the format from the extension given in the URL.
Expand Down Expand Up @@ -43,15 +44,22 @@ exports.errorHandler = function(request, response, error) {
// introduce in multi-line templates and XML doesn't like if there's any
// leading space before the XML declaration.
var templateContent = settings.error_templates[format].replace(/^\s+|\s+$/g, '');
var data = cloneDeep(settings.error_data[error] || settings.error_data.internal_server_error);
var errorData = settings.error_data[error];
if(!errorData) {
errorData = settings.error_data.internal_server_error;
logger.error({ error_type: error }, 'Error data not found for error type: ' + error);
}
data = _.merge({
baseUrl: request.base,
}, data || {}, errorData);

var prop;
for(prop in data) {
try {
// TODO: Templates should be precompiled. Only compile them when the
// configuration is read-in or changed.
var valueTemplate = handlebars.compile(data[prop], { noEscape: true });
data[prop] = valueTemplate({ baseUrl: request.base });
data[prop] = valueTemplate(data);
} catch(e) {
}
}
Expand Down
1 change: 1 addition & 0 deletions lib/gatekeeper/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ _.extend(Worker.prototype, {
middleware.apiMatcher(),
middleware.apiSettings(),
middleware.apiKeyValidator(this),
middleware.httpsRequirements(),
middleware.roleValdiator(this),
middleware.ipValidator(this),
middleware.refererValidator(this),
Expand Down
1 change: 1 addition & 0 deletions lib/models/api_user_schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module.exports = function(mongoose) {
type: String,
index: { unique: true },
},
created_at: Date,
first_name: String,
last_name: String,
email: String,
Expand Down
2 changes: 2 additions & 0 deletions test/config/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ apis:
url_matches:
- frontend_prefix: /
backend_prefix: /
apiSettings:
require_https: optional
Loading

0 comments on commit f58c5ee

Please sign in to comment.