Browse Source

default client: use custom HostnameVerifier if overridden

Sometimes, it's useful to override the hostname verifier for SSL connections.
One example, would be when you're developing against a test server managed by
another company that's using a self-signed certificate with a mis-matched
hostname. This patch enables that usage by overriding the default
HostnameVerifier in a Dagger module.

Adding test coverage required switching the TrustingSSLSocketFactory from
using an anonymous cipher suite to one that authenticates.  A test keystore is
used for this purpose.  It contains two self-signed certificates, one
each with alias (and CN) "localhost" and "bad.example.com".  The
TrustingSSLSocketFactory is no longer a singleton; it now
optionally takes a key alias as an argument.
4.x
David M. Carr 11 years ago committed by adriancole
parent
commit
f1cea1cca5
  1. 4
      NOTICE
  2. 6
      core/src/main/java/feign/Client.java
  3. 7
      core/src/main/java/feign/Feign.java
  4. 26
      core/src/test/java/feign/AcceptAllHostnameVerifier.java
  5. 29
      core/src/test/java/feign/FeignTest.java
  6. 99
      core/src/test/java/feign/TrustingSSLSocketFactory.java
  7. BIN
      core/src/test/resources/keystore.jks

4
NOTICE

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
Feign
Copyright 2013 Netflix, Inc.
Portions of this software developed by Commerce Technologies, Inc.

6
core/src/main/java/feign/Client.java

