diff --git a/README.md b/README.md index 91e9126..f685cdd 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ The complete list of options to `globalTunnel.initialize`: - **host** - the hostname or IP of the HTTP proxy to use - **port** - the TCP port to use on that proxy -- **tunnel** _(optional)_ controls what protocols use the `CONNECT` method. It +- **connect** _(optional)_ controls what protocols use the `CONNECT` method. It has three possible values (strings): - **neither** - don't use `CONNECT`; just use absolute URIs - **https** - _(the default)_ only use `CONNECT` for HTTPS requests @@ -55,6 +55,10 @@ The complete list of options to `globalTunnel.initialize`: - **sockets** - _(optional)_ maximum number of TCP sockets to use in each pool. There are two pools: one for HTTP and one for HTTPS. Uses node's default (5) if falsy. +- **proxyAuth** - _(optional)_ a string to use for Basic HTTP authentication. + This gets base64-encoded then sent as part of the standard + `Proxy-Authorization` header on `CONNECT` requests. Typically, this is of the + form `username:password`. ## Variations diff --git a/index.js b/index.js index fd4cfe0..f6209a3 100644 --- a/index.js +++ b/index.js @@ -54,8 +54,14 @@ globalTunnel.isProxying = false; * @param {object} conf * @param {string} conf.host * @param {int} conf.port + * @param {string} conf.protocol the protocol the proxy speaks, 'http:' or + * 'https:' + * @param {string} [conf.connect] controls what protocols to use CONNECT with. + * See README for possible values (e.g. "neither", "https", "both"). Default is + * "https". * @param {int} [conf.sockets] maximum number of sockets to pool (falsy uses * node's default). + * @param {string} [conf.proxyAuth] Basic authentication string */ globalTunnel.initialize = function(conf) { if (globalTunnel.isProxying) { @@ -139,7 +145,7 @@ globalTunnel._makeAgent = function(conf, innerProtocol, useCONNECT) { innerProtocol = innerProtocol + ':'; var opts = { - proxy: _.pick(conf, 'host','port','protocol','localAddress'), + proxy: _.pick(conf, 'host','port','protocol','localAddress','proxyAuth'), maxSockets: conf.sockets }; opts.proxy.innerProtocol = innerProtocol; diff --git a/lib/agents.js b/lib/agents.js index 088a4f5..4a81cb3 100644 --- a/lib/agents.js +++ b/lib/agents.js @@ -9,6 +9,10 @@ var HttpsAgent = https.Agent; var _ = require('lodash'); +function base64(string) { + return new Buffer(string).toString('base64'); +} + /** * Proxy some traffic over HTTP. */ @@ -52,6 +56,10 @@ function mixinProxying(agent, proxyOpts) { if (this.proxy.localAddress) { localAddress = this.proxy.localAddress; } + if (this.proxy.proxyAuth) { + req.setHeader('Proxy-Authorization', + 'Basic '+base64(this.proxy.proxyAuth)); + } return orig.addRequest.call(this, req, host, port, localAddress); }; } diff --git a/package.json b/package.json index 56a00db..add491b 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,10 @@ }, "dependencies": { "lodash": "1.3.1", - "tunnel": "0.0.2" + "tunnel": "0.0.3" }, "devDependencies": { "mocha": "1.16.2", - "goinstant-assert": "1.1.1", "request": "2.30.0", "sinon": "1.7.3" }, diff --git a/test/index.js b/test/index.js index 5281a75..c676cd1 100644 --- a/test/index.js +++ b/test/index.js @@ -1,7 +1,8 @@ /*jshint node:true*/ 'use strict'; -var assert = require('goinstant-assert'); +var assert = require('assert'); var sinon = require('sinon'); +var _ = require('lodash'); // deliberate: node and 3rd party modules before global-tunnel var EventEmitter = require('events').EventEmitter; @@ -79,25 +80,25 @@ describe('global-proxy', function() { describe('invalid configs', function() { it('requires a host', function() { var conf = { host: null, port: 1234 }; - assert.exception(function() { + assert.throws(function() { globalTunnel.initialize(conf); - }, 'upstream proxy host is required'); + }, /upstream proxy host is required/); globalTunnel.end(); }); it('requires a port', function() { var conf = { host: '10.2.3.4', port: 0 }; - assert.exception(function() { + assert.throws(function() { globalTunnel.initialize(conf); - }, 'upstream proxy port is required'); + }, /upstream proxy port is required/); globalTunnel.end(); }); it('clamps tunnel types', function() { var conf = { host: '10.2.3.4', port: 1234, connect: 'INVALID' }; - assert.exception(function() { + assert.throws(function() { globalTunnel.initialize(conf); - }, 'valid connect options are "neither", "https", or "both"'); + }, /valid connect options are "neither", "https", or "both"/); globalTunnel.end(); }); }); @@ -106,6 +107,7 @@ describe('global-proxy', function() { function connected(innerProto) { var innerSecure = (innerProto === 'https:'); + var whichAgent = innerSecure ? https.globalAgent : http.globalAgent; var called; if (testParams.secure) { @@ -129,13 +131,19 @@ describe('global-proxy', function() { (innerSecure && testParams.connect === 'https'); if (isCONNECT) { var expectConnect = 'example.dev:' + (innerSecure ? 443 : 80); - var whichAgent = innerSecure ? https.globalAgent : http.globalAgent; sinon.assert.calledOnce(whichAgent.request); sinon.assert.calledWith(whichAgent.request, sinon.match.has('method','CONNECT')); sinon.assert.calledWith(whichAgent.request, sinon.match.has('path',expectConnect)); + + if (testParams.auth) { + var headers = whichAgent.request.firstCall.args[0].headers; + assert.equal(headers['Proxy-Authorization'], + 'Basic ' + testParams.auth); + } + } else { sinon.assert.calledOnce(http.Agent.prototype.addRequest); var req = http.Agent.prototype.addRequest.getCall(0).args[0]; @@ -143,11 +151,17 @@ describe('global-proxy', function() { var method = req.method; assert.equal(method, 'GET'); - var path = req.path; + var pattern; if (innerSecure) { - assert.match(path, new RegExp('^https://example\\.dev:443/')); + pattern = new RegExp('^https://example\\.dev:443/'); } else { - assert.match(path, new RegExp('^http://example\\.dev:80/')); + pattern = new RegExp('^http://example\\.dev:80/'); + } + assert(pattern.test(req.path)); + + if (testParams.auth) { + assert.equal(req.getHeader('proxy-authorization'), + 'Basic ' + testParams.auth); } } } @@ -167,12 +181,12 @@ describe('global-proxy', function() { }); it('(got proxying set up)', function() { - assert.isTrue(globalTunnel.isProxying); + assert(globalTunnel.isProxying); }); describe('with the request library', function() { it('will proxy http requests', function(done) { - assert.isTrue(globalTunnel.isProxying); + assert(globalTunnel.isProxying); var dummyCb = sinon.stub(); request.get('http://example.dev/', dummyCb); setImmediate(function() { @@ -184,7 +198,7 @@ describe('global-proxy', function() { }); it('will proxy https requests', function(done) { - assert.isTrue(globalTunnel.isProxying); + assert(globalTunnel.isProxying); var dummyCb = sinon.stub(); request.get('https://example.dev/', dummyCb); setImmediate(function() { @@ -278,62 +292,62 @@ describe('global-proxy', function() { proxyEnabledTests(testParams); } - describe('with http proxy in intercept mode', function() { - var conf = { - connect: 'neither', - protocol: 'http:', - host: '10.2.3.4', - port: 3333 - }; - enabledBlock(conf, { secure: false, connect: 'neither', port: 3333 }); - }); - - describe('with https proxy in intercept mode', function() { - var conf = { - connect: 'neither', - protocol: 'https:', - host: '10.2.3.4', - port: 3334 - }; - enabledBlock(conf, { secure: true, connect: 'neither', port: 3334 }); - }); - - describe('with http proxy in CONNECT mode', function() { + [ + { + label: 'http proxy in intercept mode', + config: { connect: 'neither', protocol: 'http:' }, + expect: { secure: false, connect: 'neither' } + }, + { + label: 'https proxy in intercept mode', + config: { connect: 'neither', protocol: 'https:' }, + expect: { secure: true, connect: 'neither' } + }, + { + label: 'http proxy in CONNECT mode', + config: { connect: 'both', protocol: 'http:' }, + expect: { secure: false, connect: 'both' } + }, + { + label: 'https proxy in CONNECT mode', + config: { connect: 'both', protocol: 'https:' }, + expect: { secure: true, connect: 'both' } + }, + { + label: 'http proxy in mixed mode', + config: { protocol: 'http:' }, + expect: { secure: false, connect: 'https' } + }, + { + label: 'https proxy in mixed mode', + config: { protocol: 'https:' }, + expect: { secure: true, connect: 'https' } + } + ].forEach(function(testCase) { var conf = { - connect: 'both', - protocol: 'http:', + protocol: testCase.config.protocol, + connect: testCase.config.connect, host: '10.2.3.4', - port: 3335 + port: 3333, }; - enabledBlock(conf, { secure: false, connect: 'both', port: 3335 }); - }); + var expect = { + secure: testCase.expect.secure, + connect: testCase.expect.connect, + port: 3333 + } - describe('with https proxy in CONNECT mode', function() { - var conf = { - connect: 'both', - protocol: 'https:', - host: '10.2.3.4', - port: 3336 - }; - enabledBlock(conf, { secure: true, connect: 'both', port: 3336 }); - }); + var authConf = _.clone(conf); + var authExpect = _.clone(expect); + authConf.proxyAuth = 'un:pw'; + authExpect.auth = 'dW46cHc='; - describe('with http proxy in mixed mode', function() { - var conf = { - protocol: 'http:', - host: '10.2.3.4', - port: 3337 - }; - enabledBlock(conf, { secure: false, connect: 'https', port: 3337 }); - }); + describe('with '+testCase.label, function() { + enabledBlock(conf, expect); + }); - describe('with https proxy in mixed mode', function() { - var conf = { - protocol: 'https:', - host: '10.2.3.4', - port: 3338 - }; - enabledBlock(conf, { secure: true, connect: 'https', port: 3338 }); + describe('with authenticated '+testCase.label, function() { + enabledBlock(authConf, authExpect); + }); }); @@ -361,7 +375,7 @@ describe('global-proxy', function() { // deliberately after the block above describe('with proxy disabled', function() { it('claims to be disabled', function() { - assert.isFalse(globalTunnel.isProxying); + assert(!globalTunnel.isProxying); }); it('will NOT proxy http requests', function(done) {