diff --git a/lib/transports.js b/lib/transports.js index a606a95..6bd9e29 100644 --- a/lib/transports.js +++ b/lib/transports.js @@ -6,8 +6,11 @@ var timeoutReq = require('timed-out'); var http = require('http'); var https = require('https'); - -var agentOptions = {keepAlive: true, maxSockets: 100}; +var tunnel = require('tunnel-agent'); +var agentOptions = { + keepAlive: true, + maxSockets: 100 +}; var httpAgent = new http.Agent(agentOptions); var httpsAgent = new https.Agent(agentOptions); @@ -31,6 +34,34 @@ 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') { + 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 + 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 + }; + } + } + for (var key in this.options) { if (this.options.hasOwnProperty(key)) { options[key] = this.options[key]; @@ -38,7 +69,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 @@ -63,7 +97,6 @@ HTTPTransport.prototype.send = function(client, message, headers, eventId, cb) { client.emit('error', e); cb && cb(e); } - // force the socket to drain var noop = function() {}; res.on('data', noop); @@ -89,10 +122,47 @@ 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; + + if (options.proxyUser && options.proxyPassword) { + this.options.proxyAuth = options.proxyUser + ':' + options.proxyPassword; + } +} + +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: + options.proxyUser && options.proxyPassword + ? options.proxyUser + ':' + options.proxyPassword + : null + }, + 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 aabdea4..35ebe35 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "md5": "^2.2.1", "stack-trace": "0.0.10", "timed-out": "4.0.1", + "tunnel-agent": "^0.6.0", "uuid": "3.0.0" }, "devDependencies": { diff --git a/test/raven.transports.js b/test/raven.transports.js index 87da3dc..a3e6ae1 100644 --- a/test/raven.transports.js +++ b/test/raven.transports.js @@ -15,6 +15,92 @@ describe('transports', function() { https.agent.maxSockets.should.equal(100); }); + it('should set HTTPS proxy transport when proxy config is specified and request is sent', function(done) { + var option = { + proxyHost: 'localhost', + proxyPort: '8080' + }; + + // HTTPS + var httpsProxyTransport = new transports.HTTPSProxyTransport(option); + httpsProxyTransport.options.agent.proxyOptions.should.exist; + + var _cachedAgent = httpsProxyTransport.agent; + var requests = {}; + for (var i = 0; i < 10; i++) { + requests[i] = 'req'; + } + + httpsProxyTransport.agent = Object.assign({}, _cachedAgent, { + getName: function() { + return 'foo:123'; + }, + requests: { + 'foo:123': {} + } + }); + + httpsProxyTransport.send( + { + dsn: { + host: 'foo', + port: 123 + }, + emit: function() {} + }, + null, + null, + null, + function() { + httpsProxyTransport.options.agent.proxyOptions.headers.host.should.exist; + 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', + path: '/', + project_id: '1' + }, + emit: function() {} + }, + null, + null, + null, + function() { + httpProxyTransport.options.path.should.equal('http://foo:123/api/1/store/'); + done(); + } + ); + }); + it('should emit error when requests queued over the limit', function(done) { var http = transports.http; var _cachedAgent = http.options.agent;