@ -28,6 +28,7 @@ import java.util.List; @@ -28,6 +28,7 @@ import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
@ -55,9 +56,11 @@ public interface Client { @@ -55,9 +56,11 @@ public interface Client {
public static class Default implements Client {
private final Lazy<SSLSocketFactory> sslContextFactory;
private final Lazy<HostnameVerifier> hostnameVerifier;
@Inject public Default(Lazy<SSLSocketFactory> sslContextFactory) {
@Inject public Default(Lazy<SSLSocketFactory> sslContextFactory, Lazy<HostnameVerifier> hostnameVerifier) {
this.sslContextFactory = sslContextFactory;
this.hostnameVerifier = hostnameVerifier;
}
@Override public Response execute(Request request, Options options) throws IOException {
@ -70,6 +73,7 @@ public interface Client { @@ -70,6 +73,7 @@ public interface Client {
if (connection instanceof HttpsURLConnection) {
HttpsURLConnection sslCon = (HttpsURLConnection) connection;
sslCon.setSSLSocketFactory(sslContextFactory.get());
sslCon.setHostnameVerifier(hostnameVerifier.get());
}
connection.setConnectTimeout(options.connectTimeoutMillis());
connection.setReadTimeout(options.readTimeoutMillis());

7
core/src/main/java/feign/Feign.java

@ -29,6 +29,8 @@ import feign.codec.IncrementalDecoder; @@ -29,6 +29,8 @@ import feign.codec.IncrementalDecoder;
import javax.inject.Named;
import javax.inject.Singleton;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import java.io.Closeable;
import java.lang.reflect.Method;
@ -104,6 +106,11 @@ public abstract class Feign implements Closeable { @@ -104,6 +106,11 @@ public abstract class Feign implements Closeable {
return SSLSocketFactory.class.cast(SSLSocketFactory.getDefault());
}
@Provides
HostnameVerifier hostnameVerifier() {
return HttpsURLConnection.getDefaultHostnameVerifier();
}
@Provides Client httpClient(Client.Default client) {
return client;
}

26
core/src/test/java/feign/AcceptAllHostnameVerifier.java

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
/*
* Copyright 2013 Netflix, Inc.
*
* Licensed 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 feign;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;
final class AcceptAllHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String s, SSLSession sslSession) {
return true;
}
}

29
core/src/test/java/feign/FeignTest.java

@ -31,6 +31,7 @@ import org.testng.annotations.Test; @@ -31,6 +31,7 @@ import org.testng.annotations.Test;
import javax.inject.Named;
import javax.inject.Singleton;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.io.Reader;
@ -522,7 +523,7 @@ public class FeignTest { @@ -522,7 +523,7 @@ public class FeignTest {
}
}
@Module(injects = Client.Default.class, overrides = true)
@Module(injects = Client.Default.class, overrides = true, addsTo = Feign.Defaults.class)
static class TrustSSLSockets {
@Provides SSLSocketFactory trustingSSLSocketFactory() {
return TrustingSSLSocketFactory.get();
@ -531,7 +532,7 @@ public class FeignTest { @@ -531,7 +532,7 @@ public class FeignTest {
@Test public void canOverrideSSLSocketFactory() throws IOException, InterruptedException {
MockWebServer server = new MockWebServer();
server.useHttps(TrustingSSLSocketFactory.get(), false);
server.useHttps(TrustingSSLSocketFactory.get("localhost"), false);
server.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
server.play();
@ -544,9 +545,31 @@ public class FeignTest { @@ -544,9 +545,31 @@ public class FeignTest {
}
}
@Module(injects = Client.Default.class, overrides = true, addsTo = Feign.Defaults.class)
static class DisableHostnameVerification {
@Provides HostnameVerifier acceptAllHostnameVerifier() {
return new AcceptAllHostnameVerifier();
}
}
@Test public void canOverrideHostnameVerifier() throws IOException, InterruptedException {
MockWebServer server = new MockWebServer();
server.useHttps(TrustingSSLSocketFactory.get("bad.example.com"), false);
server.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
server.play();
try {
TestInterface api = Feign.create(TestInterface.class, "https://localhost:" + server.getPort(),
new TestInterface.Module(), new TrustSSLSockets(), new DisableHostnameVerification());
api.post();
} finally {
server.shutdown();
}
}
@Test public void retriesFailedHandshake() throws IOException, InterruptedException {
MockWebServer server = new MockWebServer();
server.useHttps(TrustingSSLSocketFactory.get(), false);
server.useHttps(TrustingSSLSocketFactory.get("localhost"), false);
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
server.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
server.play();

99
core/src/test/java/feign/TrustingSSLSocketFactory.java

@ -15,11 +15,24 @@ @@ -15,11 +15,24 @@
*/
package feign;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.io.Closer;
import com.google.common.io.InputSupplier;
import com.google.common.io.Resources;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.security.KeyStore;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import javax.inject.Provider;
import javax.net.ssl.KeyManager;
@ -27,22 +40,40 @@ import javax.net.ssl.SSLContext; @@ -27,22 +40,40 @@ import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
import static com.google.common.base.Throwables.propagate;
/**
* used for ssl tests so that they can avoid having to read a keystore.
* Used for ssl tests to simplify setup.
*/
final class TrustingSSLSocketFactory extends SSLSocketFactory implements X509TrustManager, KeyManager {
final class TrustingSSLSocketFactory extends SSLSocketFactory implements X509TrustManager, X509KeyManager {
private static LoadingCache<String, SSLSocketFactory> sslSocketFactories =
CacheBuilder.newBuilder().build(new CacheLoader<String, SSLSocketFactory>() {
@Override
public SSLSocketFactory load(String serverAlias) throws Exception {
return new TrustingSSLSocketFactory(serverAlias);
}
});
public static SSLSocketFactory get() {
return Singleton.INSTANCE.get();
return get("");
}
public static SSLSocketFactory get(String serverAlias) {
return sslSocketFactories.getUnchecked(serverAlias);
}
private static final char[] KEYSTORE_PASSWORD = "password".toCharArray();
private final SSLSocketFactory delegate;
private final String serverAlias;
private final PrivateKey privateKey;
private final X509Certificate[] certificateChain;
private TrustingSSLSocketFactory() {
private TrustingSSLSocketFactory(String serverAlias) {
try {
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(new KeyManager[]{this}, new TrustManager[]{this}, new SecureRandom());
@ -50,6 +81,20 @@ final class TrustingSSLSocketFactory extends SSLSocketFactory implements X509Tru @@ -50,6 +81,20 @@ final class TrustingSSLSocketFactory extends SSLSocketFactory implements X509Tru
} catch (Exception e) {
throw propagate(e);
}
this.serverAlias = serverAlias;
if (serverAlias.isEmpty()) {
this.privateKey = null;
this.certificateChain = null;
} else {
try {
KeyStore keyStore = loadKeyStore(Resources.newInputStreamSupplier(Resources.getResource("keystore.jks")));
this.privateKey = (PrivateKey) keyStore.getKey(serverAlias, KEYSTORE_PASSWORD);
Certificate[] rawChain = keyStore.getCertificateChain(serverAlias);
this.certificateChain = Arrays.copyOf(rawChain, rawChain.length, X509Certificate[].class);
} catch (Exception e) {
throw propagate(e);
}
}
}
@Override public String[] getDefaultCipherSuites() {
@ -100,15 +145,49 @@ final class TrustingSSLSocketFactory extends SSLSocketFactory implements X509Tru @@ -100,15 +145,49 @@ final class TrustingSSLSocketFactory extends SSLSocketFactory implements X509Tru
public void checkServerTrusted(X509Certificate[] certs, String authType) {
}
private final static String[] ENABLED_CIPHER_SUITES = {"SSL_DH_anon_WITH_RC4_128_MD5"};
@Override
public String[] getClientAliases(String keyType, Principal[] issuers) {
return null;
}
private static enum Singleton implements Provider<SSLSocketFactory> {
INSTANCE;
@Override
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
return null;
}
private final SSLSocketFactory sslSocketFactory = new TrustingSSLSocketFactory();
@Override
public String[] getServerAliases(String keyType, Principal[] issuers) {
return null;
}
@Override public SSLSocketFactory get() {
return sslSocketFactory;
@Override
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
return serverAlias;
}
@Override
public X509Certificate[] getCertificateChain(String alias) {
return certificateChain;
}
@Override
public PrivateKey getPrivateKey(String alias) {
return privateKey;
}
private static KeyStore loadKeyStore(InputSupplier<InputStream> inputStreamSupplier) throws IOException {
Closer closer = Closer.create();
try {
InputStream inputStream = closer.register(inputStreamSupplier.getInput());
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(inputStream, KEYSTORE_PASSWORD);
return keyStore;
} catch (Throwable e) {
throw closer.rethrow(e);
} finally {
closer.close();
}
}
private final static String[] ENABLED_CIPHER_SUITES = {"SSL_RSA_WITH_RC4_128_MD5"};
}

BIN
core/src/test/resources/keystore.jks

Binary file not shown.
Loading…
Cancel
Save