From c20f3ba99688d2267eedbfd7712577b7f9e78691 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Wed, 31 Jan 2018 14:12:25 +0100 Subject: [PATCH] Watcher: Add support for scheme in proxy configuration (elastic/x-pack-elasticsearch#3614) This adds support to allow different schemes in a proxy being used compared to what the actual request requires. So if your proxy runs via HTTP, but the endpoint you want to connect to uses HTTPS, this is now possible to configure the proxy explicitely. Also a small unit test for parsing this has been added. relates elastic/x-pack-elasticsearch#3596 Original commit: elastic/x-pack-elasticsearch@176f7cdf0e0f9a4fdb235aa691ced7333170e3cc --- .../xpack/watcher/common/http/HttpClient.java | 10 +- .../xpack/watcher/common/http/HttpProxy.java | 55 +++++---- .../watcher/common/http/HttpSettings.java | 20 ++-- .../watcher/common/http/HttpClientTests.java | 47 +++++++- .../watcher/common/http/HttpProxyTests.java | 109 ++++++++++++++++++ 5 files changed, 201 insertions(+), 40 deletions(-) create mode 100644 plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/HttpProxyTests.java diff --git a/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/common/http/HttpClient.java b/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/common/http/HttpClient.java index ecd6933fc46..b616933261d 100644 --- a/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/common/http/HttpClient.java +++ b/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/common/http/HttpClient.java @@ -65,6 +65,7 @@ public class HttpClient extends AbstractComponent { private final CloseableHttpClient client; private final Integer proxyPort; private final String proxyHost; + private final String proxyScheme; private final TimeValue defaultConnectionTimeout; private final TimeValue defaultReadTimeout; private final ByteSizeValue maxResponseSize; @@ -78,6 +79,7 @@ public class HttpClient extends AbstractComponent { // proxy setup this.proxyHost = HttpSettings.PROXY_HOST.get(settings); + this.proxyScheme = HttpSettings.PROXY_SCHEME.exists(settings) ? HttpSettings.PROXY_SCHEME.get(settings) : null; this.proxyPort = HttpSettings.PROXY_PORT.get(settings); if (proxyPort != 0 && Strings.hasText(proxyHost)) { logger.info("Using default proxy for http input and slack/hipchat/pagerduty/webhook actions [{}:{}]", proxyHost, proxyPort); @@ -139,10 +141,14 @@ public class HttpClient extends AbstractComponent { // proxy if (request.proxy != null && request.proxy.equals(HttpProxy.NO_PROXY) == false) { - HttpHost proxy = new HttpHost(request.proxy.getHost(), request.proxy.getPort(), request.scheme.scheme()); + // if a proxy scheme is configured use this, but fall back to the same than the request in case there was no special + // configuration given + String scheme = request.proxy.getScheme() != null ? request.proxy.getScheme().scheme() : request.scheme.scheme(); + HttpHost proxy = new HttpHost(request.proxy.getHost(), request.proxy.getPort(), scheme); config.setProxy(proxy); } else if (proxyPort != null && Strings.hasText(proxyHost)) { - HttpHost proxy = new HttpHost(proxyHost, proxyPort, request.scheme.scheme()); + String scheme = proxyScheme != null ? proxyScheme : request.scheme.scheme(); + HttpHost proxy = new HttpHost(proxyHost, proxyPort, scheme); config.setProxy(proxy); } diff --git a/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/common/http/HttpProxy.java b/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/common/http/HttpProxy.java index c94b219a9b0..4b439b7617b 100644 --- a/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/common/http/HttpProxy.java +++ b/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/common/http/HttpProxy.java @@ -22,34 +22,37 @@ import java.net.Proxy; import java.net.UnknownHostException; import java.util.Objects; -public class HttpProxy implements ToXContentFragment, Streamable { +public class HttpProxy implements ToXContentFragment { - public static final HttpProxy NO_PROXY = new HttpProxy(null, null); + public static final HttpProxy NO_PROXY = new HttpProxy(null, null, null); + + private static final ParseField HOST = new ParseField("host"); + private static final ParseField PORT = new ParseField("port"); + private static final ParseField SCHEME = new ParseField("scheme"); private String host; private Integer port; + private Scheme scheme; public HttpProxy(String host, Integer port) { this.host = host; this.port = port; } - @Override - public void readFrom(StreamInput in) throws IOException { - host = in.readOptionalString(); - port = in.readOptionalVInt(); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeOptionalString(host); - out.writeOptionalVInt(port); + public HttpProxy(String host, Integer port, Scheme scheme) { + this.host = host; + this.port = port; + this.scheme = scheme; } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { if (Strings.hasText(host) && port != null) { - builder.startObject("proxy").field("host", host).field("port", port).endObject(); + builder.startObject("proxy").field("host", host).field("port", port); + if (scheme != null) { + builder.field("scheme", scheme.scheme()); + } + builder.endObject(); } return builder; } @@ -62,12 +65,8 @@ public class HttpProxy implements ToXContentFragment, Streamable { return port; } - public Proxy proxy() throws UnknownHostException { - if (Strings.hasText(host) && port != null) { - return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(InetAddress.getByName(host), port)); - } - - return Proxy.NO_PROXY; + public Scheme getScheme() { + return scheme; } @Override @@ -77,12 +76,12 @@ public class HttpProxy implements ToXContentFragment, Streamable { HttpProxy that = (HttpProxy) o; - return Objects.equals(port, that.port) && Objects.equals(host, that.host); + return Objects.equals(port, that.port) && Objects.equals(host, that.host) && Objects.equals(scheme, that.scheme); } @Override public int hashCode() { - return Objects.hash(host, port); + return Objects.hash(host, port, scheme); } @@ -91,13 +90,16 @@ public class HttpProxy implements ToXContentFragment, Streamable { String currentFieldName = null; String host = null; Integer port = null; + Scheme scheme = null; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { currentFieldName = parser.currentName(); - } else if (Field.HOST.match(currentFieldName)) { + } else if (HOST.match(currentFieldName)) { host = parser.text(); - } else if (Field.PORT.match(currentFieldName)) { + } else if (SCHEME.match(currentFieldName)) { + scheme = Scheme.parse(parser.text()); + } else if (PORT.match(currentFieldName)) { port = parser.intValue(); if (port <= 0 || port >= 65535) { throw new ElasticsearchParseException("Proxy port must be between 1 and 65534, but was " + port); @@ -109,11 +111,6 @@ public class HttpProxy implements ToXContentFragment, Streamable { throw new ElasticsearchParseException("Proxy must contain 'port' and 'host' field"); } - return new HttpProxy(host, port); - } - - public interface Field { - ParseField HOST = new ParseField("host"); - ParseField PORT = new ParseField("port"); + return new HttpProxy(host, port, scheme); } } diff --git a/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/common/http/HttpSettings.java b/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/common/http/HttpSettings.java index 7b8e0efdf7b..f4f97df1d4f 100644 --- a/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/common/http/HttpSettings.java +++ b/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/common/http/HttpSettings.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.watcher.common.http; import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; @@ -23,22 +24,24 @@ public class HttpSettings { private static final TimeValue DEFAULT_CONNECTION_TIMEOUT = DEFAULT_READ_TIMEOUT; static final Setting READ_TIMEOUT = Setting.timeSetting("xpack.http.default_read_timeout", - DEFAULT_READ_TIMEOUT, Setting.Property.NodeScope); + DEFAULT_READ_TIMEOUT, Property.NodeScope); static final Setting CONNECTION_TIMEOUT = Setting.timeSetting("xpack.http.default_connection_timeout", - DEFAULT_CONNECTION_TIMEOUT, Setting.Property.NodeScope); + DEFAULT_CONNECTION_TIMEOUT, Property.NodeScope); - static final String PROXY_HOST_KEY = "xpack.http.proxy.host"; - static final String PROXY_PORT_KEY = "xpack.http.proxy.port"; - static final String SSL_KEY_PREFIX = "xpack.http.ssl."; + private static final String PROXY_HOST_KEY = "xpack.http.proxy.host"; + private static final String PROXY_PORT_KEY = "xpack.http.proxy.port"; + private static final String PROXY_SCHEME_KEY = "xpack.http.proxy.scheme"; + private static final String SSL_KEY_PREFIX = "xpack.http.ssl."; - static final Setting PROXY_HOST = Setting.simpleString(PROXY_HOST_KEY, Setting.Property.NodeScope); - static final Setting PROXY_PORT = Setting.intSetting(PROXY_PORT_KEY, 0, 0, 0xFFFF, Setting.Property.NodeScope); + static final Setting PROXY_HOST = Setting.simpleString(PROXY_HOST_KEY, Property.NodeScope); + static final Setting PROXY_SCHEME = Setting.simpleString(PROXY_SCHEME_KEY, (v, s) -> Scheme.parse(v), Property.NodeScope); + static final Setting PROXY_PORT = Setting.intSetting(PROXY_PORT_KEY, 0, 0, 0xFFFF, Property.NodeScope); static final Setting MAX_HTTP_RESPONSE_SIZE = Setting.byteSizeSetting("xpack.http.max_response_size", new ByteSizeValue(10, ByteSizeUnit.MB), // default new ByteSizeValue(1, ByteSizeUnit.BYTES), // min new ByteSizeValue(50, ByteSizeUnit.MB), // max - Setting.Property.NodeScope); + Property.NodeScope); private static final SSLConfigurationSettings SSL = SSLConfigurationSettings.withPrefix(SSL_KEY_PREFIX); @@ -49,6 +52,7 @@ public class HttpSettings { settings.add(CONNECTION_TIMEOUT); settings.add(PROXY_HOST); settings.add(PROXY_PORT); + settings.add(PROXY_SCHEME); settings.add(MAX_HTTP_RESPONSE_SIZE); return settings; } diff --git a/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/HttpClientTests.java b/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/HttpClientTests.java index 61422edb845..0fa0e3fd526 100644 --- a/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/HttpClientTests.java +++ b/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/HttpClientTests.java @@ -323,6 +323,50 @@ public class HttpClientTests extends ESTestCase { } } + public void testProxyCanHaveDifferentSchemeThanRequest() throws Exception { + // this test fakes a proxy server that sends a response instead of forwarding it to the mock web server + // on top of that the proxy request is HTTPS but the real request is HTTP only + MockSecureSettings serverSecureSettings = new MockSecureSettings(); + // We can't use the client created above for the server since it is only a truststore + serverSecureSettings.setString("xpack.ssl.keystore.secure_password", "testnode"); + Settings serverSettings = Settings.builder() + .put("xpack.ssl.keystore.path", getDataPath("/org/elasticsearch/xpack/security/keystore/testnode.jks")) + .setSecureSettings(serverSecureSettings) + .build(); + TestsSSLService sslService = new TestsSSLService(serverSettings, environment); + + try (MockWebServer proxyServer = new MockWebServer(sslService.sslContext(), false)) { + proxyServer.enqueue(new MockResponse().setResponseCode(200).setBody("fullProxiedContent")); + proxyServer.start(); + + Path resource = getDataPath("/org/elasticsearch/xpack/security/keystore/truststore-testnode-only.jks"); + MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("xpack.http.ssl.truststore.secure_password", "truststore-testnode-only"); + Settings settings = Settings.builder() + .put(HttpSettings.PROXY_HOST.getKey(), "localhost") + .put(HttpSettings.PROXY_PORT.getKey(), proxyServer.getPort()) + .put(HttpSettings.PROXY_SCHEME.getKey(), "https") + .put("xpack.http.ssl.truststore.path", resource.toString()) + .setSecureSettings(secureSettings) + .build(); + + HttpClient httpClient = new HttpClient(settings, authRegistry, new SSLService(settings, environment)); + + HttpRequest.Builder requestBuilder = HttpRequest.builder("localhost", webServer.getPort()) + .method(HttpMethod.GET) + .scheme(Scheme.HTTP) + .path("/"); + + HttpResponse response = httpClient.execute(requestBuilder.build()); + assertThat(response.status(), equalTo(200)); + assertThat(response.body().utf8ToString(), equalTo("fullProxiedContent")); + + // ensure we hit the proxyServer and not the webserver + assertThat(webServer.requests(), hasSize(0)); + assertThat(proxyServer.requests(), hasSize(1)); + } + } + public void testThatProxyCanBeOverriddenByRequest() throws Exception { // this test fakes a proxy server that sends a response instead of forwarding it to the mock web server try (MockWebServer proxyServer = new MockWebServer()) { @@ -331,12 +375,13 @@ public class HttpClientTests extends ESTestCase { Settings settings = Settings.builder() .put(HttpSettings.PROXY_HOST.getKey(), "localhost") .put(HttpSettings.PROXY_PORT.getKey(), proxyServer.getPort() + 1) + .put(HttpSettings.PROXY_HOST.getKey(), "https") .build(); HttpClient httpClient = new HttpClient(settings, authRegistry, new SSLService(settings, environment)); HttpRequest.Builder requestBuilder = HttpRequest.builder("localhost", webServer.getPort()) .method(HttpMethod.GET) - .proxy(new HttpProxy("localhost", proxyServer.getPort())) + .proxy(new HttpProxy("localhost", proxyServer.getPort(), Scheme.HTTP)) .path("/"); HttpResponse response = httpClient.execute(requestBuilder.build()); diff --git a/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/HttpProxyTests.java b/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/HttpProxyTests.java new file mode 100644 index 00000000000..ce54e464095 --- /dev/null +++ b/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/HttpProxyTests.java @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.watcher.common.http; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.ESTestCase; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +public class HttpProxyTests extends ESTestCase { + + public void testParser() throws Exception { + int port = randomIntBetween(1, 65000); + String host = randomAlphaOfLength(10); + XContentBuilder builder = jsonBuilder().startObject().field("host", host).field("port", port); + boolean isSchemeConfigured = randomBoolean(); + String scheme = null; + if (isSchemeConfigured) { + scheme = randomFrom(Scheme.values()).scheme(); + builder.field("scheme", scheme); + } + builder.endObject(); + try (XContentParser parser = XContentFactory.xContent(XContentType.JSON) + .createParser(NamedXContentRegistry.EMPTY, builder.bytes())) { + parser.nextToken(); + HttpProxy proxy = HttpProxy.parse(parser); + assertThat(proxy.getHost(), is(host)); + assertThat(proxy.getPort(), is(port)); + if (isSchemeConfigured) { + assertThat(proxy.getScheme().scheme(), is(scheme)); + } else { + assertThat(proxy.getScheme(), is(nullValue())); + } + } + } + + public void testParserValidScheme() throws Exception { + XContentBuilder builder = jsonBuilder().startObject() + .field("host", "localhost").field("port", 12345).field("scheme", "invalid") + .endObject(); + try (XContentParser parser = XContentFactory.xContent(XContentType.JSON) + .createParser(NamedXContentRegistry.EMPTY, builder.bytes())) { + parser.nextToken(); + expectThrows(IllegalArgumentException.class, () -> HttpProxy.parse(parser)); + } + } + + public void testParserValidPortRange() throws Exception { + XContentBuilder builder = jsonBuilder().startObject() + .field("host", "localhost").field("port", -1) + .endObject(); + try (XContentParser parser = XContentFactory.xContent(XContentType.JSON) + .createParser(NamedXContentRegistry.EMPTY, builder.bytes())) { + parser.nextToken(); + expectThrows(ElasticsearchParseException.class, () -> HttpProxy.parse(parser)); + } + } + + public void testParserNoHost() throws Exception { + XContentBuilder builder = jsonBuilder().startObject() + .field("port", -1) + .endObject(); + try (XContentParser parser = XContentFactory.xContent(XContentType.JSON) + .createParser(NamedXContentRegistry.EMPTY, builder.bytes())) { + parser.nextToken(); + expectThrows(ElasticsearchParseException.class, () -> HttpProxy.parse(parser)); + } + } + + public void testParserNoPort() throws Exception { + XContentBuilder builder = jsonBuilder().startObject() + .field("host", "localhost") + .endObject(); + try (XContentParser parser = XContentFactory.xContent(XContentType.JSON) + .createParser(NamedXContentRegistry.EMPTY, builder.bytes())) { + parser.nextToken(); + expectThrows(ElasticsearchParseException.class, () -> HttpProxy.parse(parser)); + } + } + + public void testToXContent() throws Exception { + try (XContentBuilder builder = jsonBuilder()) { + builder.startObject(); + HttpProxy proxy = new HttpProxy("localhost", 3128); + proxy.toXContent(builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + assertThat(builder.string(), is("{\"proxy\":{\"host\":\"localhost\",\"port\":3128}}")); + } + + try (XContentBuilder builder = jsonBuilder()) { + builder.startObject(); + HttpProxy httpsProxy = new HttpProxy("localhost", 3128, Scheme.HTTPS); + httpsProxy.toXContent(builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + assertThat(builder.string(), is("{\"proxy\":{\"host\":\"localhost\",\"port\":3128,\"scheme\":\"https\"}}")); + } + } +} \ No newline at end of file