diff --git a/lib/client/socket.js b/lib/client/socket.js index 164ca62ca..3c76f9d75 100644 --- a/lib/client/socket.js +++ b/lib/client/socket.js @@ -1,3 +1,5 @@ +require('../patch-https-request-for-proxying'); + const Primus = require('primus'); const relay = require('../relay'); const httpErrors = require('../http-errors'); diff --git a/lib/patch-https-request-for-proxying.js b/lib/patch-https-request-for-proxying.js new file mode 100644 index 000000000..dae1ac2c3 --- /dev/null +++ b/lib/patch-https-request-for-proxying.js @@ -0,0 +1,102 @@ +/* + * Monkey-patch https.request to proxy request to the broker server + * according the the env variables `https_proxy` and `no_proxy`: + * get the https proxy from the env. proxy requests unless the + * broker server is in the no_proxy list. + * Start from the "Entry point" below. + */ +const url = require('url'); +const tunnel = require('tunnel'); +const {brokerServerUrl, httpsProxy, noProxy} = require('./config'); +const brokerServer = url.parse(brokerServerUrl || ''); +brokerServer.port = brokerServer.port || + brokerServer.protocol === 'https:' ? '443' : '80'; + +// adapted from https://github.com/request/request/master/lib/getProxyFromURI.js + +function formatHostname(hostname) { + // canonicalize the hostname, so that 'oogle.com' won't match 'google.com' + return hostname.replace(/^\.*/, '.').toLowerCase(); +} + +function parseNoProxyZone(zone) { + zone = zone.trim().toLowerCase(); + + const zoneParts = zone.split(':', 2); + const zoneHost = formatHostname(zoneParts[0]); + const zonePort = zoneParts[1]; + const hasPort = zone.indexOf(':') > -1; + + return {hostname: zoneHost, port: zonePort, hasPort: hasPort}; +} + +function uriInNoProxy(uri, noProxy) { + const port = uri.port || (uri.protocol === 'https:' ? '443' : '80'); + const hostname = formatHostname(uri.hostname); + const noProxyList = noProxy.split(','); + + // iterate through the noProxyList until it finds a match. + return noProxyList.map(parseNoProxyZone).some(function (noProxyZone) { + const isMatchedAt = hostname.indexOf(noProxyZone.hostname); + const hostnameMatched = ( + isMatchedAt > -1 && + (isMatchedAt === hostname.length - noProxyZone.hostname.length)); + + if (noProxyZone.hasPort) { + return (port === noProxyZone.port) && hostnameMatched; + } + + return hostnameMatched; + }); +} + +function shouldProxy(uri) { + // Decide the proper request proxy to use based on the request URI object and the + // environmental variables (NO_PROXY, HTTP_PROXY, etc.) + // respect NO_PROXY environment variables (see: http://lynx.isc.org/current/breakout/lynx_help/keystrokes/environments.html) + + // if no https proxy is defined - don't proxy + + if (!httpsProxy) { + return false; + } + + // if the noProxy is a wildcard then return null + + if (noProxy === '*') { + return false; + } + + // if the noProxy is not empty and the uri is found return null + + if (noProxy && noProxy !== '' && uriInNoProxy(uri, noProxy)) { + return false; + } + + // we should proxy + return true; +} + +// Entry point: To patch or not to patch? +if (brokerServer.host && httpsProxy && shouldProxy(brokerServer)) { + const proxy = url.parse(httpsProxy); + const tunnelingAgent = tunnel.httpsOverHttp({ + proxy: { + host: proxy.hostname, + port: proxy.port + } + }); + + // actual monkey patching: BEWARE! + // we're only patching HTTPS requests to the broker server + const https = require('https'); + const oldhttpsreq = https.request; + https.request = function (options, callback) { + if (options.host === brokerServer.host) { + options.agent = tunnelingAgent; + } + return oldhttpsreq.call(null, options, callback); + }; +} + +module.exports = {shouldProxy}; // for testing diff --git a/package.json b/package.json index b92f63924..18979a1cf 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "babel-preset-stage-3": "^6.11.0", "eslint": "^3.3.1", "proxyquire": "^1.7.10", + "require-reload": "^0.2.2", "semantic-release": "^4.3.5", "sinon": "^1.17.5", "tap": "^6.3.0", @@ -61,6 +62,7 @@ "snyk": "^1.25.2", "snyk-config": "^1.0.1", "then-fs": "^2.0.0", + "tunnel": "0.0.4", "undefsafe": "^1.2.0" }, "repository": { diff --git a/test/unit/proxying-decision.js b/test/unit/proxying-decision.js new file mode 100644 index 000000000..70d34851c --- /dev/null +++ b/test/unit/proxying-decision.js @@ -0,0 +1,42 @@ +const test = require('tap-only'); +const u = require('url').parse; +const reload = require('require-reload')(require); + +test('shouldProxy: no https proxy', t => { + t.plan(1); + process.env.http_proxy = 'localhost:4444'; // http, not httpS + + // loaded now, for config to be reloaded after env vars + const config = reload('../../lib/config'); + const {shouldProxy} = reload('../../lib/patch-https-request-for-proxying'); + t.false(shouldProxy(u('https://broker.snyk.io')), + 'should not proxy when no https proxy is defined'); +}); + +test('shouldProxy: no_proxy', t => { + t.plan(2); + const oldestDomainOnINET = process.env.no_proxy = 'symbolics.com'; + process.env.https_proxy = 'https://localhost:8888'; + + // loaded now, for config to be reloaded after env vars + const config = reload('../../lib/config'); + const {shouldProxy} = reload('../../lib/patch-https-request-for-proxying'); + const url = u('http://' + oldestDomainOnINET); + t.false(shouldProxy(url), + 'should not proxy domains from no_proxy'); + t.true(shouldProxy(u('https://shambhala.org/')), 'not in no_proxy'); +}); + +test('shouldProxy: NO_PROXY', t => { + t.plan(2); + const dontProx = 'wiki.c2.com'; + process.env.NO_PROXY = 'symbolics.com,' + dontProx; + process.env.HTTPS_PROXY = 'https://localhost:8888'; + + // loaded now, for config to be reloaded after env vars + const config = reload('../../lib/config'); + const {shouldProxy} = reload('../../lib/patch-https-request-for-proxying'); + t.false(shouldProxy(u('https://' + dontProx + '/?XeroxParc')), + 'should not proxy domains from NO_PROXY'); + t.true(shouldProxy(u('http://silibank.net.kp')), 'not in NO_PROXY'); +});