536 lines
15 KiB
JavaScript
536 lines
15 KiB
JavaScript
|
'use strict';
|
||
|
var assert = require('chai').assert;
|
||
|
var sinon = require('sinon');
|
||
|
var assign = require('lodash/assign');
|
||
|
var pick = require('lodash/pick');
|
||
|
|
||
|
// Deliberate: node and 3rd party modules before global-tunnel
|
||
|
var EventEmitter = require('events').EventEmitter;
|
||
|
var net = require('net');
|
||
|
var tls = require('tls');
|
||
|
var http = require('http');
|
||
|
var globalHttpAgent = http.globalAgent;
|
||
|
var https = require('https');
|
||
|
var globalHttpsAgent = https.globalAgent;
|
||
|
var request = require('request');
|
||
|
|
||
|
// Deliberate: load after all 3rd party modules
|
||
|
var globalTunnel = require('../index');
|
||
|
|
||
|
function newFakeAgent() {
|
||
|
var fakeAgent = {
|
||
|
addRequest: sinon.stub()
|
||
|
};
|
||
|
return fakeAgent;
|
||
|
}
|
||
|
|
||
|
// This function replaces 'host' by 'hostname' in the options for http.request()
|
||
|
// background: http.request() allows to use either 'host' or 'hostname' to be used,
|
||
|
// both needs to be tested
|
||
|
function replaceHostByHostname(useHostname, options) {
|
||
|
if (useHostname) {
|
||
|
options.hostname = options.host;
|
||
|
delete options.host;
|
||
|
}
|
||
|
return options;
|
||
|
}
|
||
|
|
||
|
var origEnv;
|
||
|
function saveEnv() {
|
||
|
origEnv = process.env.http_proxy;
|
||
|
delete process.env.http_proxy;
|
||
|
}
|
||
|
function restoreEnv() {
|
||
|
if (origEnv !== undefined) {
|
||
|
process.env.http_proxy = origEnv; // eslint-disable-line camelcase
|
||
|
}
|
||
|
}
|
||
|
|
||
|
describe('global-proxy', function() {
|
||
|
// Save and restore http_proxy environment variable (yes, it's lower-case by
|
||
|
// convention).
|
||
|
before(saveEnv);
|
||
|
after(restoreEnv);
|
||
|
|
||
|
// Sinon setup & teardown
|
||
|
var sandbox;
|
||
|
var origHttpCreateConnection;
|
||
|
|
||
|
before(function() {
|
||
|
sandbox = sinon.createSandbox();
|
||
|
|
||
|
sandbox.stub(globalHttpAgent, 'addRequest');
|
||
|
sandbox.stub(globalHttpsAgent, 'addRequest');
|
||
|
|
||
|
assert.equal(http.Agent.prototype.addRequest, https.Agent.prototype.addRequest);
|
||
|
sandbox.spy(http.Agent.prototype, 'addRequest');
|
||
|
|
||
|
sandbox.stub(net, 'createConnection').callsFake(function() {
|
||
|
return new EventEmitter();
|
||
|
});
|
||
|
sandbox.stub(tls, 'connect').callsFake(function() {
|
||
|
return new EventEmitter();
|
||
|
});
|
||
|
|
||
|
// This is needed as at some point Node HTTP aggent implementation started
|
||
|
// plucking the createConnection method from the `net` module
|
||
|
// instead of doing `net.createConnection`
|
||
|
origHttpCreateConnection = http.Agent.prototype.createConnection;
|
||
|
http.Agent.prototype.createConnection = net.createConnection;
|
||
|
});
|
||
|
|
||
|
afterEach(function() {
|
||
|
sandbox.resetHistory();
|
||
|
});
|
||
|
|
||
|
after(function() {
|
||
|
sandbox.restore();
|
||
|
http.Agent.prototype.createConnection = origHttpCreateConnection;
|
||
|
});
|
||
|
|
||
|
describe('invalid configs', function() {
|
||
|
it('requires a host', function() {
|
||
|
var conf = { host: null, port: 1234 };
|
||
|
assert.throws(function() {
|
||
|
globalTunnel.initialize(conf);
|
||
|
}, 'upstream proxy host is required');
|
||
|
globalTunnel.end();
|
||
|
});
|
||
|
|
||
|
it('requires a port', function() {
|
||
|
var conf = { host: '10.2.3.4', port: 0 };
|
||
|
assert.throws(function() {
|
||
|
globalTunnel.initialize(conf);
|
||
|
}, 'upstream proxy port is required');
|
||
|
globalTunnel.end();
|
||
|
});
|
||
|
|
||
|
it('clamps tunnel types', function() {
|
||
|
var conf = { host: '10.2.3.4', port: 1234, connect: 'INVALID' };
|
||
|
assert.throws(function() {
|
||
|
globalTunnel.initialize(conf);
|
||
|
}, 'valid connect options are "neither", "https", or "both"');
|
||
|
globalTunnel.end();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('exposed config', function() {
|
||
|
afterEach(function() {
|
||
|
globalTunnel.end();
|
||
|
});
|
||
|
|
||
|
it('has the same params as the passed config', function() {
|
||
|
var conf = {
|
||
|
host: 'proxy.com',
|
||
|
port: 1234,
|
||
|
proxyAuth: 'user:pwd',
|
||
|
protocol: 'https:'
|
||
|
};
|
||
|
globalTunnel.initialize(conf);
|
||
|
assert.deepEqual(
|
||
|
conf,
|
||
|
pick(globalTunnel.proxyConfig, ['host', 'port', 'proxyAuth', 'protocol'])
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('has the expected defaults', function() {
|
||
|
var conf = { host: 'proxy.com', port: 1234, proxyAuth: 'user:pwd' };
|
||
|
globalTunnel.initialize(conf);
|
||
|
assert.equal(globalTunnel.proxyConfig.protocol, 'http:');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('stringified config', function() {
|
||
|
afterEach(function() {
|
||
|
globalTunnel.end();
|
||
|
});
|
||
|
|
||
|
it('has the same params as the passed config', function() {
|
||
|
var conf = {
|
||
|
host: 'proxy.com',
|
||
|
port: 1234,
|
||
|
proxyAuth: 'user:pwd',
|
||
|
protocol: 'https'
|
||
|
};
|
||
|
globalTunnel.initialize(conf);
|
||
|
assert.equal(globalTunnel.proxyUrl, 'https://user:pwd@proxy.com:1234');
|
||
|
});
|
||
|
|
||
|
it('encodes url', function() {
|
||
|
var conf = {
|
||
|
host: 'proxy.com',
|
||
|
port: 1234,
|
||
|
proxyAuth: 'user:4P@S$W0_r-D',
|
||
|
protocol: 'https'
|
||
|
};
|
||
|
globalTunnel.initialize(conf);
|
||
|
assert.equal(globalTunnel.proxyUrl, 'https://user:4P%40S%24W0_r-D@proxy.com:1234');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
function proxyEnabledTests(testParams) {
|
||
|
function connected(innerProto) {
|
||
|
var innerSecure = innerProto === 'https:';
|
||
|
|
||
|
var called;
|
||
|
if (testParams.isHttpsProxy) {
|
||
|
called = tls.connect;
|
||
|
sinon.assert.notCalled(net.createConnection);
|
||
|
} else {
|
||
|
called = net.createConnection;
|
||
|
sinon.assert.notCalled(tls.connect);
|
||
|
}
|
||
|
|
||
|
sinon.assert.calledOnce(called);
|
||
|
if (typeof called.getCall(0).args[0] === 'object') {
|
||
|
sinon.assert.calledWith(called, sinon.match.has('port', testParams.port));
|
||
|
sinon.assert.calledWith(called, sinon.match.has('host', '10.2.3.4'));
|
||
|
} else {
|
||
|
sinon.assert.calledWith(called, testParams.port, '10.2.3.4');
|
||
|
}
|
||
|
|
||
|
var isCONNECT =
|
||
|
testParams.connect === 'both' || (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)
|
||
|
);
|
||
|
} else {
|
||
|
sinon.assert.calledOnce(http.Agent.prototype.addRequest);
|
||
|
var req = http.Agent.prototype.addRequest.getCall(0).args[0];
|
||
|
|
||
|
var method = req.method;
|
||
|
assert.equal(method, 'GET');
|
||
|
|
||
|
var path = req.path;
|
||
|
if (innerSecure) {
|
||
|
assert.match(path, new RegExp('^https://example\\.dev:443/'));
|
||
|
} else {
|
||
|
assert.match(path, new RegExp('^http://example\\.dev:80/'));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var localSandbox;
|
||
|
beforeEach(function() {
|
||
|
localSandbox = sinon.createSandbox();
|
||
|
if (testParams.connect === 'both') {
|
||
|
localSandbox.spy(http.globalAgent, 'request');
|
||
|
}
|
||
|
if (testParams.connect !== 'neither') {
|
||
|
localSandbox.spy(https.globalAgent, 'request');
|
||
|
}
|
||
|
});
|
||
|
afterEach(function() {
|
||
|
localSandbox.restore();
|
||
|
});
|
||
|
|
||
|
it('(got proxying set up)', function() {
|
||
|
assert.isTrue(globalTunnel.isProxying);
|
||
|
});
|
||
|
|
||
|
describe('with the request library', function() {
|
||
|
it('will proxy http requests', function(done) {
|
||
|
assert.isTrue(globalTunnel.isProxying);
|
||
|
var dummyCb = sinon.stub();
|
||
|
request.get('http://example.dev/', dummyCb);
|
||
|
setImmediate(function() {
|
||
|
connected('http:');
|
||
|
sinon.assert.notCalled(globalHttpAgent.addRequest);
|
||
|
sinon.assert.notCalled(globalHttpsAgent.addRequest);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('will proxy https requests', function(done) {
|
||
|
assert.isTrue(globalTunnel.isProxying);
|
||
|
var dummyCb = sinon.stub();
|
||
|
request.get('https://example.dev/', dummyCb);
|
||
|
setImmediate(function() {
|
||
|
connected('https:');
|
||
|
sinon.assert.notCalled(globalHttpAgent.addRequest);
|
||
|
sinon.assert.notCalled(globalHttpsAgent.addRequest);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('using raw request interface', function() {
|
||
|
function rawRequest(useHostname) {
|
||
|
var req = http.request(
|
||
|
replaceHostByHostname(useHostname, {
|
||
|
method: 'GET',
|
||
|
path: '/raw-http',
|
||
|
host: 'example.dev'
|
||
|
}),
|
||
|
function() {}
|
||
|
);
|
||
|
req.end();
|
||
|
|
||
|
connected('http:');
|
||
|
sinon.assert.notCalled(globalHttpAgent.addRequest);
|
||
|
sinon.assert.notCalled(globalHttpsAgent.addRequest);
|
||
|
}
|
||
|
it('will proxy http requests (`host`)', function() {
|
||
|
rawRequest(false);
|
||
|
});
|
||
|
it('will proxy http requests (`hostname`)', function() {
|
||
|
rawRequest(true);
|
||
|
});
|
||
|
|
||
|
it('will proxy https requests', function() {
|
||
|
var req = https.request(
|
||
|
replaceHostByHostname(false, {
|
||
|
method: 'GET',
|
||
|
path: '/raw-https',
|
||
|
host: 'example.dev'
|
||
|
}),
|
||
|
function() {}
|
||
|
);
|
||
|
req.end();
|
||
|
|
||
|
connected('https:');
|
||
|
sinon.assert.notCalled(globalHttpAgent.addRequest);
|
||
|
sinon.assert.notCalled(globalHttpsAgent.addRequest);
|
||
|
});
|
||
|
|
||
|
it('request respects explicit agent param', function() {
|
||
|
var agent = newFakeAgent();
|
||
|
var req = http.request(
|
||
|
replaceHostByHostname(false, {
|
||
|
method: 'GET',
|
||
|
path: '/raw-http-w-agent',
|
||
|
host: 'example.dev',
|
||
|
agent: agent
|
||
|
}),
|
||
|
function() {}
|
||
|
);
|
||
|
req.end();
|
||
|
|
||
|
sinon.assert.notCalled(globalHttpAgent.addRequest);
|
||
|
sinon.assert.notCalled(globalHttpsAgent.addRequest);
|
||
|
sinon.assert.notCalled(net.createConnection);
|
||
|
sinon.assert.notCalled(tls.connect);
|
||
|
sinon.assert.calledOnce(agent.addRequest);
|
||
|
});
|
||
|
|
||
|
describe('request with `null` agent and defined `createConnection`', function() {
|
||
|
before(function() {
|
||
|
sinon.stub(http.ClientRequest.prototype, 'onSocket');
|
||
|
});
|
||
|
after(function() {
|
||
|
http.ClientRequest.prototype.onSocket.restore();
|
||
|
});
|
||
|
|
||
|
function noAgent(useHostname) {
|
||
|
var createConnection = sinon.stub();
|
||
|
var req = http.request(
|
||
|
replaceHostByHostname(useHostname, {
|
||
|
method: 'GET',
|
||
|
path: '/no-agent',
|
||
|
host: 'example.dev',
|
||
|
agent: null,
|
||
|
createConnection: createConnection
|
||
|
}),
|
||
|
function() {} // eslint-disable-line max-nested-callbacks
|
||
|
);
|
||
|
req.end();
|
||
|
|
||
|
sinon.assert.notCalled(globalHttpAgent.addRequest);
|
||
|
sinon.assert.notCalled(globalHttpsAgent.addRequest);
|
||
|
sinon.assert.calledOnce(createConnection);
|
||
|
}
|
||
|
it('uses no agent (`host`)', function() {
|
||
|
noAgent(false);
|
||
|
});
|
||
|
it('uses no agent (`hostname`)', function() {
|
||
|
noAgent(true);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function enabledBlock(conf, testParams) {
|
||
|
before(function() {
|
||
|
globalTunnel.initialize(conf);
|
||
|
});
|
||
|
after(function() {
|
||
|
globalTunnel.end();
|
||
|
});
|
||
|
|
||
|
testParams = assign(
|
||
|
{
|
||
|
port: conf && conf.port,
|
||
|
isHttpsProxy: conf && conf.protocol === 'https:',
|
||
|
connect: (conf && conf.connect) || 'https'
|
||
|
},
|
||
|
testParams
|
||
|
);
|
||
|
|
||
|
proxyEnabledTests(testParams);
|
||
|
}
|
||
|
|
||
|
describe('with http proxy in intercept mode', function() {
|
||
|
enabledBlock({
|
||
|
connect: 'neither',
|
||
|
protocol: 'http:',
|
||
|
host: '10.2.3.4',
|
||
|
port: 3333
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with https proxy in intercept mode', function() {
|
||
|
enabledBlock({
|
||
|
connect: 'neither',
|
||
|
protocol: 'https:',
|
||
|
host: '10.2.3.4',
|
||
|
port: 3334
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with http proxy in CONNECT mode', function() {
|
||
|
enabledBlock({
|
||
|
connect: 'both',
|
||
|
protocol: 'http:',
|
||
|
host: '10.2.3.4',
|
||
|
port: 3335
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with https proxy in CONNECT mode', function() {
|
||
|
enabledBlock({
|
||
|
connect: 'both',
|
||
|
protocol: 'https:',
|
||
|
host: '10.2.3.4',
|
||
|
port: 3336
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with http proxy in mixed mode', function() {
|
||
|
enabledBlock({
|
||
|
protocol: 'http:',
|
||
|
host: '10.2.3.4',
|
||
|
port: 3337
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with https proxy in mixed mode', function() {
|
||
|
enabledBlock({
|
||
|
protocol: 'https:',
|
||
|
host: '10.2.3.4',
|
||
|
port: 3338
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('using env var', function() {
|
||
|
after(function() {
|
||
|
delete process.env.http_proxy;
|
||
|
assert.isUndefined(process.env.http_proxy);
|
||
|
});
|
||
|
|
||
|
describe('for http', function() {
|
||
|
before(function() {
|
||
|
process.env.http_proxy = 'http://10.2.3.4:1234'; // eslint-disable-line camelcase
|
||
|
});
|
||
|
enabledBlock(null, { isHttpsProxy: false, connect: 'https', port: 1234 });
|
||
|
});
|
||
|
|
||
|
describe('for https', function() {
|
||
|
before(function() {
|
||
|
process.env.http_proxy = 'https://10.2.3.4:1235'; // eslint-disable-line camelcase
|
||
|
});
|
||
|
enabledBlock(null, { isHttpsProxy: true, connect: 'https', port: 1235 });
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('using npm config', function() {
|
||
|
var expectedProxy = { isHttpsProxy: false, connect: 'https', port: 1234 };
|
||
|
var npmConfig = { get: function() {} };
|
||
|
var npmConfigStub = sinon.stub(npmConfig, 'get');
|
||
|
|
||
|
function configNpm(key, value) {
|
||
|
return function() {
|
||
|
global.__GLOBAL_TUNNEL_DEPENDENCY_NPMCONF__ = function() {
|
||
|
return npmConfig;
|
||
|
};
|
||
|
|
||
|
npmConfigStub.withArgs(key).returns(value || 'http://10.2.3.4:1234');
|
||
|
};
|
||
|
}
|
||
|
|
||
|
after(function() {
|
||
|
global.__GLOBAL_TUNNEL_DEPENDENCY_NPMCONF__ = undefined;
|
||
|
});
|
||
|
|
||
|
describe('https-proxy', function() {
|
||
|
before(configNpm('https-proxy'));
|
||
|
enabledBlock(null, expectedProxy);
|
||
|
});
|
||
|
|
||
|
describe('http-proxy', function() {
|
||
|
before(configNpm('http-proxy'));
|
||
|
enabledBlock(null, expectedProxy);
|
||
|
});
|
||
|
|
||
|
describe('proxy', function() {
|
||
|
before(configNpm('proxy'));
|
||
|
enabledBlock(null, expectedProxy);
|
||
|
});
|
||
|
describe('order', function() {
|
||
|
before(function() {
|
||
|
configNpm('proxy')();
|
||
|
configNpm('https-proxy', 'http://10.2.3.4:12345')();
|
||
|
configNpm('http-proxy')();
|
||
|
});
|
||
|
|
||
|
enabledBlock(null, { isHttpsProxy: false, connect: 'https', port: 12345 });
|
||
|
});
|
||
|
|
||
|
describe('also using env var', function() {
|
||
|
before(function() {
|
||
|
configNpm('proxy')();
|
||
|
process.env.http_proxy = 'http://10.2.3.4:1234'; // eslint-disable-line camelcase
|
||
|
});
|
||
|
|
||
|
after(function() {
|
||
|
delete process.env.http_proxy;
|
||
|
});
|
||
|
|
||
|
enabledBlock(null, expectedProxy);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// Deliberately after the block above
|
||
|
describe('with proxy disabled', function() {
|
||
|
it('claims to be disabled', function() {
|
||
|
assert.isFalse(globalTunnel.isProxying);
|
||
|
});
|
||
|
|
||
|
it('will NOT proxy http requests', function(done) {
|
||
|
var dummyCb = sinon.stub();
|
||
|
request.get('http://example.dev/', dummyCb);
|
||
|
setImmediate(function() {
|
||
|
sinon.assert.calledOnce(globalHttpAgent.addRequest);
|
||
|
sinon.assert.notCalled(globalHttpsAgent.addRequest);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('will NOT proxy https requests', function(done) {
|
||
|
var dummyCb = sinon.stub();
|
||
|
request.get('https://example.dev/', dummyCb);
|
||
|
setImmediate(function() {
|
||
|
sinon.assert.notCalled(globalHttpAgent.addRequest);
|
||
|
sinon.assert.calledOnce(globalHttpsAgent.addRequest);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|