From f09b8f86e712eb6cc8da043500b5096ac8345ab6 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 29 Oct 2019 20:15:30 +0100 Subject: [PATCH] Issue #20 - Support forward proxies. Implemented support for forward proxies. Signed-off-by: Simone Bordet --- .gitignore | 7 + cometd-nodejs-client.js | 50 +++++- package.json | 6 +- test/proxy.js | 330 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 384 insertions(+), 9 deletions(-) create mode 100644 .gitignore create mode 100644 test/proxy.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d57ba1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Development +*.ipr +*.iws +*.iml +.idea/ + +node_modules/ diff --git a/cometd-nodejs-client.js b/cometd-nodejs-client.js index d0924e0..fb5d119 100644 --- a/cometd-nodejs-client.js +++ b/cometd-nodejs-client.js @@ -1,5 +1,5 @@ module.exports = { - adapt: function() { + adapt: function(options) { var url = require('url'); var httpc = require('http'); var https = require('https'); @@ -16,12 +16,14 @@ module.exports = { window.console.debug = window.console.log; // Fields shared by all XMLHttpRequest instances. - var _agentc = new httpc.Agent({ + var _agentOptions = { keepAlive: true - }); - var _agents = new https.Agent({ - keepAlive: true - }); + }; + var _agentc = new httpc.Agent(_agentOptions); + var _agents = new https.Agent(_agentOptions); + var HttpcProxyAgent = require('http-proxy-agent'); + var HttpsProxyAgent = require('https-proxy-agent'); + var _globalCookies = {}; function _secure(uri) { @@ -66,6 +68,40 @@ module.exports = { return cookies; } + function _chooseAgent(serverURI) { + var serverHostPort = serverURI.host; + var proxy = options && options.httpProxy && options.httpProxy.uri; + if (proxy) { + var isIncluded = true; + var includes = options.httpProxy.includes; + if (includes && Array.isArray(includes)) { + isIncluded = false; + for (var i = 0; i < includes.length; ++i) { + if (new RegExp(includes[i]).test(serverHostPort)) { + isIncluded = true; + break; + } + } + } + if (isIncluded) { + var excludes = options.httpProxy.excludes; + if (excludes && Array.isArray(excludes)) { + for (var e = 0; e < excludes.length; ++e) { + if (new RegExp(excludes[e]).test(serverHostPort)) { + isIncluded = false; + break; + } + } + } + } + if (isIncluded) { + var agentOpts = Object.assign(url.parse(proxy), _agentOptions); + return _secure(serverURI) ? new HttpsProxyAgent(agentOpts) : new HttpcProxyAgent(agentOpts); + } + } + return _secure(serverURI) ? _agents : _agentc; + } + this.status = 0; this.statusText = ''; this.readyState = window.XMLHttpRequest.UNSENT; @@ -73,8 +109,8 @@ module.exports = { this.open = function(method, uri) { _config = url.parse(uri); + _config.agent = _chooseAgent(_config); _config.method = method; - _config.agent = _secure(_config) ? _agents : _agentc; _config.headers = {}; this.readyState = window.XMLHttpRequest.OPENED; }; diff --git a/package.json b/package.json index 1a9fa91..2be666c 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,11 @@ "test": "mocha --exit" }, "dependencies": { - "cometd": ">=3.1.2" + "cometd": ">=3.1.2", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^2.1.0" }, "devDependencies": { - "mocha": "*" + "mocha": "^6.2.2" } } diff --git a/test/proxy.js b/test/proxy.js new file mode 100644 index 0000000..d037d24 --- /dev/null +++ b/test/proxy.js @@ -0,0 +1,330 @@ +var assert = require('assert'); +var nodeCometD = require('..'); +var http = require('http'); +var url = require('url'); + +describe('proxy', function() { + var _lib = require('cometd'); + var _proxy; + + afterEach(function() { + if (_proxy) { + _proxy.close(); + } + }); + + it('proxies cometd calls', function(done) { + _proxy = http.createServer(function(request, response) { + var serverPort = parseInt(url.parse(request.url).port); + assert.ok(Number.isInteger(serverPort)); + assert.notStrictEqual(serverPort, _proxy.address().port); + + var content = ''; + request.addListener('data', function(chunk) { + content += chunk; + }); + request.addListener('end', function() { + response.statusCode = 200; + response.setHeader('Content-Type', 'application/json'); + var content = '[{' + + '"id":"1",' + + '"version":"1.0",' + + '"channel":"/meta/handshake",' + + '"clientId":"0123456789abcdef",' + + '"supportedConnectionTypes":["long-polling"],' + + '"advice":{"reconnect":"none"},' + + '"successful":true' + + '}]'; + response.end(content, 'utf8'); + }); + }); + _proxy.listen(0, 'localhost', function() { + var proxyPort = _proxy.address().port; + console.log('proxy listening on localhost:' + proxyPort); + + nodeCometD.adapt({ + httpProxy: { + uri: 'http://localhost:' + proxyPort + } + }); + + // Any port will do for the server. + var serverPort = proxyPort + 1; + var cometd = new _lib.CometD(); + cometd.configure({ + url: 'http://localhost:' + serverPort + '/cometd', + logLevel: 'info' + }); + cometd.handshake(function(r) { + if (r.successful) { + done(); + } else { + // Stop /meta/handshake retries. + cometd.disconnect(); + done(new Error('could not handshake')); + } + }); + }); + }); + + it('proxies with includes list', function(done) { + _proxy = http.createServer(function(request, response) { + var serverPort = parseInt(url.parse(request.url).port); + assert.ok(Number.isInteger(serverPort)); + assert.notStrictEqual(serverPort, _proxy.address().port); + + var content = ''; + request.addListener('data', function(chunk) { + content += chunk; + }); + request.addListener('end', function() { + response.statusCode = 200; + response.setHeader('Content-Type', 'application/json'); + var content = '[{' + + '"id":"1",' + + '"version":"1.0",' + + '"channel":"/meta/handshake",' + + '"clientId":"0123456789abcdef",' + + '"supportedConnectionTypes":["long-polling"],' + + '"advice":{"reconnect":"none"},' + + '"successful":true' + + '}]'; + response.end(content, 'utf8'); + }); + }); + _proxy.listen(0, 'localhost', function() { + var proxyPort = _proxy.address().port; + console.log('proxy listening on localhost:' + proxyPort); + // Any port will do for the server. + var serverPort1 = proxyPort + 1; + var serverPort2 = proxyPort + 2; + + nodeCometD.adapt({ + httpProxy: { + uri: 'http://localhost:' + proxyPort, + includes: ['localhost:' + serverPort1] + } + }); + + var cometd1 = new _lib.CometD(); + cometd1.configure({ + url: 'http://localhost:' + serverPort1 + '/cometd', + logLevel: 'info' + }); + cometd1.handshake(function(r) { + if (r.successful) { + var cometd2 = new _lib.CometD(); + cometd2.configure({ + url: 'http://localhost:' + serverPort2 + '/cometd', + logLevel: 'info' + }); + cometd2.handshake(function(r) { + if (r.successful) { + done(new Error('must not handshake')); + } else { + // Stop /meta/handshake retries. + cometd2.disconnect(); + done(); + } + }); + } else { + cometd1.disconnect(); + done(new Error('could not handshake')); + } + }); + }); + }); + + it('proxies with excludes list', function(done) { + _proxy = http.createServer(function(request, response) { + var serverPort = parseInt(url.parse(request.url).port); + assert.ok(Number.isInteger(serverPort)); + assert.notStrictEqual(serverPort, _proxy.address().port); + + var content = ''; + request.addListener('data', function(chunk) { + content += chunk; + }); + request.addListener('end', function() { + response.statusCode = 200; + response.setHeader('Content-Type', 'application/json'); + var content = '[{' + + '"id":"1",' + + '"version":"1.0",' + + '"channel":"/meta/handshake",' + + '"clientId":"0123456789abcdef",' + + '"supportedConnectionTypes":["long-polling"],' + + '"advice":{"reconnect":"none"},' + + '"successful":true' + + '}]'; + response.end(content, 'utf8'); + }); + }); + _proxy.listen(0, 'localhost', function() { + var proxyPort = _proxy.address().port; + console.log('proxy listening on localhost:' + proxyPort); + // Any port will do for the server. + var serverPort1 = proxyPort + 1; + var serverPort2 = proxyPort + 2; + + nodeCometD.adapt({ + httpProxy: { + uri: 'http://localhost:' + proxyPort, + excludes: ['local.*:' + serverPort1] + } + }); + + var cometd1 = new _lib.CometD(); + cometd1.configure({ + url: 'http://localhost:' + serverPort1 + '/cometd', + logLevel: 'info' + }); + cometd1.handshake(function(r) { + if (r.successful) { + done(new Error('could not handshake')); + } else { + // Stop /meta/handshake retries. + cometd1.disconnect(); + var cometd2 = new _lib.CometD(); + cometd2.configure({ + url: 'http://localhost:' + serverPort2 + '/cometd', + logLevel: 'info' + }); + cometd2.handshake(function(r) { + if (r.successful) { + done(); + } else { + // Stop /meta/handshake retries. + cometd2.disconnect(); + done(new Error('must not handshake')); + } + }); + } + }); + }); + }); + + it('proxies with includes and excludes list', function(done) { + _proxy = http.createServer(function(request, response) { + var serverPort = parseInt(url.parse(request.url).port); + assert.ok(Number.isInteger(serverPort)); + assert.notStrictEqual(serverPort, _proxy.address().port); + + var content = ''; + request.addListener('data', function(chunk) { + content += chunk; + }); + request.addListener('end', function() { + response.statusCode = 200; + response.setHeader('Content-Type', 'application/json'); + var content = '[{' + + '"id":"1",' + + '"version":"1.0",' + + '"channel":"/meta/handshake",' + + '"clientId":"0123456789abcdef",' + + '"supportedConnectionTypes":["long-polling"],' + + '"advice":{"reconnect":"none"},' + + '"successful":true' + + '}]'; + response.end(content, 'utf8'); + }); + }); + _proxy.listen(0, 'localhost', function() { + var proxyPort = _proxy.address().port; + console.log('proxy listening on localhost:' + proxyPort); + // Any port will do for the server. + var serverPort1 = proxyPort + 1; + var serverPort2 = proxyPort + 2; + + nodeCometD.adapt({ + httpProxy: { + uri: 'http://localhost:' + proxyPort, + includes: ['.*:' + serverPort1, '.*host:' + serverPort2], + excludes: ['local.*:' + serverPort1] + } + }); + + var cometd1 = new _lib.CometD(); + cometd1.configure({ + url: 'http://localhost:' + serverPort1 + '/cometd', + logLevel: 'info' + }); + cometd1.handshake(function(r) { + if (r.successful) { + done(new Error('could not handshake')); + } else { + // Stop /meta/handshake retries. + cometd1.disconnect(); + var cometd2 = new _lib.CometD(); + cometd2.configure({ + url: 'http://localhost:' + serverPort2 + '/cometd', + logLevel: 'info' + }); + cometd2.handshake(function(r) { + if (r.successful) { + done(); + } else { + // Stop /meta/handshake retries. + cometd2.disconnect(); + done(new Error('must not handshake')); + } + }); + } + }); + }); + }); + + it('proxies with authentication', function(done) { + _proxy = http.createServer(function(request, response) { + var proxyAuth = request.headers['proxy-authorization']; + assert.ok(proxyAuth); + assert.ok(proxyAuth.startsWith('Basic ')); + + var content = ''; + request.addListener('data', function(chunk) { + content += chunk; + }); + request.addListener('end', function() { + response.statusCode = 200; + response.setHeader('Content-Type', 'application/json'); + var content = '[{' + + '"id":"1",' + + '"version":"1.0",' + + '"channel":"/meta/handshake",' + + '"clientId":"0123456789abcdef",' + + '"supportedConnectionTypes":["long-polling"],' + + '"advice":{"reconnect":"none"},' + + '"successful":true' + + '}]'; + response.end(content, 'utf8'); + }); + }); + _proxy.listen(0, 'localhost', function() { + var proxyPort = _proxy.address().port; + console.log('proxy listening on localhost:' + proxyPort); + + nodeCometD.adapt({ + httpProxy: { + uri: 'http://user:password@localhost:' + proxyPort + } + }); + + // Any port will do for the server. + var serverPort = proxyPort + 1; + var cometd = new _lib.CometD(); + cometd.configure({ + url: 'http://localhost:' + serverPort + '/cometd', + logLevel: 'info' + }); + cometd.handshake(function(r) { + if (r.successful) { + done(); + } else { + // Stop /meta/handshake retries. + cometd.disconnect(); + done(new Error('could not handshake')); + } + }); + }); + }); +});