diff --git a/docs/plugins.asciidoc b/docs/plugins.asciidoc index 0c2d8355dc4e2..b9365dd54b075 100644 --- a/docs/plugins.asciidoc +++ b/docs/plugins.asciidoc @@ -66,6 +66,20 @@ If plugins were installed as a different user and the server is not starting, th [source,shell] $ chown -R kibana:kibana /path/to/kibana/optimize +[float] +=== Proxy support for plugin installation + +Kibana supports plugin installation via a proxy. It uses the `http_proxy` and `https_proxy` +environment variables to detect a proxy for HTTP and HTTPS URLs. + +It also respects the `no_proxy` environment variable to exclude specific URLs from proxying. + +You can specify the environment variable directly when installing plugins: + +[source,shell] +$ http_proxy="http://proxy.local:4242" bin/kibana-plugin install + + == Updating & Removing Plugins To update a plugin, remove the current version and reinstall the plugin. diff --git a/package.json b/package.json index 0df6485f28c4f..a94a4ab1d4b96 100644 --- a/package.json +++ b/package.json @@ -133,6 +133,7 @@ "h2o2": "5.1.1", "handlebars": "4.0.5", "hapi": "14.2.0", + "http-proxy-agent": "1.0.0", "imports-loader": "0.6.4", "inert": "4.0.2", "jade": "1.11.0", @@ -157,6 +158,7 @@ "node-fetch": "1.3.2", "pegjs": "0.9.0", "postcss-loader": "1.2.1", + "proxy-from-env": "1.0.0", "prop-types": "15.5.8", "pui-react-overlay-trigger": "7.5.4", "pui-react-tooltip": "7.5.4", diff --git a/src/cli_plugin/install/__tests__/download.js b/src/cli_plugin/install/__tests__/download.js index 63cddbfc3fdd8..4693ed10e853b 100644 --- a/src/cli_plugin/install/__tests__/download.js +++ b/src/cli_plugin/install/__tests__/download.js @@ -8,6 +8,7 @@ import Logger from '../../lib/logger'; import { UnsupportedProtocolError } from '../../lib/errors'; import { download, _downloadSingle, _getFilePath, _checkFilePathDeprecation } from '../download'; import { join } from 'path'; +import http from 'http'; describe('kibana cli', function () { @@ -251,6 +252,150 @@ describe('kibana cli', function () { }); }); + after(function () { + nock.cleanAll(); + }); + + }); + + describe('proxy support', function () { + + const proxyPort = 2626; + const proxyUrl = `http://localhost:${proxyPort}`; + + let proxyHit = false; + + const proxy = http.createServer(function (req, res) { + proxyHit = true; + // Our test proxy simply returns an empty 200 response, since we only + // care about the download promise being resolved. + res.writeHead(200); + res.end(); + }); + + function expectProxyHit() { + expect(proxyHit).to.be(true); + } + + function expectNoProxyHit() { + expect(proxyHit).to.be(false); + } + + function nockPluginForUrl(url) { + nock(url) + .get('/plugin.zip') + .replyWithFile(200, join(__dirname, 'replies/test_plugin.zip')); + } + + before(function (done) { + proxy.listen(proxyPort, done); + }); + + beforeEach(function () { + proxyHit = false; + }); + + afterEach(function () { + delete process.env.http_proxy; + delete process.env.https_proxy; + delete process.env.no_proxy; + }); + + it('should use http_proxy env variable', function () { + process.env.http_proxy = proxyUrl; + settings.urls = ['http://example.com/plugin.zip']; + + return download(settings, logger) + .then(expectProxyHit); + }); + + it('should use https_proxy for secure URLs', function () { + process.env.https_proxy = proxyUrl; + settings.urls = ['https://example.com/plugin.zip']; + + return download(settings, logger) + .then(expectProxyHit); + }); + + it('should not use http_proxy for HTTPS urls', function () { + process.env.http_proxy = proxyUrl; + settings.urls = ['https://example.com/plugin.zip']; + + nockPluginForUrl('https://example.com'); + + return download(settings, logger) + .then(expectNoProxyHit); + }); + + it('should not use https_proxy for HTTP urls', function () { + process.env.https_proxy = proxyUrl; + settings.urls = ['http://example.com/plugin.zip']; + + nockPluginForUrl('http://example.com'); + + return download(settings, logger) + .then(expectNoProxyHit); + }); + + it('should support domains in no_proxy', function () { + process.env.http_proxy = proxyUrl; + process.env.no_proxy = 'foo.bar, example.com'; + settings.urls = ['http://example.com/plugin.zip']; + + nockPluginForUrl('http://example.com'); + + return download(settings, logger) + .then(expectNoProxyHit); + }); + + it('should support subdomains in no_proxy', function () { + process.env.http_proxy = proxyUrl; + process.env.no_proxy = 'foo.bar,plugins.example.com'; + settings.urls = ['http://plugins.example.com/plugin.zip']; + + nockPluginForUrl('http://plugins.example.com'); + + return download(settings, logger) + .then(expectNoProxyHit); + }); + + it('should accept wildcard subdomains in no_proxy', function () { + process.env.http_proxy = proxyUrl; + process.env.no_proxy = 'foo.bar, .example.com'; + settings.urls = ['http://plugins.example.com/plugin.zip']; + + nockPluginForUrl('http://plugins.example.com'); + + return download(settings, logger) + .then(expectNoProxyHit); + }); + + it('should support asterisk wildcard no_proxy syntax', function () { + process.env.http_proxy = proxyUrl; + process.env.no_proxy = '*.example.com'; + settings.urls = ['http://plugins.example.com/plugin.zip']; + + nockPluginForUrl('http://plugins.example.com'); + + return download(settings, logger) + .then(expectNoProxyHit); + }); + + it('should support implicit ports in no_proxy', function () { + process.env.https_proxy = proxyUrl; + process.env.no_proxy = 'example.com:443'; + settings.urls = ['https://example.com/plugin.zip']; + + nockPluginForUrl('https://example.com'); + + return download(settings, logger) + .then(expectNoProxyHit); + }); + + after(function (done) { + proxy.close(done); + }); + }); }); diff --git a/src/cli_plugin/install/downloaders/http.js b/src/cli_plugin/install/downloaders/http.js index f8c28d65b7733..1c261be5643e5 100644 --- a/src/cli_plugin/install/downloaders/http.js +++ b/src/cli_plugin/install/downloaders/http.js @@ -2,11 +2,31 @@ import Wreck from 'wreck'; import Progress from '../progress'; import { fromNode as fn } from 'bluebird'; import { createWriteStream } from 'fs'; +import HttpProxyAgent from 'http-proxy-agent'; +import { getProxyForUrl } from 'proxy-from-env'; -function sendRequest({ sourceUrl, timeout }) { +function getProxyAgent(sourceUrl, logger) { + const proxy = getProxyForUrl(sourceUrl); + + if (!proxy) { + return null; + } + + logger.log(`Picked up proxy ${proxy} from environment variable.`); + return new HttpProxyAgent(proxy); +} + +function sendRequest({ sourceUrl, timeout }, logger) { const maxRedirects = 11; //Because this one goes to 11. return fn(cb => { - const req = Wreck.request('GET', sourceUrl, { timeout, redirects: maxRedirects }, (err, resp) => { + const reqOptions = { timeout, redirects: maxRedirects }; + const proxyAgent = getProxyAgent(sourceUrl, logger); + + if (proxyAgent) { + reqOptions.agent = proxyAgent; + } + + const req = Wreck.request('GET', sourceUrl, reqOptions, (err, resp) => { if (err) { if (err.code === 'ECONNREFUSED') { err = new Error('ENOTFOUND'); @@ -50,7 +70,7 @@ Responsible for managing http transfers */ export default async function downloadUrl(logger, sourceUrl, targetPath, timeout) { try { - const { req, resp } = await sendRequest({ sourceUrl, timeout }); + const { req, resp } = await sendRequest({ sourceUrl, timeout }, logger); try { const totalSize = parseFloat(resp.headers['content-length']) || 0;