Skip to content
This repository has been archived by the owner on Apr 21, 2020. It is now read-only.

Commit

Permalink
Reorganize validations and mutators for clarity. The core pipeline of…
Browse files Browse the repository at this point in the history
… the proxy should be far more obvious now, making it easier to land features like quotas.
  • Loading branch information
ryanbreen committed Jan 15, 2015
1 parent 5aa67f3 commit f472f89
Show file tree
Hide file tree
Showing 21 changed files with 417 additions and 377 deletions.
80 changes: 37 additions & 43 deletions lib/proxy/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,8 @@ var connect = require('connect');
var httpProxy = require('http-proxy');
var util = require('util');

var oauth_params = require('./oauth_params.js');
var whitelist = require('./whitelist.js');
var authenticator = require('./authenticator.js');
var header_modifier = require('./header_modifier.js');
var ProxyKeys = require('./keys.js');

var apply_xforwarded_headers = header_modifier.applyXForwardedHeaders();
var url_parser = authenticator.urlParser();

var MAXIMUM_URL_LENGTH = 16*1024;

// Increase the maxSockets managed by this process to ensure that we can keep up with many
// concurrent connections under load. Also, node-http-proxy uses this agent to manage
// connection keep-alives. If no agent is provided, node-http-proxy will return a connection: close.
Expand Down Expand Up @@ -65,60 +56,63 @@ Proxy.prototype.start = function(cb) {
return cb(err);
}

var validate_whitelist = authenticator.whitelistValidator(this_obj);
var collect_oauth_params = oauth_params.collectOAuthParams;
var validate_oauth_params = oauth_params.oauthParamValidator(this_obj);
var validate_oauth_signature = authenticator.oauthValidator(this_obj);
var request_validator = authenticator.requestValidator(this_obj);
var modify_host_header = header_modifier.modifyHostHeaders(this_obj.config.from_port, this_obj.config.to_port);
// Validators
var oauth_param_sanity_validator = require('./validators/oauth_param_sanity_validator.js')(this_obj);
var oauth_signature_validator = require('./validators/oauth_signature_validator.js')(this_obj);
var oauth_timestamp_validator = require('./validators/oauth_timestamp_validator.js')(this_obj);
var quota_validator = require('./validators/quota_validator.js')(this_obj);
var request_sanity_validator = require('./validators/request_sanity_validator.js')(this_obj);
var url_length_validator = require('./validators/url_length_validator.js')(this_obj);
var whitelist_validator = require('./validators/whitelist_validator.js')(this_obj);

// Mutators
var form_parser = connect.urlencoded();
var forward_header_mutator = require('./mutators/forward_header_mutator.js')(this_obj);
var host_header_mutator = require('./mutators/host_header_mutator.js')(this_obj);
var oauth_param_collector = require('./mutators/oauth_param_collector.js')(this_obj);
var query_string_parser = connect.query();
var restreamer = require('./mutators/restreamer.js')({stringify:require('querystring').stringify});
var url_parser = require('./mutators/url_parser.js')(this_obj);

var restreamer = require('connect-restreamer')({stringify:require('querystring').stringify});
var proxy = httpProxy.createProxyServer({});

