Skip to content

Commit

Permalink
Allow use of custom HostnameVerifier on clients.
Browse files Browse the repository at this point in the history
While the improvements to TLS configuration of HTTP clients in 1.0.0
(maybe prior) are awesome, as part of that process the ability to set a
custom HostnameVerifier easily on the HTTP client has been lost.

You used to be able to do e.g. as:

JerseyClientConfiguration myJerseyClientConfiguration = <some
configuration>;
HostnameVerifier verifier = new MyCustomHostnameVerifier();
JerseyClientBuilder clientBuilder = new JerseyClientBuilder(env);
clientBuilder.using(myJerseyClientConfiguration).using(verifier);
Client httpClient = clientBuilder.build();
Same is true for HttpClientBuilder too.

You can still do it by creating a custom Apache
Registry<ConnectionSocketFactory> but you need to set up socket
factories for every scheme.

This change restores the ability to set a custom HostnameVerifier
for clients.

[Fixes #1663]
(cherry picked from commit 160502f)
  • Loading branch information
tbartley authored and arteam committed Aug 4, 2016
1 parent 7bfdeab commit e34310b
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 36 deletions.
Expand Up @@ -19,9 +19,15 @@
public class DropwizardSSLConnectionSocketFactory {

private final TlsConfiguration configuration;
private final HostnameVerifier verifier;

public DropwizardSSLConnectionSocketFactory(TlsConfiguration configuration) {
this(configuration, null);
}

public DropwizardSSLConnectionSocketFactory(TlsConfiguration configuration, HostnameVerifier verifier) {
this.configuration = configuration;
this.verifier = verifier;
}

public SSLConnectionSocketFactory getSocketFactory() throws SSLInitializationException {
Expand All @@ -46,13 +52,11 @@ private String[] getSupportedProtocols() {
}

private HostnameVerifier chooseHostnameVerifier() {
HostnameVerifier hostnameVerifier;
if (configuration.isVerifyHostname()) {
hostnameVerifier = SSLConnectionSocketFactory.getDefaultHostnameVerifier();
return verifier != null ? verifier : SSLConnectionSocketFactory.getDefaultHostnameVerifier();
} else {
hostnameVerifier = new NoopHostnameVerifier();
return new NoopHostnameVerifier();
}
return hostnameVerifier;
}

private SSLContext buildSslContext() throws SSLInitializationException {
Expand Down
Expand Up @@ -9,9 +9,11 @@
import io.dropwizard.client.proxy.AuthConfiguration;
import io.dropwizard.client.proxy.NonProxyListProxyRoutePlanner;
import io.dropwizard.client.proxy.ProxyConfiguration;
import io.dropwizard.client.ssl.TlsConfiguration;
import io.dropwizard.lifecycle.Managed;
import io.dropwizard.setup.Environment;
import io.dropwizard.util.Duration;
import javax.net.ssl.HostnameVerifier;
import org.apache.http.ConnectionReuseStrategy;
import org.apache.http.Header;
import org.apache.http.HttpHost;
Expand Down Expand Up @@ -62,6 +64,7 @@ public class HttpClientBuilder {
private Environment environment;
private HttpClientConfiguration configuration = new HttpClientConfiguration();
private DnsResolver resolver = new SystemDefaultDnsResolver();
private HostnameVerifier verifier;
private HttpRequestRetryHandler httpRequestRetryHandler;
private Registry<ConnectionSocketFactory> registry;

Expand Down Expand Up @@ -115,6 +118,17 @@ public HttpClientBuilder using(DnsResolver resolver) {
return this;
}

/**
* Use the give (@link HostnameVerifier} instance.
*
* @param verifier a {@link HostnameVerifier} instance
* @return {@code this}
*/
public HttpClientBuilder using(HostnameVerifier verifier) {
this.verifier = verifier;
return this;
}

/**
* Uses the {@link HttpRequestRetryHandler} for handling request retries.
*
Expand Down Expand Up @@ -346,6 +360,10 @@ public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
builder.setDefaultHeaders(defaultHeaders);
}

if (verifier != null) {
builder.setSSLHostnameVerifier(verifier);
}

return new ConfiguredCloseableHttpClient(builder.build(), requestConfig);
}

Expand Down Expand Up @@ -385,17 +403,23 @@ protected InstrumentedHttpClientConnectionManager createConnectionManager(Regist
return configureConnectionManager(manager);
}

private Registry<ConnectionSocketFactory> createConfiguredRegistry() {
@VisibleForTesting
Registry<ConnectionSocketFactory> createConfiguredRegistry() {
if (registry != null) {
return registry;
}

TlsConfiguration tlsConfiguration = configuration.getTlsConfiguration();
if (tlsConfiguration == null && verifier != null) {
tlsConfiguration = new TlsConfiguration();
}

final SSLConnectionSocketFactory sslConnectionSocketFactory;
if (configuration.getTlsConfiguration() == null) {
if (tlsConfiguration == null) {
sslConnectionSocketFactory = SSLConnectionSocketFactory.getSocketFactory();
} else {
sslConnectionSocketFactory = new DropwizardSSLConnectionSocketFactory(configuration.getTlsConfiguration())
.getSocketFactory();
sslConnectionSocketFactory = new DropwizardSSLConnectionSocketFactory(tlsConfiguration,
verifier).getSocketFactory();
}

return RegistryBuilder.<ConnectionSocketFactory>create()
Expand Down
Expand Up @@ -11,6 +11,7 @@
import io.dropwizard.jersey.validation.Validators;
import io.dropwizard.lifecycle.Managed;
import io.dropwizard.setup.Environment;
import javax.net.ssl.HostnameVerifier;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.config.Registry;
Expand Down Expand Up @@ -224,6 +225,22 @@ public JerseyClientBuilder using(DnsResolver resolver) {
return this;
}

/**
* Use the given {@link HostnameVerifier} instance.
*
* Note that if {@link io.dropwizard.client.ssl.TlsConfiguration#isVerifyHostname()}
* returns false, all host name verification is bypassed, including
* host name verification performed by a verifier specified
* through this interface.
*
* @param verifier a {@link HostnameVerifier} instance
* @return {@code this}
*/
public JerseyClientBuilder using(HostnameVerifier verifier) {
apacheHttpClientBuilder.using(verifier);
return this;
}

/**
* Use the given {@link Registry} instance of connection socket factories.
*
Expand Down
@@ -1,16 +1,25 @@
package io.dropwizard.client;

import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.httpclient.HttpClientMetricNameStrategies;
import com.codahale.metrics.httpclient.InstrumentedHttpClientConnectionManager;
import com.codahale.metrics.httpclient.InstrumentedHttpRequestExecutor;
import com.google.common.collect.ImmutableList;
import io.dropwizard.client.proxy.AuthConfiguration;
import io.dropwizard.client.proxy.ProxyConfiguration;
import io.dropwizard.lifecycle.Managed;
import io.dropwizard.lifecycle.setup.LifecycleEnvironment;
import io.dropwizard.setup.Environment;
import io.dropwizard.util.Duration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.validateMockitoUsage;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;

import java.io.IOException;
import java.lang.reflect.Field;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.SocketAddress;
import java.net.URI;
import java.util.List;
import java.util.Optional;

import javax.net.ssl.HostnameVerifier;

import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.http.Header;
import org.apache.http.HeaderIterator;
Expand Down Expand Up @@ -51,26 +60,24 @@
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;

import java.io.IOException;
import java.lang.reflect.Field;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.SocketAddress;
import java.net.URI;
import java.util.List;
import java.util.Optional;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.httpclient.HttpClientMetricNameStrategies;
import com.codahale.metrics.httpclient.InstrumentedHttpClientConnectionManager;
import com.codahale.metrics.httpclient.InstrumentedHttpRequestExecutor;
import com.google.common.collect.ImmutableList;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import io.dropwizard.client.proxy.AuthConfiguration;
import io.dropwizard.client.proxy.ProxyConfiguration;
import io.dropwizard.client.ssl.TlsConfiguration;
import io.dropwizard.lifecycle.Managed;
import io.dropwizard.lifecycle.setup.LifecycleEnvironment;
import io.dropwizard.setup.Environment;
import io.dropwizard.util.Duration;

public class HttpClientBuilderTest {
class CustomBuilder extends HttpClientBuilder {
Expand Down Expand Up @@ -100,7 +107,7 @@ protected org.apache.http.impl.client.HttpClientBuilder customizeBuilder(
private HttpClientBuilder builder;
private InstrumentedHttpClientConnectionManager connectionManager;
private org.apache.http.impl.client.HttpClientBuilder apacheBuilder;

public HttpClientBuilderTest() throws ClassNotFoundException {
this.httpClientBuilderClass = Class.forName("org.apache.http.impl.client.HttpClientBuilder");
this.httpClientClass = Class.forName("org.apache.http.impl.client.InternalHttpClient");
Expand All @@ -113,10 +120,14 @@ public void setUp() {
builder = new HttpClientBuilder(metricRegistry);
connectionManager = spy(new InstrumentedHttpClientConnectionManager(metricRegistry, registry));
apacheBuilder = org.apache.http.impl.client.HttpClientBuilder.create();

initMocks(this);
}

@After
public void validate() {
validateMockitoUsage();
}

@Test
public void setsTheMaximumConnectionPoolSize() throws Exception {
configuration.setMaxConnections(412);
Expand Down Expand Up @@ -175,6 +186,88 @@ public void usesASystemDnsResolverByDefault() throws Exception {
assertThat(dnsResolverField.get(connectOperator)).isInstanceOf(SystemDefaultDnsResolver.class);
}

@Test
public void canUseACustomHostnameVerifierWhenTlsConfigurationNotSpecified() throws Exception {
final HostnameVerifier customVerifier = (s, sslSession) -> false;

final Registry<ConnectionSocketFactory> configuredRegistry;
configuredRegistry = builder.using(customVerifier).createConfiguredRegistry();
assertThat(configuredRegistry).isNotNull();

final SSLConnectionSocketFactory socketFactory =
(SSLConnectionSocketFactory) configuredRegistry.lookup("https");
assertThat(socketFactory).isNotNull();

final Field hostnameVerifierField =
FieldUtils.getField(SSLConnectionSocketFactory.class, "hostnameVerifier", true);
assertThat(hostnameVerifierField.get(socketFactory)).isSameAs(customVerifier);
}

@Test
public void canUseACustomHostnameVerifierWhenTlsConfigurationSpecified() throws Exception {
final TlsConfiguration tlsConfiguration = new TlsConfiguration();
tlsConfiguration.setVerifyHostname(true);
configuration.setTlsConfiguration(tlsConfiguration);

final HostnameVerifier customVerifier = (s, sslSession) -> false;

final Registry<ConnectionSocketFactory> configuredRegistry;
configuredRegistry = builder.using(configuration).using(customVerifier).createConfiguredRegistry();
assertThat(configuredRegistry).isNotNull();

final SSLConnectionSocketFactory socketFactory =
(SSLConnectionSocketFactory) configuredRegistry.lookup("https");
assertThat(socketFactory).isNotNull();

final Field hostnameVerifierField =
FieldUtils.getField(SSLConnectionSocketFactory.class, "hostnameVerifier", true);
assertThat(hostnameVerifierField.get(socketFactory)).isSameAs(customVerifier);
}

@Test
public void canUseASystemHostnameVerifierByDefaultWhenTlsConfigurationNotSpecified() throws Exception {
final Registry<ConnectionSocketFactory> configuredRegistry;
configuredRegistry = builder.createConfiguredRegistry();
assertThat(configuredRegistry).isNotNull();

final SSLConnectionSocketFactory socketFactory =
(SSLConnectionSocketFactory) configuredRegistry.lookup("https");
assertThat(socketFactory).isNotNull();

final Field hostnameVerifierField =
FieldUtils.getField(SSLConnectionSocketFactory.class, "hostnameVerifier", true);
assertThat(hostnameVerifierField.get(socketFactory)).isInstanceOf(HostnameVerifier.class);
}

@Test
public void canUseASystemHostnameVerifierByDefaultWhenTlsConfigurationSpecified() throws Exception {
final TlsConfiguration tlsConfiguration = new TlsConfiguration();
tlsConfiguration.setVerifyHostname(true);
configuration.setTlsConfiguration(tlsConfiguration);

final Registry<ConnectionSocketFactory> configuredRegistry;
configuredRegistry = builder.using(configuration).createConfiguredRegistry();
assertThat(configuredRegistry).isNotNull();

final SSLConnectionSocketFactory socketFactory =
(SSLConnectionSocketFactory) configuredRegistry.lookup("https");
assertThat(socketFactory).isNotNull();

final Field hostnameVerifierField =
FieldUtils.getField(SSLConnectionSocketFactory.class, "hostnameVerifier", true);
assertThat(hostnameVerifierField.get(socketFactory)).isInstanceOf(HostnameVerifier.class);
}

@Test
public void createClientCanPassCustomVerifierToApacheBuilder() throws Exception {
final HostnameVerifier customVerifier = (s, sslSession) -> false;

assertThat(builder.using(customVerifier).createClient(apacheBuilder, connectionManager, "test")).isNotNull();

final Field hostnameVerifierField =
FieldUtils.getField(org.apache.http.impl.client.HttpClientBuilder.class, "hostnameVerifier", true);
assertThat(hostnameVerifierField.get(apacheBuilder)).isSameAs(customVerifier);
}

@Test
public void doesNotReuseConnectionsIfKeepAliveIsZero() throws Exception {
Expand Down
Expand Up @@ -31,6 +31,7 @@
import org.junit.Test;
import org.mockito.ArgumentCaptor;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
Expand Down Expand Up @@ -262,6 +263,13 @@ public void usesACustomDnsResolver() {
verify(apacheHttpClientBuilder).using(customDnsResolver);
}

@Test
public void usesACustomHostnameVerifier() {
final HostnameVerifier customHostnameVerifier = new NoopHostnameVerifier();
builder.using(customHostnameVerifier);
verify(apacheHttpClientBuilder).using(customHostnameVerifier);
}

@Test
public void usesACustomConnectionFactoryRegistry() throws Exception {
final SSLContext ctx = SSLContext.getInstance(SSLConnectionSocketFactory.TLS);
Expand Down

0 comments on commit e34310b

Please sign in to comment.