diff --git a/examples/http/proxy-http2-to-http.js b/examples/http/proxy-http2-to-http.js new file mode 100644 index 000000000..2d2bdcdc0 --- /dev/null +++ b/examples/http/proxy-http2-to-http.js @@ -0,0 +1,59 @@ +/* + proxy-http2-to-http.js: Basic example of proxying over HTTP2 to a target HTTP server + + Copyright (c) Nodejitsu 2013 + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +var http = require('http'), + path = require('path'), + fs = require('fs'), + colors = require('colors'), + httpProxy = require('../../lib/http-proxy'), + fixturesDir = path.join(__dirname, '..', '..', 'test', 'fixtures'); + +// +// Create the target HTTP server +// +var source = http.createServer(function(req, res) { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Hello from HTTP2'); +}); + +source.listen(9009); + +// +// Create the HTTP2 proxy server listening on port 8009 +// +var proxy = httpProxy.createProxyServer({ + target: 'http://127.0.0.1:9009', + http2: { + key: fs.readFileSync(path.join(fixturesDir, 'agent2-key.pem'), 'utf8'), + cert: fs.readFileSync(path.join(fixturesDir, 'agent2-cert.pem'), 'utf8'), + ciphers: 'AES128-GCM-SHA256', + } +}); + +proxy.listen(8009); + +console.log('http2 proxy server'.blue + ' started '.green.bold + 'on port '.blue + '8009'.yellow); +console.log('http server '.blue + 'started '.green.bold + 'on port '.blue + '9009 '.yellow); diff --git a/lib/http-proxy.js b/lib/http-proxy.js index 365acedb1..d3927bf7a 100644 --- a/lib/http-proxy.js +++ b/lib/http-proxy.js @@ -34,6 +34,7 @@ module.exports.createProxyServer = * forward: * agent : * ssl : + * http2 : * ws : * xfwd : * secure : @@ -48,9 +49,8 @@ module.exports.createProxyServer = * protocolRewrite: rewrites the location protocol on (301/302/307/308) redirects to 'http' or 'https'. Default: null. * } * - * NOTE: `options.ws` and `options.ssl` are optional. - * `options.target and `options.forward` cannot be - * both missing + * NOTE: `options.ws` `options.http2` and `options.ssl` are optional. + * `options.target and `options.forward` cannot be both missing * } */ diff --git a/lib/http-proxy/index.js b/lib/http-proxy/index.js index 7a5e1d2e8..4dc1e61cb 100644 --- a/lib/http-proxy/index.js +++ b/lib/http-proxy/index.js @@ -4,6 +4,7 @@ var httpProxy = exports, EE3 = require('eventemitter3'), http = require('http'), https = require('https'), + http2 = require('http2'), web = require('./passes/web-incoming'), ws = require('./passes/ws-incoming'); @@ -126,7 +127,9 @@ ProxyServer.prototype.listen = function(port, hostname) { this._server = this.options.ssl ? https.createServer(this.options.ssl, closure) : - http.createServer(closure); + this.options.http2 ? + http2.createServer(this.options.http2, closure) : + http.createServer(closure); if(this.options.ws) { this._server.on('upgrade', function(req, socket, head) { self.ws(req, socket, head); }); diff --git a/lib/http-proxy/passes/web-incoming.js b/lib/http-proxy/passes/web-incoming.js index 4070eb316..7766f44a2 100644 --- a/lib/http-proxy/passes/web-incoming.js +++ b/lib/http-proxy/passes/web-incoming.js @@ -63,10 +63,9 @@ web_o = Object.keys(web_o).map(function(pass) { function XHeaders(req, res, options) { if(!options.xfwd) return; - - var encrypted = req.isSpdy || common.hasEncryptedConnection(req); + var encrypted = req.isSpdy || req.httpVersion === '2.0' || common.hasEncryptedConnection(req); var values = { - for : req.connection.remoteAddress || req.socket.remoteAddress, + for : (req.connection && req.connection.remoteAddress) || req.socket.remoteAddress, port : common.getPort(req), proto: encrypted ? 'https' : 'http' }; @@ -98,7 +97,7 @@ web_o = Object.keys(web_o).map(function(pass) { if(options.forward) { // If forward enable, so just pipe the request var forwardReq = (options.forward.protocol === 'https:' ? https : http).request( - common.setupOutgoing(options.ssl || {}, options, req, 'forward') + common.setupOutgoing(options.ssl || options.http2 || {}, options, req, 'forward') ); (options.buffer || req).pipe(forwardReq); if(!options.target) { return res.end(); } @@ -106,7 +105,7 @@ web_o = Object.keys(web_o).map(function(pass) { // Request initalization var proxyReq = (options.target.protocol === 'https:' ? https : http).request( - common.setupOutgoing(options.ssl || {}, options, req) + common.setupOutgoing(options.ssl || options.http2 || {}, options, req) ); // Enable developers to modify the proxyReq before headers are sent diff --git a/lib/http-proxy/passes/web-outgoing.js b/lib/http-proxy/passes/web-outgoing.js index 977f1f747..062fe57aa 100644 --- a/lib/http-proxy/passes/web-outgoing.js +++ b/lib/http-proxy/passes/web-outgoing.js @@ -23,7 +23,7 @@ var redirectRegex = /^30(1|2|7|8)$/; * @api private */ function removeChunked(req, res, proxyRes) { - if (req.httpVersion === '1.0') { + if (req.httpVersion === '1.0' || req.httpVersion === '2.0' ) { delete proxyRes.headers['transfer-encoding']; } }, @@ -41,6 +41,8 @@ var redirectRegex = /^30(1|2|7|8)$/; function setConnection(req, res, proxyRes) { if (req.httpVersion === '1.0') { proxyRes.headers.connection = req.headers.connection || 'close'; + } else if (req.httpVersion === '2.0') { + delete proxyRes.headers.connection; } else if (!proxyRes.headers.connection) { proxyRes.headers.connection = req.headers.connection || 'keep-alive'; } diff --git a/package.json b/package.json index b003eb8c7..6befac732 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "main": "index.js", "dependencies": { "eventemitter3": "1.x.x", + "http2": "^3.2.0", "requires-port": "1.x.x" }, "devDependencies": { diff --git a/test/lib-http2-proxy-test.js b/test/lib-http2-proxy-test.js new file mode 100644 index 000000000..a2481e38e --- /dev/null +++ b/test/lib-http2-proxy-test.js @@ -0,0 +1,253 @@ +var httpProxy = require('../lib/http-proxy'), + semver = require('semver'), + expect = require('expect.js'), + http = require('http') + https = require('https'), + http2 = require('http2'), + path = require('path'), + fs = require('fs'); + +// +// Expose a port number generator. +// thanks to @3rd-Eden +// +var initialPort = 1024, gen = {}; +Object.defineProperty(gen, 'port', { + get: function get() { + return initialPort++; + } +}); + +describe('lib/http-proxy.js', function() { + describe('HTTP2 #createProxyServer', function() { + + describe('HTTP2 to HTTP', function () { + it('should proxy the request en send back the response', function (done) { + var ports = { source: gen.port, proxy: gen.port }; + var source = http.createServer(function(req, res) { + expect(req.method).to.eql('GET'); + // expect(req.headers.host.split(':')[1]).to.eql(ports.proxy); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Hello from ' + ports.source); + }); + + source.listen(ports.source); + + var proxy = httpProxy.createProxyServer({ + target: 'http://127.0.0.1:' + ports.source, + http2: { + key: fs.readFileSync(path.join(__dirname, 'fixtures', 'agent2-key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'fixtures', 'agent2-cert.pem')), + ciphers: 'AES128-GCM-SHA256', + } + }).listen(ports.proxy); + + http2.get({ + host: 'localhost', + port: ports.proxy, + path: '/', + method: 'GET', + rejectUnauthorized: false + }, function(res) { + expect(res.statusCode).to.eql(200); + expect(res.httpVersion).to.eql(2.0); + + res.on('data', function (data) { + expect(data.toString()).to.eql('Hello from ' + ports.source); + }); + + res.on('end', function () { + source.close(); + proxy.close(); + done(); + }) + }).end(); + }) + }); + + describe('HTTP to HTTP2', function () { + it('should proxy the request en send back the response', function (done) { + var ports = { source: gen.port, proxy: gen.port }; + var source = http2.createServer({ + key: fs.readFileSync(path.join(__dirname, 'fixtures', 'agent2-key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'fixtures', 'agent2-cert.pem')), + ciphers: 'AES128-GCM-SHA256', + }, function (req, res) { + expect(req.method).to.eql('GET'); + expect(req.headers.host.split(':')[1]).to.eql(ports.proxy); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Hello from ' + ports.source); + }); + + source.listen(ports.source); + + var proxy = httpProxy.createProxyServer({ + target: 'https://127.0.0.1:' + ports.source, + // Allow to use SSL self signed + secure: false + }).listen(ports.proxy); + + http.request({ + hostname: '127.0.0.1', + port: ports.proxy, + method: 'GET' + }, function(res) { + expect(res.statusCode).to.eql(200); + + res.on('data', function (data) { + expect(data.toString()).to.eql('Hello from ' + ports.source); + }); + + res.on('end', function () { + source.close(); + proxy.close(); + done(); + }); + }).end(); + }) + }) + describe('HTTP2 to HTTP2', function () { + it('should proxy the request en send back the response', function (done) { + var ports = { source: gen.port, proxy: gen.port }; + var source = http2.createServer({ + key: fs.readFileSync(path.join(__dirname, 'fixtures', 'agent2-key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'fixtures', 'agent2-cert.pem')), + ciphers: 'AES128-GCM-SHA256', + }, function(req, res) { + expect(req.method).to.eql('GET'); + // expect(req.headers.host.split(':')[1]).to.eql(ports.proxy); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Hello from ' + ports.source); + }); + + source.listen(ports.source); + + var proxy = httpProxy.createProxyServer({ + target: 'https://127.0.0.1:' + ports.source, + http2: { + key: fs.readFileSync(path.join(__dirname, 'fixtures', 'agent2-key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'fixtures', 'agent2-cert.pem')), + ciphers: 'AES128-GCM-SHA256', + }, + secure: false + }) + + proxy.listen(ports.proxy); + + http2.request({ + host: 'localhost', + port: ports.proxy, + path: '/', + method: 'GET', + rejectUnauthorized: false + }, function(res) { + expect(res.statusCode).to.eql(200); + + res.on('data', function (data) { + expect(data.toString()).to.eql('Hello from ' + ports.source); + }); + + res.on('end', function () { + source.close(); + proxy.close(); + done(); + }) + }).end(); + }) + }); + describe('HTTP2 not allow SSL self signed', function () { + it('should fail with error', function (done) { + + var ports = { source: gen.port, proxy: gen.port }; + var source = http2.createServer({ + key: fs.readFileSync(path.join(__dirname, 'fixtures', 'agent2-key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'fixtures', 'agent2-cert.pem')), + ciphers: 'AES128-GCM-SHA256', + }); + + source.listen(ports.source); + + var proxy = httpProxy.createProxyServer({ + target: 'https://127.0.0.1:' + ports.source, + http2: { + key: fs.readFileSync(path.join(__dirname, 'fixtures', 'agent2-key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'fixtures', 'agent2-cert.pem')), + ciphers: 'AES128-GCM-SHA256', + }, + secure: true + }) + + proxy.listen(ports.proxy); + + proxy.on('error', function (err, req, res) { + expect(err).to.be.an(Error); + if (semver.gt(process.versions.node, '0.12.0')) { + expect(err.toString()).to.be('Error: self signed certificate') + } else { + expect(err.toString()).to.be('Error: DEPTH_ZERO_SELF_SIGNED_CERT') + } + source.close(); + proxy.close(); + done(); + }); + + http2.request({ + host: 'localhost', + port: ports.proxy, + path: '/', + method: 'GET', + rejectUnauthorized: false, + }).end(); + }) + }) + describe('HTTP2 to HTTP using own server', function () { + it('should proxy the request en send back the response', function (done) { + var ports = { source: gen.port, proxy: gen.port }; + var source = http.createServer(function(req, res) { + expect(req.method).to.eql('GET'); + // expect(req.headers.host.split(':')[1]).to.eql(ports.proxy); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Hello from ' + ports.source); + }); + + source.listen(ports.source); + + var proxy = httpProxy.createServer({ + agent: new http.Agent({ maxSockets: 2 }) + }); + + var ownServer = http2.createServer({ + key: fs.readFileSync(path.join(__dirname, 'fixtures', 'agent2-key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'fixtures', 'agent2-cert.pem')), + ciphers: 'AES128-GCM-SHA256', + }, function (req, res) { + proxy.web(req, res, { + target: 'http://127.0.0.1:' + ports.source + }) + }); + + ownServer.listen(ports.proxy); + + http2.request({ + host: 'localhost', + port: ports.proxy, + path: '/', + method: 'GET', + rejectUnauthorized: false + }, function(res) { + expect(res.httpVersion).to.eql(2.0); + + res.on('data', function (data) { + expect(data.toString()).to.eql('Hello from ' + ports.source); + }); + + res.on('end', function () { + source.close(); + ownServer.close(); + done(); + }) + }).end(); + }) + }); + }); +});