// The express server is wired up with a list of mutators and validators that are applied to
// each inbound request.
this_obj.server = connect.createServer(
// Test for minimum viable sanity for an inbound request. Pass in the proxy object so that
// the URI and Host header can be matched against the expected values, if provided.
request_validator,
request_sanity_validator,
// Reject request with URLs longer than 16kb
url_length_validator,
// Unpack the body of POSTs so we can use them in signatures. Note that this
// will implicitly limit POST size to 1mb. We may wish to add configuration around
// this in the future.
connect.urlencoded(),
// Reject request with URLs longer than 16kb
function(req, res, next) {
if (req.originalUrl.length < MAXIMUM_URL_LENGTH) {
return next();
}

res.writeHead(413, "URL exceeds maximum allowed length for oauth_reverse_proxy");
res.end();
},
form_parser,
// Parse query string
connect.query(),
query_string_parser,
// Parse url once so that it's available in a clean format for the oauth validator
url_parser,
// Gather the oauth params from the request
collect_oauth_params,
oauth_param_collector,
// Modify the request headers to add x-forwarded-*
apply_xforwarded_headers,
forward_header_mutator,
// Check the request against our path/verb whitelist
validate_whitelist,
whitelist_validator,
// Validate that the request is within quota
// quota_validator,
// Validate that the oauth params pass a set of viability checks (existence, version, etc)
validate_oauth_params,
oauth_param_sanity_validator,
// Validate that the timestamp of the request is legal
oauth_timestamp_validator,
// Perform the oauth signature validation
validate_oauth_signature,
oauth_signature_validator,
// Update the host header
modify_host_header,
host_header_mutator,
// Since connect messes with the input parameters and we want to pass them through
// unadulterated to the target, we need to add restreamer to the chain. But we only
// need to do this if we're given a formencoded request.
function(req, res, next) {
if (req.headers && req.headers['content-type'] &&
req.headers['content-type'].indexOf('application/x-www-form-urlencoded') === 0) {
// Reconstitute form body only if necessary.
restreamer(req, res, next);
} else {
next();
}
},
restreamer,
// Whew. After all of that, we're ready to proxy the request.
function(req, res) {
// Proxy a web request to the target port on localhost using the provided agent.
// If no agent is provided, node-http-proxy will return a connection: close.
Expand Down
33 changes: 0 additions & 33 deletions lib/proxy/messages.js

This file was deleted.

18 changes: 18 additions & 0 deletions lib/proxy/messages/bad_request.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Utility method for returning a bad request failure message.
*/
module.exports = function(logger, req, res, message) {
if (req && req.headers) {
logger.info("Rejecting %s %s%s, error %s", req.method, req.headers.host, req.url, message);
} else {
logger.warn('Rejecting malformed request');
}

process.nextTick(function() {
res.writeHead(400, message);
res.end();
});

// Return false from this method so it can signal to a calling function that the request failed.
return false;
};
13 changes: 13 additions & 0 deletions lib/proxy/messages/unauthorized.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Utility method for returning an authentication failure message.
*/
module.exports = function(logger, req, res, message) {
logger.info("Rejecting %s %s%s, error %s", req.method, req.headers.host, req.url, message);
process.nextTick(function() {
res.writeHead(401, message);
res.end();
});

// Return false from this method so it can signal to a calling function that the request failed.
return false;
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
var uuid = require('node-uuid');

// Return the port, either by parsing the host header or by determining whether oauth_reverse_proxy did ssl
// termination for the request.
function getPortForRequest(req) {
Expand Down Expand Up @@ -29,7 +27,7 @@ var DEFAULT_VIA_HEADER = '1.1 localhost (oauth_reverse_proxy v' + OAUTH_REVERSE_
// Append x-forwarded-for, x-forwarded-port, x-forwarded-proto, x-forwarded-host, and via to the request
// headers before proxying to the target server. If these headers are already set, append information
// received by oauth_reverse_proxy to the existing headers.
exports.applyXForwardedHeaders = function() {
module.exports = function() {
return function(req, res, next) {

var values = {
Expand All @@ -48,24 +46,4 @@ exports.applyXForwardedHeaders = function() {

next();
};
};

// Adjust the host header to reflect the proxy port.
exports.modifyHostHeaders = function(from_port, to_port) {

// The new host header should take the form :to_port unless to_port is 443 or 80.
var new_port = (to_port === 443 || to_port === 80) ? '' : ':' + to_port;

return function(req, res, next) {
var host = req.headers['host'];

// If the existing host header includes a port, replace it. Otherwise, append the value of new_port
// to the existing host header.
var idx = host.indexOf(':');
req.headers['host'] = idx !== -1 ?
host.substring(0, idx) + new_port :
host + new_port;

next();
};
};
};
22 changes: 22 additions & 0 deletions lib/proxy/mutators/host_header_mutator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Adjust the host header to reflect the proxy port.
module.exports = function(proxy) {

var from_port = proxy.config.from_port;
var to_port = proxy.config.to_port;

// The new host header should take the form :to_port unless to_port is 443 or 80.
var new_port = (to_port === 443 || to_port === 80) ? '' : ':' + to_port;

return function(req, res, next) {
var host = req.headers['host'];

// If the existing host header includes a port, replace it. Otherwise, append the value of new_port
// to the existing host header.
var idx = host.indexOf(':');
req.headers['host'] = idx !== -1 ?
host.substring(0, idx) + new_port :
host + new_port;

next();
};
};
96 changes: 96 additions & 0 deletions lib/proxy/mutators/oauth_param_collector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
var encoding = require('../../encoding.js');

var oauth_constants = require('../oauth_constants.js');

/**
* This mutator is responsible for scouring an inbound request and gathering OAuth parameters. These
* parameters are cached in the request in an efficient format for processing by validators later in
* the pipeline.
*/

/**
* Query strings and post contents might be parsed by connect as arrays if there are name
* collisions. Unpack these into individual entries in the argument_pairs array.
*/
function safelyAddValues(argument_pairs, name, value) {
value = value || /* istanbul ignore next */ "";
if (Array.isArray(value)) {
for (var i=0; i<value.length; ++i) {
safelyAddValues(argument_pairs, name, value[i]);
}
} else {
argument_pairs.push([name, encoding.encodeData(value)]);
}
}

/**
* Loop over the collection and add entries to oauth_params and/or argument_pairs.
*/
function collectParams(req, collection) {

for (var param_name in collection) {
// Ignore realm. It is not used in signing.
if (param_name === 'realm') {
continue;
}
// For any parameter other than the oauth signature, add it to the argument pairs array. This array
// is used for signing, and we don't want to re-sign the signature.
if (param_name !== oauth_constants.OAUTH_SIGNATURE) {
safelyAddValues(req.argument_pairs, param_name, collection[param_name]);
}
// If the parameter is an oauth param, track it in the oauth_params object for easy lookup.
if (param_name.indexOf('oauth_') === 0) {
req.oauth_params[param_name] = encoding.encodeData(collection[param_name]);
}
}
}

/**
* Go through the auth headers, the query params, and the post body (if applicable) to build the
* set of values that form the signature.
*/
module.exports = function() {
return function(req, res, next) {
req.oauth_params = {};
req.argument_pairs = [];
if (req.headers.authorization) {
// If the OAuth creds are provided as an auth header, enumerate them here and add them to the
// list of things we need to sign.
var auth_header = req.headers.authorization;
auth_header = auth_header.substring(6);

var auth_header_parts = auth_header.split(/[=,\,,"]/);

// Terminate this loop early if the list of header parts isn't cleanly divisble by 4. This can happen
// if a client sends us parameters with a trailing comma, and we don't want to fail to authenticate
// those clients due to an exception accessing auth_header_parts[i+2] even if those clients suck.
for (var i=0; i < auth_header_parts.length-2; i+=4) {
var param_name = auth_header_parts[i].trim();
if (param_name === 'realm') {
continue;
}
var param_value = auth_header_parts[i+2].trim();
// For any auth header param other than the oauth signature, add it to the argument pairs array. This array
// is used for signing, and we don't want to re-sign the signature.
if (param_name !== oauth_constants.OAUTH_SIGNATURE) {
req.argument_pairs.push([param_name, param_value]);
}

// Add all non-realm and non oauth_signature parameters from the auth header to our oauth_params object
// for easy lookup.
req.oauth_params[param_name] = param_value;
}
}

// Add query params
collectParams(req, req.query);

// Add POST body params. This will noop if the request is not a POST or is not form urlencoded.
// If the parameter is an oauth param, track it in the oauth_params object for easy lookup. Note that
// it's seriously non-standard to send oauth parameters in the POST body, but DotNetOpenAuth does it
// in some circumstances.
collectParams(req, req.body);

next();
}
};
13 changes: 13 additions & 0 deletions lib/proxy/mutators/restreamer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
var connect_restreamer = require('connect-restreamer')({stringify:require('querystring').stringify});

module.exports = function() {
return function(req, res, next) {
if (req.headers && req.headers['content-type'] &&
req.headers['content-type'].indexOf('application/x-www-form-urlencoded') === 0) {
// Reconstitute form body only if necessary.
connect_restreamer(req, res, next);
} else {
next();
}
};
}
9 changes: 9 additions & 0 deletions lib/proxy/mutators/url_parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Parse the input url once and cache the parsed value in the request object.
*/
module.exports = function() {
return function(req, res, next) {
req.parsed_url = require('url').parse(req.url, false);
next();
};
};

0 comments on commit f472f89

Please sign in to comment.