oauth_reverse_proxy
Advanced tools
Comparing version 1.1.0 to 1.2.0
@@ -27,2 +27,9 @@ var fs = require('fs'); | ||
// Whether this proxy listens on an HTTPS socket on from_port. Defaults to false. | ||
Object.defineProperty(this_obj, 'https', { 'value': config.https != undefined || false, writable: false }); | ||
if (this_obj.https) { | ||
Object.defineProperty(this_obj, 'https_key_file', { 'value': config.https.key, writable: false }); | ||
Object.defineProperty(this_obj, 'https_cert_file', { 'value': config.https.cert, writable: false }); | ||
} | ||
// An optional object defining quotas to apply to inbound requests. | ||
@@ -33,10 +40,10 @@ Object.defineProperty(this_obj, 'quotas', { 'value': (config.quotas || {thresholds:{}}), writable: false}); | ||
var whitelist = config.whitelist || | ||
[{ | ||
path: "/livecheck", | ||
methods: [ "GET" ] | ||
}, | ||
{ | ||
path: "/healthcheck", | ||
methods: [ "GET" ] | ||
}]; | ||
[{ | ||
path: "/livecheck", | ||
methods: [ "GET" ] | ||
}, | ||
{ | ||
path: "/healthcheck", | ||
methods: [ "GET" ] | ||
}]; | ||
@@ -90,2 +97,23 @@ Object.defineProperty(this_obj, 'whitelist', { 'value': whitelist, writable: false }); | ||
if (this.https) { | ||
// Validate that the key and cert files exist | ||
if (this.https_key_file == undefined) | ||
return "no ssl key file provided"; | ||
try { | ||
fs.statSync(this.https_key_file); | ||
} catch(e) { | ||
return "https key file " + this.https_key_file + " does not exist"; | ||
} | ||
if (this.https_cert_file == undefined) | ||
return "no ssl cert file provided"; | ||
try { | ||
fs.statSync(this.https_cert_file); | ||
} catch(e) { | ||
return "https cert file " + this.https_cert_file + " does not exist"; | ||
} | ||
} | ||
var my_from_port = parseInt(this.from_port); | ||
@@ -92,0 +120,0 @@ var my_to_port = parseInt(this.to_port); |
var _ = require('underscore'); | ||
var fs = require('fs'); | ||
var http = require('http'); | ||
var https = require('https'); | ||
var connect = require('connect'); | ||
@@ -44,2 +46,78 @@ var httpProxy = require('http-proxy'); | ||
Proxy.prototype.getConnectApp = function() { | ||
var this_obj = this; | ||
// 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 proxy = httpProxy.createProxyServer({}); | ||
// Handle connection errors to the underlying service. Normal errors returned by the | ||
// service (like 404s) will get proxied through without any tampering. | ||
proxy.on('error', function(err, req, res) { | ||
this_obj.logger.info("Got error %s communicating with underlying server.", util.inspect(err)); | ||
res.writeHead(500, "Connection to " + this_obj.config.service_name + " failed"); | ||
res.end(); | ||
}); | ||
// Return an all-singing, all-dancing OAuth validating connect pipeline. | ||
return connect( | ||
// 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_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. | ||
form_parser, | ||
// Parse query string | ||
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 | ||
oauth_param_collector, | ||
// Modify the request headers to add x-forwarded-* | ||
forward_header_mutator, | ||
// Check the request against our path/verb whitelist | ||
whitelist_validator, | ||
// Validate that the oauth params pass a set of viability checks (existence, version, etc) | ||
oauth_param_sanity_validator, | ||
// Validate that the request is within quota | ||
quota_validator, | ||
// Validate that the timestamp of the request is legal | ||
oauth_timestamp_validator, | ||
// Perform the oauth signature validation | ||
oauth_signature_validator, | ||
// Update the 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. | ||
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. | ||
proxy.web(req, res, {agent: HTTP_AGENT, target: { 'host' : 'localhost', 'port' : this_obj.config.to_port }}); | ||
} | ||
); | ||
}; | ||
/** | ||
@@ -59,74 +137,6 @@ * Initialize the proxy by loading its keys and wiring up a connect pipeline to route | ||
// 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 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_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. | ||
form_parser, | ||
// Parse query string | ||
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 | ||
oauth_param_collector, | ||
// Modify the request headers to add x-forwarded-* | ||
forward_header_mutator, | ||
// Check the request against our path/verb whitelist | ||
whitelist_validator, | ||
// Validate that the oauth params pass a set of viability checks (existence, version, etc) | ||
oauth_param_sanity_validator, | ||
// Validate that the request is within quota | ||
quota_validator, | ||
// Validate that the timestamp of the request is legal | ||
oauth_timestamp_validator, | ||
// Perform the oauth signature validation | ||
oauth_signature_validator, | ||
// Update the 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. | ||
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. | ||
proxy.web(req, res, {agent: HTTP_AGENT, target: { 'host' : 'localhost', 'port' : this_obj.config.to_port }}); | ||
} | ||
); | ||
var app = this_obj.getConnectApp.apply(this_obj); | ||
// Handle connection errors to the underlying service. Normal errors returned by the | ||
// service (like 404s) will get proxied through without any tampering. | ||
proxy.on('error', function(err, req, res) { | ||
this_obj.logger.info("Got error %s communicating with underlying server.", util.inspect(err)); | ||
res.writeHead(500, "Connection to " + this_obj.config.service_name + " failed"); | ||
res.end(); | ||
}); | ||
// Start watching the key directory for changes | ||
@@ -137,5 +147,23 @@ this_obj.keystore.setupWatcher(); | ||
this_obj.logger.info("Listening on port %s", this_obj.config.from_port); | ||
this_obj.server.listen(this_obj.config.from_port, '0.0.0.0'); | ||
this_obj.server.listen(this_obj.config.from_port, '::'); | ||
// If the proxy config specifically asks for https, use https. Otherwise, use http. | ||
if (this_obj.config.https) { | ||
var ipv4_server = https.createServer({ | ||
key: fs.readFileSync(this_obj.config.https_key_file), | ||
cert: fs.readFileSync(this_obj.config.https_cert_file) | ||
}, app); | ||
var ipv6_server = https.createServer({ | ||
key: fs.readFileSync(this_obj.config.https_key_file), | ||
cert: fs.readFileSync(this_obj.config.https_cert_file) | ||
}, app); | ||
} else { | ||
var ipv4_server = http.createServer(app); | ||
var ipv6_server = http.createServer(app); | ||
} | ||
// Listen on our 2 servers. If we attempt to listen on a single server, the results will be non-deterministic. | ||
// It works on node 0.10.30 but not on 0.10.35, for example. Separating the two servers appears to work everywhere. | ||
ipv4_server.listen(this_obj.config.from_port, '0.0.0.0'); | ||
ipv6_server.listen(this_obj.config.from_port, '::'); | ||
cb(null, this_obj); | ||
@@ -142,0 +170,0 @@ }); |
{ | ||
"name": "oauth_reverse_proxy", | ||
"description": "An OAuth 1.0a authenticating reverse proxy", | ||
"version": "1.1.0", | ||
"version": "1.2.0", | ||
"contributors": [ | ||
@@ -6,0 +6,0 @@ { |
@@ -19,3 +19,4 @@ oauth_reverse_proxy is an authenticating service proxy that fronts any web server and enforces that callers present the correct OAuth credentials. | ||
* Built to perform: A single node can authenticate around 10k requests per second on reasonable hardware. | ||
* Flexible enough to front multiple services: If you run more than one HTTP server per system, as is common in the case of an nginx-fronted application, you can put an instance of `oauth_reverse_proxy` either in front of or behind nginx. A single instance of `oauth_reverse_proxy` can bind a separate proxy to any number of inbound ports. | ||
* Supports inbound requests over http and https. | ||
* Is flexible enough to front multiple services: If you run more than one HTTP server per system, as is common in the case of an nginx-fronted application, you can put an instance of `oauth_reverse_proxy` either in front of or behind nginx. A single instance of `oauth_reverse_proxy` can bind a separate proxy to any number of inbound ports. | ||
* Supports configurable whitelisting: You likely have a load balancer that needs to perform health-checks against your application without performing authentication. `oauth_reverse_proxy` supports regex-based whitelists, so you can configure an un-authenticated path through to only those routes. | ||
@@ -22,0 +23,0 @@ * Supports a quota per key, allowing you to define that a given key should only be allowed to make a certain number of hits per a given time interval. |
@@ -95,3 +95,7 @@ var fs = require('fs'); | ||
{ 'filename': 'giant_from_port_service.json', 'expected_error': 'from_port must be a valid port number'}, | ||
{ 'filename': 'giant_to_port_service.json', 'expected_error': 'to_port must be a valid port number'} | ||
{ 'filename': 'giant_to_port_service.json', 'expected_error': 'to_port must be a valid port number'}, | ||
{ 'filename': 'no_ssl_cert_service.json', 'expected_error': 'no ssl cert file provided'}, | ||
{ 'filename': 'no_ssl_key_service.json', 'expected_error': 'no ssl key file provided'}, | ||
{ 'filename': 'invalid_ssl_cert_service.json', 'expected_error': 'https cert file ./test/resources/cert_oops.pem does not exist'}, | ||
{ 'filename': 'invalid_ssl_key_service.json', 'expected_error': 'https key file ./test/resources/key_oops.pem does not exist'} | ||
].forEach(function(validation) { | ||
@@ -98,0 +102,0 @@ it ('should reject a proxy config with error: ' + validation.expected_error, function() { |
@@ -105,3 +105,5 @@ var should = require('should'); | ||
'nonnumeric_quota_key_threshold_service.json', 'nonpositive_quota_default_threshold_service.json', | ||
'nonpositive_quota_key_threshold_service.json', 'subsecond_quota_interval_service.json' | ||
'nonpositive_quota_key_threshold_service.json', 'subsecond_quota_interval_service.json', | ||
'no_ssl_cert_service.json', 'no_ssl_key_service.json', | ||
'invalid_ssl_cert_service.json', 'invalid_ssl_key_service.json' | ||
].forEach(function(invalid_config_file) { | ||
@@ -108,0 +110,0 @@ it ('should reject invalid proxy config file ' + invalid_config_file, function() { |
var _ = require('underscore'); | ||
var job_server = require('./server/test_server.js').JobServer; | ||
// All the messy business of creating and sending requests (both authenticated and unauthenticated) | ||
@@ -6,0 +4,0 @@ // lives in request_sender. |
@@ -199,3 +199,4 @@ var should = require('should'); | ||
signature_components[0] = options.method; | ||
signature_components[1] = 'http://' + options.hostname + ':' + options.port + options.pathname; | ||
var proto = options.protocol || 'http:'; | ||
signature_components[1] = proto + '//' + options.hostname + ':' + options.port + options.pathname; | ||
@@ -202,0 +203,0 @@ // We don't technically need to reset the options value, but it does make it more clear that |
Future Items: | ||
- [ ] Add support for HTTPs termination | ||
- [ ] Add configurable location for SSL certs per proxy | ||
- [x] Add support for HTTPs termination | ||
- [x] Add configurable location for SSL certs per proxy | ||
- [x] Add support for per-key rate-limit quotas | ||
- [ ] Add data collection via statsd |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
16162744
120
3716
102
24
8