From 6cce30b3eb27be9bfdb6a3c9ee814d4613e2b227 Mon Sep 17 00:00:00 2001 From: Neil Manvar Date: Fri, 17 Nov 2017 00:52:32 -0800 Subject: [PATCH 1/5] Add support for sending events through proxy For both HTTP and HTTPS endpoints. Uses tunnel-agent for https. --- lib/transports.js | 64 +++++++++++++++++++++++++++++++++++++++++------ package.json | 1 + 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/lib/transports.js b/lib/transports.js index a606a95..5933578 100644 --- a/lib/transports.js +++ b/lib/transports.js @@ -6,8 +6,12 @@ var timeoutReq = require('timed-out'); var http = require('http'); var https = require('https'); +var tunnel = require('tunnel-agent'); -var agentOptions = {keepAlive: true, maxSockets: 100}; +var agentOptions = { + keepAlive: true, + maxSockets: 100 +}; var httpAgent = new http.Agent(agentOptions); var httpsAgent = new https.Agent(agentOptions); @@ -21,7 +25,7 @@ function HTTPTransport(options) { this.agent = httpAgent; } util.inherits(HTTPTransport, Transport); -HTTPTransport.prototype.send = function(client, message, headers, eventId, cb) { +HTTPTransport.prototype.send = function (client, message, headers, eventId, cb) { var options = { hostname: client.dsn.host, path: client.dsn.path + 'api/' + client.dsn.project_id + '/store/', @@ -31,6 +35,20 @@ HTTPTransport.prototype.send = function(client, message, headers, eventId, cb) { ca: client.ca, agent: this.agent }; + + // set path apprpriately when using http endpoint + proxy, set proxy headers appropriately when using https endpoint + proxy + if (this.options.hasOwnProperty('proxyHost')) { + if (client.dsn.protocol === 'http') { + options.path = 'http://' + client.dsn.host + ':' + client.dsn.port + client.dsn.path + 'api/' + client.dsn.project_id + '/store/'; + delete options.hostname; // only 'host' should be set when using proxy + } else { + this.options.agent.proxyOptions.headers = { + 'Content-Type': 'application/octet-stream', + host: client.dsn.host + ':' + client.dsn.port + } + } + } + for (var key in this.options) { if (this.options.hasOwnProperty(key)) { options[key] = this.options[key]; @@ -38,7 +56,10 @@ HTTPTransport.prototype.send = function(client, message, headers, eventId, cb) { } // prevent off heap memory explosion - var _name = this.agent.getName({host: client.dsn.host, port: client.dsn.port}); + var _name = this.agent.getName({ + host: client.dsn.host, + port: client.dsn.port + }); var _requests = this.agent.requests[_name]; if (_requests && Object.keys(_requests).length > client.maxReqQueueCount) { // other feedback strategy @@ -46,7 +67,7 @@ HTTPTransport.prototype.send = function(client, message, headers, eventId, cb) { return; } - var req = this.transport.request(options, function(res) { + var req = this.transport.request(options, function (res) { res.setEncoding('utf8'); if (res.statusCode >= 200 && res.statusCode < 300) { client.emit('logged', eventId); @@ -63,9 +84,8 @@ HTTPTransport.prototype.send = function(client, message, headers, eventId, cb) { client.emit('error', e); cb && cb(e); } - // force the socket to drain - var noop = function() {}; + var noop = function () {}; res.on('data', noop); res.on('end', noop); }); @@ -73,7 +93,7 @@ HTTPTransport.prototype.send = function(client, message, headers, eventId, cb) { timeoutReq(req, client.sendTimeout * 1000); var cbFired = false; - req.on('error', function(e) { + req.on('error', function (e) { client.emit('error', e); if (!cbFired) { cb && cb(e); @@ -89,10 +109,40 @@ function HTTPSTransport(options) { this.options = options || {}; this.agent = httpsAgent; } + +function HTTPProxyTransport(options) { + this.defaultPort = 80; + this.transport = http; + this.options = options || {}; + this.agent = httpAgent; + this.options.host = options.proxyHost; + this.options.port = options.proxyPort; +} + +function HTTPSProxyTransport(options) { + this.transport = https; + this.options = options || {}; + this.agent = httpsAgent; + this.options.agent = tunnel.httpsOverHttp({ + proxy: { + host: options.proxyHost, + port: options.proxyPort, + proxyAuth: null // TODO: Add ability to specify creds/auth + }, + keepAlive: agentOptions.keepAlive, + maxSockets: agentOptions.maxSockets + }); +} + util.inherits(HTTPSTransport, HTTPTransport); +util.inherits(HTTPProxyTransport, HTTPTransport); +util.inherits(HTTPSProxyTransport, HTTPTransport); module.exports.http = new HTTPTransport(); module.exports.https = new HTTPSTransport(); module.exports.Transport = Transport; module.exports.HTTPTransport = HTTPTransport; module.exports.HTTPSTransport = HTTPSTransport; + +module.exports.HTTPProxyTransport = HTTPProxyTransport; +module.exports.HTTPSProxyTransport = HTTPSProxyTransport; diff --git a/package.json b/package.json index ebeec85..0899449 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "lsmod": "1.0.0", "stack-trace": "0.0.9", "timed-out": "4.0.1", + "tunnel-agent": "^0.6.0", "uuid": "3.0.0" }, "devDependencies": { From de6da9ec5b6c4835d1fff859233a783d75e8a10d Mon Sep 17 00:00:00 2001 From: Neil Manvar Date: Thu, 4 Jan 2018 16:35:52 -0800 Subject: [PATCH 2/5] Add proxyAuth (ability to specify user/password for proxy) --- lib/transports.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/transports.js b/lib/transports.js index 5933578..0ad457b 100644 --- a/lib/transports.js +++ b/lib/transports.js @@ -7,6 +7,7 @@ var timeoutReq = require('timed-out'); var http = require('http'); var https = require('https'); var tunnel = require('tunnel-agent'); +var Buffer = require("safe-buffer").Buffer; var agentOptions = { keepAlive: true, @@ -41,6 +42,11 @@ HTTPTransport.prototype.send = function (client, message, headers, eventId, cb) if (client.dsn.protocol === 'http') { options.path = 'http://' + client.dsn.host + ':' + client.dsn.port + client.dsn.path + 'api/' + client.dsn.project_id + '/store/'; delete options.hostname; // only 'host' should be set when using proxy + + if (this.options.proxyAuth) { + // might be able to use httpOverHttp agent + options.headers["Proxy-Authorization"] = "Basic " + Buffer.from(this.options.proxyAuth).toString("base64"); + } } else { this.options.agent.proxyOptions.headers = { 'Content-Type': 'application/octet-stream', @@ -117,6 +123,10 @@ function HTTPProxyTransport(options) { this.agent = httpAgent; this.options.host = options.proxyHost; this.options.port = options.proxyPort; + + if (options.proxyUser && options.proxyPassword) { + this.options.proxyAuth = options.proxyUser + ':' + options.proxyPassword; + } } function HTTPSProxyTransport(options) { @@ -127,7 +137,10 @@ function HTTPSProxyTransport(options) { proxy: { host: options.proxyHost, port: options.proxyPort, - proxyAuth: null // TODO: Add ability to specify creds/auth + proxyAuth: + options.proxyUser && options.proxyPassword + ? options.proxyUser + ':' + options.proxyPassword + : null }, keepAlive: agentOptions.keepAlive, maxSockets: agentOptions.maxSockets From 9a8a4c220a285522777ee85941f198432936fa1f Mon Sep 17 00:00:00 2001 From: Neil Manvar Date: Thu, 4 Jan 2018 19:11:08 -0800 Subject: [PATCH 3/5] remove require buffer --- lib/transports.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/transports.js b/lib/transports.js index 0ad457b..c81e21b 100644 --- a/lib/transports.js +++ b/lib/transports.js @@ -7,8 +7,6 @@ var timeoutReq = require('timed-out'); var http = require('http'); var https = require('https'); var tunnel = require('tunnel-agent'); -var Buffer = require("safe-buffer").Buffer; - var agentOptions = { keepAlive: true, maxSockets: 100 From 8abbf65cb95149e388ec842b79ec98285ac91f93 Mon Sep 17 00:00:00 2001 From: Neil Manvar Date: Fri, 5 Jan 2018 01:33:18 -0800 Subject: [PATCH 4/5] Add initial test --- test/raven.transports.js | 66 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/test/raven.transports.js b/test/raven.transports.js index 87da3dc..a70d3db 100644 --- a/test/raven.transports.js +++ b/test/raven.transports.js @@ -15,6 +15,72 @@ describe('transports', function() { https.agent.maxSockets.should.equal(100); }); + it('should set proxy agent when proxy config is specified and request is sent', function(done) { + var option = { + proxyHost: 'localhost', + proxyPort: '8080' + }; + + // HTTP + var httpProxyTransport = new transports.HTTPProxyTransport(option); + httpProxyTransport.options.host.should.equal('localhost'); + httpProxyTransport.options.port.should.equal('8080'); + + // HTTPS + var httpsProxyTransport = new transports.HTTPSProxyTransport(option); + httpsProxyTransport.options.agent.proxyOptions.should.exist; + + var _cachedAgent = httpProxyTransport.agent; + var requests = {}; + for (var i = 0; i < 10; i++) { + requests[i] = 'req'; + } + + httpProxyTransport.agent = Object.assign({}, _cachedAgent, { + getName: function() { + return 'foo:123'; + }, + requests: { + 'foo:123': requests + } + }); + + httpsProxyTransport.send( + { + dsn: { + host: 'foo', + port: 123 + }, + emit: function() {} + }, + null, + null, + null, + function() { + httpsProxyTransport.options.agent.proxyOptions.headers.host.should.exist; + done(); + } + ); + + httpProxyTransport.send( + { + dsn: { + host: 'foo', + port: 123, + protocol: 'http' + }, + emit: function() {} + }, + null, + null, + null, + function() { + httpProxyTransport.options.path.should.contain('foo:123'); + done(); + } + ); + }); + it('should emit error when requests queued over the limit', function(done) { var http = transports.http; var _cachedAgent = http.options.agent; From 1bc3de565e26be273d8097be95e35e3216146fc3 Mon Sep 17 00:00:00 2001 From: Neil Manvar Date: Fri, 5 Jan 2018 13:15:31 -0800 Subject: [PATCH 5/5] Make change to this.options and fix unit tests --- lib/transports.js | 23 +++++++++++++++------- test/raven.transports.js | 42 +++++++++++++++++++++++++++++----------- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/lib/transports.js b/lib/transports.js index c81e21b..6bd9e29 100644 --- a/lib/transports.js +++ b/lib/transports.js @@ -24,7 +24,7 @@ function HTTPTransport(options) { this.agent = httpAgent; } util.inherits(HTTPTransport, Transport); -HTTPTransport.prototype.send = function (client, message, headers, eventId, cb) { +HTTPTransport.prototype.send = function(client, message, headers, eventId, cb) { var options = { hostname: client.dsn.host, path: client.dsn.path + 'api/' + client.dsn.project_id + '/store/', @@ -38,18 +38,27 @@ HTTPTransport.prototype.send = function (client, message, headers, eventId, cb) // set path apprpriately when using http endpoint + proxy, set proxy headers appropriately when using https endpoint + proxy if (this.options.hasOwnProperty('proxyHost')) { if (client.dsn.protocol === 'http') { - options.path = 'http://' + client.dsn.host + ':' + client.dsn.port + client.dsn.path + 'api/' + client.dsn.project_id + '/store/'; + this.options.path = + 'http://' + + client.dsn.host + + ':' + + client.dsn.port + + client.dsn.path + + 'api/' + + client.dsn.project_id + + '/store/'; delete options.hostname; // only 'host' should be set when using proxy if (this.options.proxyAuth) { // might be able to use httpOverHttp agent - options.headers["Proxy-Authorization"] = "Basic " + Buffer.from(this.options.proxyAuth).toString("base64"); + this.options.headers['Proxy-Authorization'] = + 'Basic ' + Buffer.from(this.options.proxyAuth).toString('base64'); } } else { this.options.agent.proxyOptions.headers = { 'Content-Type': 'application/octet-stream', host: client.dsn.host + ':' + client.dsn.port - } + }; } } @@ -71,7 +80,7 @@ HTTPTransport.prototype.send = function (client, message, headers, eventId, cb) return; } - var req = this.transport.request(options, function (res) { + var req = this.transport.request(options, function(res) { res.setEncoding('utf8'); if (res.statusCode >= 200 && res.statusCode < 300) { client.emit('logged', eventId); @@ -89,7 +98,7 @@ HTTPTransport.prototype.send = function (client, message, headers, eventId, cb) cb && cb(e); } // force the socket to drain - var noop = function () {}; + var noop = function() {}; res.on('data', noop); res.on('end', noop); }); @@ -97,7 +106,7 @@ HTTPTransport.prototype.send = function (client, message, headers, eventId, cb) timeoutReq(req, client.sendTimeout * 1000); var cbFired = false; - req.on('error', function (e) { + req.on('error', function(e) { client.emit('error', e); if (!cbFired) { cb && cb(e); diff --git a/test/raven.transports.js b/test/raven.transports.js index a70d3db..a3e6ae1 100644 --- a/test/raven.transports.js +++ b/test/raven.transports.js @@ -15,33 +15,28 @@ describe('transports', function() { https.agent.maxSockets.should.equal(100); }); - it('should set proxy agent when proxy config is specified and request is sent', function(done) { + it('should set HTTPS proxy transport when proxy config is specified and request is sent', function(done) { var option = { proxyHost: 'localhost', proxyPort: '8080' }; - // HTTP - var httpProxyTransport = new transports.HTTPProxyTransport(option); - httpProxyTransport.options.host.should.equal('localhost'); - httpProxyTransport.options.port.should.equal('8080'); - // HTTPS var httpsProxyTransport = new transports.HTTPSProxyTransport(option); httpsProxyTransport.options.agent.proxyOptions.should.exist; - var _cachedAgent = httpProxyTransport.agent; + var _cachedAgent = httpsProxyTransport.agent; var requests = {}; for (var i = 0; i < 10; i++) { requests[i] = 'req'; } - httpProxyTransport.agent = Object.assign({}, _cachedAgent, { + httpsProxyTransport.agent = Object.assign({}, _cachedAgent, { getName: function() { return 'foo:123'; }, requests: { - 'foo:123': requests + 'foo:123': {} } }); @@ -61,13 +56,38 @@ describe('transports', function() { done(); } ); + }); + + it('should set HTTP proxy transport when proxy config is specified and request is sent', function(done) { + var option = { + proxyHost: 'localhost', + proxyPort: '8080' + }; + + // HTTP + var httpProxyTransport = new transports.HTTPProxyTransport(option); + httpProxyTransport.options.host.should.equal('localhost'); + httpProxyTransport.options.port.should.equal('8080'); + + var _cachedAgent = httpProxyTransport.agent; + httpProxyTransport.agent = Object.assign({}, _cachedAgent, { + getName: function() { + return 'foo:123'; + }, + requests: { + 'foo:123': {} + } + }); + httpProxyTransport.options.agent = _cachedAgent; httpProxyTransport.send( { dsn: { host: 'foo', port: 123, - protocol: 'http' + protocol: 'http', + path: '/', + project_id: '1' }, emit: function() {} }, @@ -75,7 +95,7 @@ describe('transports', function() { null, null, function() { - httpProxyTransport.options.path.should.contain('foo:123'); + httpProxyTransport.options.path.should.equal('http://foo:123/api/1/store/'); done(); } );