Repository: calcite-avatica Updated Branches: refs/heads/master 4d0f397a7 -> c3f4611d4
[CALCITE-1904] Allow SSL hostname verification to be turned off Closes #12 Project: http://git-wip-us.apache.org/repos/asf/calcite-avatica/repo Commit: http://git-wip-us.apache.org/repos/asf/calcite-avatica/commit/c3f4611d Tree: http://git-wip-us.apache.org/repos/asf/calcite-avatica/tree/c3f4611d Diff: http://git-wip-us.apache.org/repos/asf/calcite-avatica/diff/c3f4611d Branch: refs/heads/master Commit: c3f4611d4e94f50ed2130801c7054e3d109bde4b Parents: 4d0f397 Author: Josh Elser <[email protected]> Authored: Thu Jul 27 12:19:05 2017 -0400 Committer: Josh Elser <[email protected]> Committed: Thu Jul 27 12:19:05 2017 -0400 ---------------------------------------------------------------------- .../avatica/BuiltInConnectionProperty.java | 6 ++- .../calcite/avatica/ConnectionConfig.java | 3 ++ .../calcite/avatica/ConnectionConfigImpl.java | 6 +++ .../remote/AvaticaCommonsHttpClientImpl.java | 39 +++++++++++++++- .../remote/AvaticaHttpClientFactoryImpl.java | 9 ++++ .../HostnameVerificationConfigurable.java | 47 ++++++++++++++++++++ .../AvaticaCommonsHttpClientImplTest.java | 28 ++++++++++++ 7 files changed, 135 insertions(+), 3 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/c3f4611d/core/src/main/java/org/apache/calcite/avatica/BuiltInConnectionProperty.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/avatica/BuiltInConnectionProperty.java b/core/src/main/java/org/apache/calcite/avatica/BuiltInConnectionProperty.java index d68bae8..1da7025 100644 --- a/core/src/main/java/org/apache/calcite/avatica/BuiltInConnectionProperty.java +++ b/core/src/main/java/org/apache/calcite/avatica/BuiltInConnectionProperty.java @@ -17,6 +17,7 @@ package org.apache.calcite.avatica; import org.apache.calcite.avatica.remote.AvaticaHttpClientFactoryImpl; +import org.apache.calcite.avatica.remote.HostnameVerificationConfigurable.HostnameVerification; import java.util.HashMap; import java.util.HashSet; @@ -73,7 +74,10 @@ public enum BuiltInConnectionProperty implements ConnectionProperty { TRUSTSTORE("truststore", Type.STRING, null, false), /** Password for the truststore */ - TRUSTSTORE_PASSWORD("truststore_password", Type.STRING, null, false); + TRUSTSTORE_PASSWORD("truststore_password", Type.STRING, null, false), + + HOSTNAME_VERIFICATION("hostname_verification", Type.ENUM, HostnameVerification.STRICT, + HostnameVerification.class, false); private final String camelName; private final Type type; http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/c3f4611d/core/src/main/java/org/apache/calcite/avatica/ConnectionConfig.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/avatica/ConnectionConfig.java b/core/src/main/java/org/apache/calcite/avatica/ConnectionConfig.java index e8ff0be..cd18ec5 100644 --- a/core/src/main/java/org/apache/calcite/avatica/ConnectionConfig.java +++ b/core/src/main/java/org/apache/calcite/avatica/ConnectionConfig.java @@ -17,6 +17,7 @@ package org.apache.calcite.avatica; import org.apache.calcite.avatica.remote.AvaticaHttpClientFactory; +import org.apache.calcite.avatica.remote.HostnameVerificationConfigurable.HostnameVerification; import org.apache.calcite.avatica.remote.Service; import java.io.File; @@ -53,6 +54,8 @@ public interface ConnectionConfig { File truststore(); /** @see BuiltInConnectionProperty#TRUSTSTORE_PASSWORD */ String truststorePassword(); + /** @see BuiltInConnectionProperty#HOSTNAME_VERIFICATION */ + HostnameVerification hostnameVerification(); } // End ConnectionConfig.java http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/c3f4611d/core/src/main/java/org/apache/calcite/avatica/ConnectionConfigImpl.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/avatica/ConnectionConfigImpl.java b/core/src/main/java/org/apache/calcite/avatica/ConnectionConfigImpl.java index a003b82..63fbd8d 100644 --- a/core/src/main/java/org/apache/calcite/avatica/ConnectionConfigImpl.java +++ b/core/src/main/java/org/apache/calcite/avatica/ConnectionConfigImpl.java @@ -17,6 +17,7 @@ package org.apache.calcite.avatica; import org.apache.calcite.avatica.remote.AvaticaHttpClientFactory; +import org.apache.calcite.avatica.remote.HostnameVerificationConfigurable.HostnameVerification; import org.apache.calcite.avatica.remote.Service; import java.io.File; @@ -105,6 +106,11 @@ public class ConnectionConfigImpl implements ConnectionConfig { return BuiltInConnectionProperty.TRUSTSTORE_PASSWORD.wrap(properties).getString(); } + public HostnameVerification hostnameVerification() { + return BuiltInConnectionProperty.HOSTNAME_VERIFICATION.wrap(properties) + .getEnum(HostnameVerification.class); + } + /** Converts a {@link Properties} object containing (name, value) * pairs into a map whose keys are * {@link org.apache.calcite.avatica.InternalProperty} objects. http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/c3f4611d/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaCommonsHttpClientImpl.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaCommonsHttpClientImpl.java b/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaCommonsHttpClientImpl.java index 33872d0..5576847 100644 --- a/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaCommonsHttpClientImpl.java +++ b/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaCommonsHttpClientImpl.java @@ -31,6 +31,7 @@ import org.apache.http.config.Lookup; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.ContentType; @@ -55,6 +56,7 @@ import java.net.URISyntaxException; import java.net.URL; import java.util.Objects; +import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; /** @@ -62,7 +64,7 @@ import javax.net.ssl.SSLContext; * sent and received across the wire. */ public class AvaticaCommonsHttpClientImpl implements AvaticaHttpClient, - UsernamePasswordAuthenticateable, TrustStoreConfigurable { + UsernamePasswordAuthenticateable, TrustStoreConfigurable, HostnameVerificationConfigurable { private static final Logger LOG = LoggerFactory.getLogger(AvaticaCommonsHttpClientImpl.class); // Some basic exposed configurations @@ -84,6 +86,7 @@ public class AvaticaCommonsHttpClientImpl implements AvaticaHttpClient, protected File truststore = null; protected String truststorePassword = null; + protected HostnameVerification hostnameVerification = null; public AvaticaCommonsHttpClientImpl(URL url) { this.host = new HttpHost(url.getHost(), url.getPort(), url.getProtocol()); @@ -97,7 +100,10 @@ public class AvaticaCommonsHttpClientImpl implements AvaticaHttpClient, try { SSLContext sslcontext = SSLContexts.custom().loadTrustMaterial( truststore, truststorePassword.toCharArray()).build(); - sslFactory = new SSLConnectionSocketFactory(sslcontext); + + final HostnameVerifier verifier = getHostnameVerifier(hostnameVerification); + + sslFactory = new SSLConnectionSocketFactory(sslcontext, verifier); } catch (Exception e) { throw new RuntimeException(e); } @@ -128,6 +134,30 @@ public class AvaticaCommonsHttpClientImpl implements AvaticaHttpClient, this.client = HttpClients.custom().setConnectionManager(pool).build(); } + /** + * Creates the {@code HostnameVerifier} given the provided {@code verification}. + * + * @param verification The intended hostname verification action. + * @return A verifier for the request verification. + * @throws IllegalArgumentException if the provided verification cannot be handled. + */ + HostnameVerifier getHostnameVerifier(HostnameVerification verification) { + // Normally, the configuration logic would give us a default of STRICT if it was not + // provided by the user. It's easy for us to do a double-check. + if (verification == null) { + verification = HostnameVerification.STRICT; + } + switch (verification) { + case STRICT: + return SSLConnectionSocketFactory.getDefaultHostnameVerifier(); + case NONE: + return NoopHostnameVerifier.INSTANCE; + default: + throw new IllegalArgumentException("Unhandled HostnameVerification: " + + hostnameVerification); + } + } + public byte[] send(byte[] request) { while (true) { HttpClientContext context = HttpClientContext.create(); @@ -216,6 +246,11 @@ public class AvaticaCommonsHttpClientImpl implements AvaticaHttpClient, this.truststorePassword = Objects.requireNonNull(password); initializeClient(); } + + @Override public void setHostnameVerification(HostnameVerification verification) { + this.hostnameVerification = Objects.requireNonNull(verification); + initializeClient(); + } } // End AvaticaCommonsHttpClientImpl.java http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/c3f4611d/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactoryImpl.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactoryImpl.java b/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactoryImpl.java index 8778c3d..f352ac1 100644 --- a/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactoryImpl.java +++ b/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactoryImpl.java @@ -80,6 +80,15 @@ public class AvaticaHttpClientFactoryImpl implements AvaticaHttpClientFactory { LOG.debug("{} is not capable of SSL/TLS communication", client.getClass().getName()); } + // Set the SSL hostname verification if the client supports it + if (client instanceof HostnameVerificationConfigurable) { + ((HostnameVerificationConfigurable) client) + .setHostnameVerification(config.hostnameVerification()); + } else { + LOG.debug("{} is not capable of configurable SSL/TLS hostname verification", + client.getClass().getName()); + } + if (client instanceof UsernamePasswordAuthenticateable) { // Shortcircuit quickly if authentication wasn't provided (implies NONE) final String authString = config.authentication(); http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/c3f4611d/core/src/main/java/org/apache/calcite/avatica/remote/HostnameVerificationConfigurable.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/avatica/remote/HostnameVerificationConfigurable.java b/core/src/main/java/org/apache/calcite/avatica/remote/HostnameVerificationConfigurable.java new file mode 100644 index 0000000..4f6d111 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/avatica/remote/HostnameVerificationConfigurable.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.calcite.avatica.remote; + +/** + * An interface to decorate an {@link AvaticaHttpClient} that can support configuration on + * SSL hostname verification. + */ +public interface HostnameVerificationConfigurable { + /** + * Describes the support hostname verification methods of {@link AvaticaHttpClient}. + */ + public enum HostnameVerification { + /** + * The common name (CN) on the certificate must match the server's hostname. + */ + STRICT, + /** + * No verification is performed. + */ + NONE, + } + + /** + * Instructs the {@link AvaticaHttpClient} how to perform hostname verification for SSL + * connections. + * + * @param verification The mode of hostname verification + */ + void setHostnameVerification(HostnameVerification verification); +} + +// End HostnameVerificationConfigurable.java http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/c3f4611d/core/src/test/java/org/apache/calcite/avatica/remote/AvaticaCommonsHttpClientImplTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/calcite/avatica/remote/AvaticaCommonsHttpClientImplTest.java b/core/src/test/java/org/apache/calcite/avatica/remote/AvaticaCommonsHttpClientImplTest.java index 990ecd2..9918143 100644 --- a/core/src/test/java/org/apache/calcite/avatica/remote/AvaticaCommonsHttpClientImplTest.java +++ b/core/src/test/java/org/apache/calcite/avatica/remote/AvaticaCommonsHttpClientImplTest.java @@ -16,11 +16,15 @@ */ package org.apache.calcite.avatica.remote; +import org.apache.calcite.avatica.remote.HostnameVerificationConfigurable.HostnameVerification; + import org.apache.http.NoHttpResponseException; import org.apache.http.StatusLine; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.conn.ssl.DefaultHostnameVerifier; +import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.entity.StringEntity; import org.junit.Test; @@ -28,10 +32,14 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.net.HttpURLConnection; +import javax.net.ssl.HostnameVerifier; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.nullable; import static org.mockito.Mockito.when; import static java.nio.charset.StandardCharsets.UTF_8; @@ -110,6 +118,26 @@ public class AvaticaCommonsHttpClientImplTest { byte[] responseBytes = client.send(requestBytes); assertEquals("success", new String(responseBytes, UTF_8)); } + + @Test public void testHostnameVerification() throws Exception { + AvaticaCommonsHttpClientImpl client = mock(AvaticaCommonsHttpClientImpl.class); + // Call the real method + when(client.getHostnameVerifier(nullable(HostnameVerification.class))) + .thenCallRealMethod(); + + // No verification should give the default (strict) verifier + HostnameVerifier actualVerifier = client.getHostnameVerifier(null); + assertNotNull(actualVerifier); + assertTrue(actualVerifier instanceof DefaultHostnameVerifier); + + actualVerifier = client.getHostnameVerifier(HostnameVerification.STRICT); + assertNotNull(actualVerifier); + assertTrue(actualVerifier instanceof DefaultHostnameVerifier); + + actualVerifier = client.getHostnameVerifier(HostnameVerification.NONE); + assertNotNull(actualVerifier); + assertTrue(actualVerifier instanceof NoopHostnameVerifier); + } } // End AvaticaCommonsHttpClientImplTest.java
