From a5b3fbe47c72d5b2051ecd076eb1322a96b6d39b Mon Sep 17 00:00:00 2001 From: Wyatt Preul Date: Wed, 29 May 2013 11:14:46 -0500 Subject: [PATCH 1/2] Exposing rejectUnauthorized property on proxy --- docs/Reference.md | 1 + lib/client.js | 1 + lib/proxy.js | 4 +- test/integration/proxy.js | 129 +++++++++++++++++++++++++++----------- test/unit/client.js | 3 +- 5 files changed, 100 insertions(+), 38 deletions(-) diff --git a/docs/Reference.md b/docs/Reference.md index 62adc09e1..f8b6fd8cd 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -365,6 +365,7 @@ The following options are available when adding a route: - `'http'` - `'https'` - `passThrough` - if `true`, forwards the headers sent from the client to the upstream service being proxied to. Defaults to `false`. + - `rejectUnauthorized` - sets the _'rejectUnauthorized'_ property on the https [agent](http://nodejs.org/api/https.html#https_https_request_options_callback) making the request. If _'false'_ then ssl errors will be ignored. Defaults to `undefined`. - `xforward` - if `true`, sets the 'X-Forwarded-For', 'X-Forwarded-Port', 'X-Forwarded-Proto' headers when making a request to the proxied upstream endpoint. Defaults to `false`. - `redirects` - the maximum number of HTTP redirections allowed, to be followed automatically by the handler. Set to `false` or `0` to diff --git a/lib/client.js b/lib/client.js index a5cc57d6c..12c5b2c1d 100755 --- a/lib/client.js +++ b/lib/client.js @@ -24,6 +24,7 @@ exports.request = function (method, url, options, callback, _trace) { var uri = Url.parse(url); uri.method = method.toUpperCase(); uri.headers = options.headers; + uri.rejectUnauthorized = options.rejectUnauthorized === undefined ? undefined : options.rejectUnauthorized; var redirects = (options.hasOwnProperty('redirects') ? options.redirects : false); // Needed to allow 0 as valid value when passed recursively diff --git a/lib/proxy.js b/lib/proxy.js index 237332946..6641282bb 100755 --- a/lib/proxy.js +++ b/lib/proxy.js @@ -30,6 +30,7 @@ exports = module.exports = internals.Proxy = function (options, route) { this.settings.isCustomPostResponse = !!options.postResponse; this.settings.postResponse = options.postResponse || internals.postResponse; // function (request, settings, response, payload) this.settings.redirects = options.redirects || false; + this.settings.rejectUnauthorized = options.rejectUnauthorized === undefined ? undefined : options.rejectUnauthorized; }; @@ -51,7 +52,8 @@ internals.Proxy.prototype.handler = function () { headers: {}, payload: null, redirects: self.settings.redirects, - timeout: self.settings.timeout + timeout: self.settings.timeout, + rejectUnauthorized: self.settings.rejectUnauthorized }; if (self.settings.passThrough) { // Never set with cache diff --git a/test/integration/proxy.js b/test/integration/proxy.js index 0975e091a..4f0198b1e 100755 --- a/test/integration/proxy.js +++ b/test/integration/proxy.js @@ -24,9 +24,15 @@ var it = Lab.test; describe('Proxy', function () { var server = null; + var sslServer = null; before(function (done) { + var tlsOptions = { + key: '-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBANysie374iGH54SVcmM4vb+CjN4nVVCmL6af9XOUxTqq/50CBn+Z\nZol0XDG+OK55HTOht4CsQrAXey69ZTxgUMcCAwEAAQJAX5t5XtxkiraA/hZpqsdo\nnlKHibBs7DY0KvLeuybXlKS3ar/0Uz0OSJ1oLx3d0KDSmcdAIrfnyFuBNuBzb3/J\nEQIhAPX/dh9azhztRppR+9j8CxDg4ixJ4iZbHdK0pfnY9oIFAiEA5aV8edK31dkF\nfBXoqlOvIeuNc6WBZrYjUNspH8M+BVsCIQDZF3U6/nve81bXYXqMZwGtB4kR5LH7\nf3W2OU4wS9RfsQIhAJkNB76xX3AYqX0fpOcPyuLSeH2gynNH5JWY2vmeSBGNAiAm\nLon4E3M/IrVVvpxGRFOazKlgIsQFGAaoylDrRFYgBA==\n-----END RSA PRIVATE KEY-----\n', + cert: '-----BEGIN CERTIFICATE-----\nMIIB0TCCAXugAwIBAgIJANGtTMK5HBUIMA0GCSqGSIb3DQEBBQUAMEQxCzAJBgNV\nBAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UECgwJaGFwaSB0ZXN0MRQwEgYDVQQD\nDAtleGFtcGxlLmNvbTAeFw0xMzA0MDQxNDQ4MDJaFw0yMzA0MDIxNDQ4MDJaMEQx\nCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UECgwJaGFwaSB0ZXN0MRQw\nEgYDVQQDDAtleGFtcGxlLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQDcrInt\n++Ihh+eElXJjOL2/gozeJ1VQpi+mn/VzlMU6qv+dAgZ/mWaJdFwxvjiueR0zobeA\nrEKwF3suvWU8YFDHAgMBAAGjUDBOMB0GA1UdDgQWBBQBOiF6iL2PI4E6PBj071Dh\nAiQOGjAfBgNVHSMEGDAWgBQBOiF6iL2PI4E6PBj071DhAiQOGjAMBgNVHRMEBTAD\nAQH/MA0GCSqGSIb3DQEBBQUAA0EAw8Y2rpM8SUQXjgaJJmFXrfEvnl/he7q83K9W\n9Sr/QLHpCFxunWVd8c0wz+b8P/F9uW2V4wUf5NWj1UdHMCd6wQ==\n-----END CERTIFICATE-----\n' + }; + var mapUriWithError = function (request, callback) { return callback(new Error('myerror')); @@ -128,6 +134,11 @@ describe('Proxy', function () { }, 10); }; + var sslHandler = function (req) { + + req.reply('Ok'); + }; + var upstream = new Hapi.Server(0); upstream.route([ { method: 'GET', path: '/profile', handler: profile }, @@ -150,48 +161,68 @@ describe('Proxy', function () { { method: 'GET', path: '/timeout2', handler: timeoutHandler } ]); + var upstreamSsl = new Hapi.Server(0, { tls: tlsOptions }); + upstreamSsl.route([ + { method: 'GET', path: '/', handler: sslHandler } + ]); + var mapUri = function (request, callback) { return callback(null, upstream.info.uri + request.path + (request.url.search || ''), { 'x-super-special': '@' }); }; - upstream.start(function () { + var mapSslUri = function (request, callback) { - var backendPort = upstream.info.port; - var routeCache = { expiresIn: 500, mode: 'server+client' }; - - server = new Hapi.Server(0, { cors: true, maxSockets: 10 }); - server.route([ - { method: 'GET', path: '/profile', handler: { proxy: { host: 'localhost', port: backendPort, xforward: true, passThrough: true } } }, - { method: 'GET', path: '/item', handler: { proxy: { host: 'localhost', port: backendPort } }, config: { cache: routeCache } }, - { method: 'GET', path: '/unauthorized', handler: { proxy: { host: 'localhost', port: backendPort } }, config: { cache: routeCache } }, - { method: 'POST', path: '/item', handler: { proxy: { host: 'localhost', port: backendPort } } }, - { method: 'POST', path: '/notfound', handler: { proxy: { host: 'localhost', port: backendPort } } }, - { method: 'GET', path: '/proxyerror', handler: { proxy: { host: 'localhost', port: backendPort } }, config: { cache: routeCache } }, - { method: 'GET', path: '/postResponseError', handler: { proxy: { host: 'localhost', port: backendPort, postResponse: postResponseWithError } }, config: { cache: routeCache } }, - { method: 'GET', path: '/errorResponse', handler: { proxy: { host: 'localhost', port: backendPort } }, config: { cache: routeCache } }, - { method: 'POST', path: '/echo', handler: { proxy: { mapUri: mapUri } } }, - { method: 'POST', path: '/file', handler: { proxy: { host: 'localhost', port: backendPort } }, config: { payload: 'stream' } }, - { method: 'GET', path: '/maperror', handler: { proxy: { mapUri: mapUriWithError } } }, - { method: 'GET', path: '/forward', handler: { proxy: { host: 'localhost', port: backendPort, xforward: true, passThrough: true } } }, - { method: 'GET', path: '/headers', handler: { proxy: { host: 'localhost', port: backendPort, passThrough: true } } }, - { method: 'GET', path: '/noHeaders', handler: { proxy: { host: 'localhost', port: backendPort } } }, - { method: 'GET', path: '/gzip', handler: { proxy: { host: 'localhost', port: backendPort, passThrough: true } } }, - { method: 'GET', path: '/gzipstream', handler: { proxy: { host: 'localhost', port: backendPort, passThrough: true } } }, - { method: 'GET', path: '/google', handler: { proxy: { mapUri: function (request, callback) { callback(null, 'http://www.google.com'); } } } }, - { method: 'GET', path: '/googler', handler: { proxy: { mapUri: function (request, callback) { callback(null, 'http://google.com'); }, redirects: 1 } } }, - { method: 'GET', path: '/redirect', handler: { proxy: { host: 'localhost', port: backendPort, passThrough: true, redirects: 2 } } }, - { method: 'POST', path: '/post1', handler: { proxy: { host: 'localhost', port: backendPort, redirects: 3 } }, config: { payload: 'stream' } }, - { method: 'GET', path: '/nowhere', handler: { proxy: { host: 'no.such.domain.x8' } } }, - { method: 'GET', path: '/cached', handler: { proxy: { host: 'localhost', port: backendPort } }, config: { cache: routeCache } }, - { method: 'GET', path: '/timeout1', handler: { proxy: { host: 'localhost', port: backendPort, timeout: 5 } } }, - { method: 'GET', path: '/timeout2', handler: { proxy: { host: 'localhost', port: backendPort } } } - ]); - - server.state('auto', { autoValue: 'xyz' }); - server.start(function () { + return callback(null, 'https://127.0.0.1:' + upstreamSsl.info.port); + }; - done(); + upstream.start(function () { + + upstreamSsl.start(function () { + + var backendPort = upstream.info.port; + var routeCache = { expiresIn: 500, mode: 'server+client' }; + + server = new Hapi.Server(0, { cors: true, maxSockets: 10 }); + server.route([ + { method: 'GET', path: '/profile', handler: { proxy: { host: 'localhost', port: backendPort, xforward: true, passThrough: true } } }, + { method: 'GET', path: '/item', handler: { proxy: { host: 'localhost', port: backendPort } }, config: { cache: routeCache } }, + { method: 'GET', path: '/unauthorized', handler: { proxy: { host: 'localhost', port: backendPort } }, config: { cache: routeCache } }, + { method: 'POST', path: '/item', handler: { proxy: { host: 'localhost', port: backendPort } } }, + { method: 'POST', path: '/notfound', handler: { proxy: { host: 'localhost', port: backendPort } } }, + { method: 'GET', path: '/proxyerror', handler: { proxy: { host: 'localhost', port: backendPort } }, config: { cache: routeCache } }, + { method: 'GET', path: '/postResponseError', handler: { proxy: { host: 'localhost', port: backendPort, postResponse: postResponseWithError } }, config: { cache: routeCache } }, + { method: 'GET', path: '/errorResponse', handler: { proxy: { host: 'localhost', port: backendPort } }, config: { cache: routeCache } }, + { method: 'POST', path: '/echo', handler: { proxy: { mapUri: mapUri } } }, + { method: 'POST', path: '/file', handler: { proxy: { host: 'localhost', port: backendPort } }, config: { payload: 'stream' } }, + { method: 'GET', path: '/maperror', handler: { proxy: { mapUri: mapUriWithError } } }, + { method: 'GET', path: '/forward', handler: { proxy: { host: 'localhost', port: backendPort, xforward: true, passThrough: true } } }, + { method: 'GET', path: '/headers', handler: { proxy: { host: 'localhost', port: backendPort, passThrough: true } } }, + { method: 'GET', path: '/noHeaders', handler: { proxy: { host: 'localhost', port: backendPort } } }, + { method: 'GET', path: '/gzip', handler: { proxy: { host: 'localhost', port: backendPort, passThrough: true } } }, + { method: 'GET', path: '/gzipstream', handler: { proxy: { host: 'localhost', port: backendPort, passThrough: true } } }, + { method: 'GET', path: '/google', handler: { proxy: { mapUri: function (request, callback) { callback(null, 'http://www.google.com'); } } } }, + { method: 'GET', path: '/googler', handler: { proxy: { mapUri: function (request, callback) { callback(null, 'http://google.com'); }, redirects: 1 } } }, + { method: 'GET', path: '/redirect', handler: { proxy: { host: 'localhost', port: backendPort, passThrough: true, redirects: 2 } } }, + { method: 'POST', path: '/post1', handler: { proxy: { host: 'localhost', port: backendPort, redirects: 3 } }, config: { payload: 'stream' } }, + { method: 'GET', path: '/nowhere', handler: { proxy: { host: 'no.such.domain.x8' } } }, + { method: 'GET', path: '/cached', handler: { proxy: { host: 'localhost', port: backendPort } }, config: { cache: routeCache } }, + { method: 'GET', path: '/timeout1', handler: { proxy: { host: 'localhost', port: backendPort, timeout: 5 } } }, + { method: 'GET', path: '/timeout2', handler: { proxy: { host: 'localhost', port: backendPort } } } + ]); + + sslServer = new Hapi.Server(0); + sslServer.route([ + { method: 'GET', path: '/allow', handler: { proxy: { mapUri: mapSslUri, rejectUnauthorized: false } } }, + { method: 'GET', path: '/reject', handler: { proxy: { mapUri: mapSslUri, rejectUnauthorized: true } } }, + { method: 'GET', path: '/sslDefault', handler: { proxy: { mapUri: mapSslUri } } } + ]); + + server.state('auto', { autoValue: 'xyz' }); + server.start(function () { + + sslServer.start(done); + }); }); }); }); @@ -486,5 +517,33 @@ describe('Proxy', function () { done(); }); }); + + it('uses rejectUnauthorized to allow proxy to self signed ssl server', function (done) { + + sslServer.inject('/allow', function (res) { + + expect(res.statusCode).to.equal(200); + expect(res.payload).to.equal('Ok'); + done(); + }); + }); + + it('uses rejectUnauthorized to not allow proxy to self signed ssl server', function (done) { + + sslServer.inject('/reject', function (res) { + + expect(res.statusCode).to.equal(500); + done(); + }); + }); + + it('the default rejectUnauthorized should not allow proxied server cert to be self signed', function (done) { + + sslServer.inject('/sslDefault', function (res) { + + expect(res.statusCode).to.equal(500); + done(); + }); + }); }); diff --git a/test/unit/client.js b/test/unit/client.js index 4c1a47cf0..477ca361d 100644 --- a/test/unit/client.js +++ b/test/unit/client.js @@ -88,7 +88,7 @@ describe('Client', function () { server.once('listening', function () { - Client.request('get', 'http://127.0.0.1:' + server.address().port, { payload: 'asdadqweqweqweqewqweqweqeqweqewqwe' }, function (err) { + Client.request('get', 'http://127.0.0.1:' + server.address().port, { payload: '' }, function (err) { expect(err.data.err.code).to.equal('ECONNRESET'); done(); @@ -120,7 +120,6 @@ describe('Client', function () { it('handles errors when remote server is unavailable', function (done) { - Client.request('get', 'http://127.0.0.1:0', { payload: '' }, function (err) { expect(err).to.exist; From be387bd8ec5d41948edd091b440f88abed34ab3a Mon Sep 17 00:00:00 2001 From: Wyatt Preul Date: Wed, 29 May 2013 11:28:32 -0500 Subject: [PATCH 2/2] Updating readme --- docs/Reference.md | 4 +++- lib/client.js | 5 ++++- lib/proxy.js | 5 ++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/Reference.md b/docs/Reference.md index f8b6fd8cd..7e58a2529 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -365,7 +365,9 @@ The following options are available when adding a route: - `'http'` - `'https'` - `passThrough` - if `true`, forwards the headers sent from the client to the upstream service being proxied to. Defaults to `false`. - - `rejectUnauthorized` - sets the _'rejectUnauthorized'_ property on the https [agent](http://nodejs.org/api/https.html#https_https_request_options_callback) making the request. If _'false'_ then ssl errors will be ignored. Defaults to `undefined`. + - `rejectUnauthorized` - sets the _'rejectUnauthorized'_ property on the https [agent](http://nodejs.org/api/https.html#https_https_request_options_callback) making the request. + This value is only used when the proxied server uses TLS/SSL. When set it will override the node.js _'rejectUnauthorized'_ property. If _'false'_ then ssl errors will be ignored. + When _'true'_ the server certificate is verified and an 500 response will be sent when verification fails. Defaults to the https agent default value of _'true'_. - `xforward` - if `true`, sets the 'X-Forwarded-For', 'X-Forwarded-Port', 'X-Forwarded-Proto' headers when making a request to the proxied upstream endpoint. Defaults to `false`. - `redirects` - the maximum number of HTTP redirections allowed, to be followed automatically by the handler. Set to `false` or `0` to diff --git a/lib/client.js b/lib/client.js index 12c5b2c1d..ad2579720 100755 --- a/lib/client.js +++ b/lib/client.js @@ -24,7 +24,10 @@ exports.request = function (method, url, options, callback, _trace) { var uri = Url.parse(url); uri.method = method.toUpperCase(); uri.headers = options.headers; - uri.rejectUnauthorized = options.rejectUnauthorized === undefined ? undefined : options.rejectUnauthorized; + + if (options.rejectUnauthorized !== undefined && uri.protocol === 'https:') { + uri.rejectUnauthorized = options.rejectUnauthorized; + } var redirects = (options.hasOwnProperty('redirects') ? options.redirects : false); // Needed to allow 0 as valid value when passed recursively diff --git a/lib/proxy.js b/lib/proxy.js index 6641282bb..fa0cf1e50 100755 --- a/lib/proxy.js +++ b/lib/proxy.js @@ -30,7 +30,10 @@ exports = module.exports = internals.Proxy = function (options, route) { this.settings.isCustomPostResponse = !!options.postResponse; this.settings.postResponse = options.postResponse || internals.postResponse; // function (request, settings, response, payload) this.settings.redirects = options.redirects || false; - this.settings.rejectUnauthorized = options.rejectUnauthorized === undefined ? undefined : options.rejectUnauthorized; + + if (options.rejectUnauthorized !== undefined) { + this.settings.rejectUnauthorized = options.rejectUnauthorized; + } };