diff --git a/index.js b/index.js index ca235c0..0ea0d40 100644 --- a/index.js +++ b/index.js @@ -11,6 +11,7 @@ const prototypal = require('es-class'); const auto = require('autocreate'); const Client = require('./lib/Client'); +const proxy = require('./lib/proxy'); /* eslint-disable global-require */ const resources = { @@ -23,6 +24,31 @@ const resources = { }; /* eslint-enable global-require */ +/** + * withEnvProxy configures an HTTPS proxy if required to reach the Cloudflare API. + * + * @private + * @param {Object} opts - The current Cloudflare options + */ +const withEnvProxy = function withEnvProxy(opts) { + /* eslint-disable no-process-env */ + const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy; + const noProxy = process.env.NO_PROXY || process.env.no_proxy; + /* eslint-enable no-process-env */ + + if (httpsProxy) { + const agent = proxy.proxyAgent( + httpsProxy, + noProxy, + 'https://api.cloudflare.com' + ); + + if (agent) { + opts.agent = agent; + } + } +}; + /** * Constructs and returns a new Cloudflare API client with the specified authentication. * @@ -41,10 +67,14 @@ const resources = { const Cloudflare = auto( prototypal({ constructor: function constructor(auth) { - const client = new Client({ + const opts = { email: auth && auth.email, key: auth && auth.key, - }); + }; + + withEnvProxy(opts); + + const client = new Client(opts); Object.defineProperty(this, '_client', { value: client, diff --git a/lib/Getter.js b/lib/Getter.js index c3c140b..6ac78a1 100644 --- a/lib/Getter.js +++ b/lib/Getter.js @@ -12,8 +12,12 @@ const assign = require('object-assign'); const got = require('got'); module.exports = prototypal({ + constructor: function constructor(options) { + this._agent = options.agent; + }, got(uri, options) { options = assign({}, options); + options.agent = this._agent; return got(uri, options); }, diff --git a/lib/proxy.js b/lib/proxy.js new file mode 100644 index 0000000..cb2c2c0 --- /dev/null +++ b/lib/proxy.js @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2014-present Cloudflare, Inc. + + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +'use strict'; + +const shouldProxy = require('should-proxy'); +const HttpsProxyAgent = require('https-proxy-agent'); + +/** + * proxyAgent returns an HTTPS agent to use to access the base URL. + * + * @private + * @param {string} httpsProxy - HTTPS Proxy URL + * @param {string} noProxy - URLs that should be excluded from proxying + * @param {string} base - The client base URL + * @returns {https.Agent|null} - The HTTPS agent, if required to access the base URL. + */ +const proxyAgent = function proxyAgent(httpsProxy, noProxy, base) { + if (!httpsProxy) { + return null; + } + noProxy = noProxy || ''; + + const ok = shouldProxy(base, { + no_proxy: noProxy, // eslint-disable-line camelcase + }); + + if (!ok) { + return null; + } + + return new HttpsProxyAgent(httpsProxy); +}; + +module.exports.proxyAgent = proxyAgent; diff --git a/package-lock.json b/package-lock.json index 66578bd..8482d09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,14 @@ } } }, + "agent-base": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.1.2.tgz", + "integrity": "sha512-VE6QoEdaugY86BohRtfGmTDabxdU5sCKOkbcPA6PXKJsRzEi/7A3RCTxJal1ft/4qSfPht5/iQLhMh/wzSkkNw==", + "requires": { + "es6-promisify": "5.0.0" + } + }, "ajv": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", @@ -443,7 +451,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, "requires": { "ms": "2.0.0" } @@ -626,6 +633,19 @@ "event-emitter": "0.3.5" } }, + "es6-promise": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.2.tgz", + "integrity": "sha512-LSas5vsuA6Q4nEdf9wokY5/AJYXry98i0IzXsv49rYsgDGDNDPbqAYR1Pe23iFxygfbGZNR/5VrHXBCh2BhvUQ==" + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "requires": { + "es6-promise": "4.2.2" + } + }, "es6-set": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", @@ -1394,6 +1414,15 @@ "sshpk": "1.13.1" } }, + "https-proxy-agent": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.1.1.tgz", + "integrity": "sha512-LK6tQUR/VOkTI6ygAfWUKKP95I+e6M1h7N3PncGu1CATHCnex+CAv9ttR0lbHu1Uk2PXm/WoAHFo6JCGwMjVMw==", + "requires": { + "agent-base": "4.1.2", + "debug": "3.1.0" + } + }, "iconv-lite": { "version": "0.4.19", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", @@ -1973,8 +2002,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "multi-stage-sourcemap": { "version": "0.2.1", @@ -3848,6 +3876,11 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, + "should-proxy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/should-proxy/-/should-proxy-1.0.4.tgz", + "integrity": "sha1-yAWlAav2lTlgBjSAnmL78ji6NeQ=" + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", diff --git a/package.json b/package.json index 00ed94a..b45a395 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "autocreate": "^1.1.0", "es-class": "^2.1.1", "got": "^6.3.0", + "https-proxy-agent": "^2.1.1", "object-assign": "^4.1.0", + "should-proxy": "^1.0.4", "url-pattern": "^1.0.3" }, "devDependencies": { diff --git a/test/proxy.js b/test/proxy.js new file mode 100644 index 0000000..c9d6f54 --- /dev/null +++ b/test/proxy.js @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2014-present Cloudflare, Inc. + + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +'use strict'; + +const assert = require('power-assert'); +const mocha = require('mocha'); + +const proxy = require('../lib/proxy'); + +const describe = mocha.describe; +const it = mocha.it; + +describe('proxy agents', () => { + it('should not return an agent when parameters are not set', done => { + const tests = [ + ['', '', 'example.com'], + [null, null, 'http://example.com/'], + ]; + + tests.forEach(test => { + const agent = proxy.proxyAgent(test[0], test[1], test[2]); + + assert.ok(!agent, 'agent was unexpected'); + }); + + done(); + }); + + it('should not return an agent when noProxy matches base', done => { + const tests = [ + ['http://10.0.0.1:1234', 'example.com', 'http://example.com'], + ['http://10.0.0.1:1234', '.example.com', 'http://example.com'], + ['http://10.0.0.1:1234', 'foobar.com,.example.com', 'http://example.com'], + ]; + + tests.forEach(test => { + const agent = proxy.proxyAgent(test[0], test[1], test[2]); + + assert.ok(!agent, 'agent was unexpected'); + }); + + done(); + }); + + it('should return an agent when noProxy is not set', done => { + const tests = [ + ['http://10.0.0.1:1234', null, 'http://example.com'], + ['http://10.0.0.1:1234', '', 'http://example.com'], + ]; + + tests.forEach(test => { + const agent = proxy.proxyAgent(test[0], test[1], test[2]); + + assert.ok(agent, 'expected an agent'); + }); + + done(); + }); + + it("should return an agent when noProxy doesn't match", done => { + const agent = proxy.proxyAgent( + 'http://10.0.0.1:1234', + '.example.com', + 'https://example.org' + ); + + assert.ok(agent, 'expected an agent'); + + done(); + }); +});