Skip to content

Commit a91d4b9

Browse files
committed
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.
1 parent 2553699 commit a91d4b9

7 files changed

Lines changed: 157 additions & 14 deletions

File tree

NOTICE

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Feign
2+
Copyright 2013 Netflix, Inc.
3+
4+
Portions of this software developed by Commerce Technologies, Inc.

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.Map;
2929

3030
import javax.inject.Inject;
31+
import javax.net.ssl.HostnameVerifier;
3132
import javax.net.ssl.HttpsURLConnection;
3233
import javax.net.ssl.SSLSocketFactory;
3334

@@ -55,9 +56,11 @@ public interface Client {
5556

5657
public static class Default implements Client {
5758
private final Lazy<SSLSocketFactory> sslContextFactory;
59+
private final Lazy<HostnameVerifier> hostnameVerifier;
5860

59-
@Inject public Default(Lazy<SSLSocketFactory> sslContextFactory) {
61+
@Inject public Default(Lazy<SSLSocketFactory> sslContextFactory, Lazy<HostnameVerifier> hostnameVerifier) {
6062
this.sslContextFactory = sslContextFactory;
63+
this.hostnameVerifier = hostnameVerifier;
6164
}
6265

6366
@Override public Response execute(Request request, Options options) throws IOException {
@@ -70,6 +73,7 @@ HttpURLConnection convertAndSend(Request request, Options options) throws IOExce
7073
if (connection instanceof HttpsURLConnection) {
7174
HttpsURLConnection sslCon = (HttpsURLConnection) connection;
7275
sslCon.setSSLSocketFactory(sslContextFactory.get());
76+
sslCon.setHostnameVerifier(hostnameVerifier.get());
7377
}
7478
connection.setConnectTimeout(options.connectTimeoutMillis());
7579
connection.setReadTimeout(options.readTimeoutMillis());

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929

3030
import javax.inject.Named;
3131
import javax.inject.Singleton;
32+
import javax.net.ssl.HostnameVerifier;
33+
import javax.net.ssl.HttpsURLConnection;
3234
import javax.net.ssl.SSLSocketFactory;
3335
import java.io.Closeable;
3436
import java.lang.reflect.Method;
@@ -104,6 +106,11 @@ public static class Defaults {
104106
return SSLSocketFactory.class.cast(SSLSocketFactory.getDefault());
105107
}
106108

109+
@Provides
110+
HostnameVerifier hostnameVerifier() {
111+
return HttpsURLConnection.getDefaultHostnameVerifier();
112+
}
113+
107114
@Provides Client httpClient(Client.Default client) {
108115
return client;
109116
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2013 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package feign;
17+
18+
import javax.net.ssl.HostnameVerifier;
19+
import javax.net.ssl.SSLSession;
20+
21+
final class AcceptAllHostnameVerifier implements HostnameVerifier {
22+
@Override
23+
public boolean verify(String s, SSLSession sslSession) {
24+
return true;
25+
}
26+
}

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
import javax.inject.Named;
3333
import javax.inject.Singleton;
34+
import javax.net.ssl.HostnameVerifier;
3435
import javax.net.ssl.SSLSocketFactory;
3536
import java.io.IOException;
3637
import java.io.Reader;
@@ -522,7 +523,7 @@ public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedExce
522523
}
523524
}
524525

525-
@Module(injects = Client.Default.class, overrides = true)
526+
@Module(injects = Client.Default.class, overrides = true, addsTo = Feign.Defaults.class)
526527
static class TrustSSLSockets {
527528
@Provides SSLSocketFactory trustingSSLSocketFactory() {
528529
return TrustingSSLSocketFactory.get();
@@ -531,7 +532,7 @@ static class TrustSSLSockets {
531532

532533
@Test public void canOverrideSSLSocketFactory() throws IOException, InterruptedException {
533534
MockWebServer server = new MockWebServer();
534-
server.useHttps(TrustingSSLSocketFactory.get(), false);
535+
server.useHttps(TrustingSSLSocketFactory.get("localhost"), false);
535536
server.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
536537
server.play();
537538

@@ -544,9 +545,31 @@ static class TrustSSLSockets {
544545
}
545546
}
546547

548+
@Module(injects = Client.Default.class, overrides = true, addsTo = Feign.Defaults.class)
549+
static class DisableHostnameVerification {
550+
@Provides HostnameVerifier acceptAllHostnameVerifier() {
551+
return new AcceptAllHostnameVerifier();
552+
}
553+
}
554+
555+
@Test public void canOverrideHostnameVerifier() throws IOException, InterruptedException {
556+
MockWebServer server = new MockWebServer();
557+
server.useHttps(TrustingSSLSocketFactory.get("bad.example.com"), false);
558+
server.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
559+
server.play();
560+
561+
try {
562+
TestInterface api = Feign.create(TestInterface.class, "https://localhost:" + server.getPort(),
563+
new TestInterface.Module(), new TrustSSLSockets(), new DisableHostnameVerification());
564+
api.post();
565+
} finally {
566+
server.shutdown();
567+
}
568+
}
569+
547570
@Test public void retriesFailedHandshake() throws IOException, InterruptedException {
548571
MockWebServer server = new MockWebServer();
549-
server.useHttps(TrustingSSLSocketFactory.get(), false);
572+
server.useHttps(TrustingSSLSocketFactory.get("localhost"), false);
550573
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
551574
server.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
552575
server.play();

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

Lines changed: 89 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,41 +15,86 @@
1515
*/
1616
package feign;
1717

18+
import com.google.common.cache.CacheBuilder;
19+
import com.google.common.cache.CacheLoader;
20+
import com.google.common.cache.LoadingCache;
21+
import com.google.common.io.Closer;
22+
import com.google.common.io.InputSupplier;
23+
import com.google.common.io.Resources;
24+
1825
import java.io.IOException;
26+
import java.io.InputStream;
1927
import java.net.InetAddress;
2028
import java.net.Socket;
29+
import java.security.KeyStore;
30+
import java.security.Principal;
31+
import java.security.PrivateKey;
2132
import java.security.SecureRandom;
33+
import java.security.cert.Certificate;
2234
import java.security.cert.X509Certificate;
35+
import java.util.Arrays;
2336

2437
import javax.inject.Provider;
2538
import javax.net.ssl.KeyManager;
2639
import javax.net.ssl.SSLContext;
2740
import javax.net.ssl.SSLSocket;
2841
import javax.net.ssl.SSLSocketFactory;
2942
import javax.net.ssl.TrustManager;
43+
import javax.net.ssl.X509KeyManager;
3044
import javax.net.ssl.X509TrustManager;
3145

3246
import static com.google.common.base.Throwables.propagate;
3347

3448
/**
35-
* used for ssl tests so that they can avoid having to read a keystore.
49+
* Used for ssl tests to simplify setup.
3650
*/
37-
final class TrustingSSLSocketFactory extends SSLSocketFactory implements X509TrustManager, KeyManager {
51+
final class TrustingSSLSocketFactory extends SSLSocketFactory implements X509TrustManager, X509KeyManager {
52+
53+
private static LoadingCache<String, SSLSocketFactory> sslSocketFactories =
54+
CacheBuilder.newBuilder().build(new CacheLoader<String, SSLSocketFactory>() {
55+
@Override
56+
public SSLSocketFactory load(String serverAlias) throws Exception {
57+
return new TrustingSSLSocketFactory(serverAlias);
58+
}
59+
});
3860

3961
public static SSLSocketFactory get() {
40-
return Singleton.INSTANCE.get();
62+
return get("");
4163
}
4264

65+
public static SSLSocketFactory get(String serverAlias) {
66+
return sslSocketFactories.getUnchecked(serverAlias);
67+
}
68+
69+
private static final char[] KEYSTORE_PASSWORD = "password".toCharArray();
70+
4371
private final SSLSocketFactory delegate;
72+
private final String serverAlias;
73+
private final PrivateKey privateKey;
74+
private final X509Certificate[] certificateChain;
4475

45-
private TrustingSSLSocketFactory() {
76+
private TrustingSSLSocketFactory(String serverAlias) {
4677
try {
4778
SSLContext sc = SSLContext.getInstance("SSL");
4879
sc.init(new KeyManager[]{this}, new TrustManager[]{this}, new SecureRandom());
4980
this.delegate = sc.getSocketFactory();
5081
} catch (Exception e) {
5182
throw propagate(e);
5283
}
84+
this.serverAlias = serverAlias;
85+
if (serverAlias.isEmpty()) {
86+
this.privateKey = null;
87+
this.certificateChain = null;
88+
} else {
89+
try {
90+
KeyStore keyStore = loadKeyStore(Resources.newInputStreamSupplier(Resources.getResource("keystore.jks")));
91+
this.privateKey = (PrivateKey) keyStore.getKey(serverAlias, KEYSTORE_PASSWORD);
92+
Certificate[] rawChain = keyStore.getCertificateChain(serverAlias);
93+
this.certificateChain = Arrays.copyOf(rawChain, rawChain.length, X509Certificate[].class);
94+
} catch (Exception e) {
95+
throw propagate(e);
96+
}
97+
}
5398
}
5499

55100
@Override public String[] getDefaultCipherSuites() {
@@ -100,15 +145,49 @@ public void checkClientTrusted(X509Certificate[] certs, String authType) {
100145
public void checkServerTrusted(X509Certificate[] certs, String authType) {
101146
}
102147

103-
private final static String[] ENABLED_CIPHER_SUITES = {"SSL_DH_anon_WITH_RC4_128_MD5"};
148+
@Override
149+
public String[] getClientAliases(String keyType, Principal[] issuers) {
150+
return null;
151+
}
104152

105-
private static enum Singleton implements Provider<SSLSocketFactory> {
106-
INSTANCE;
153+
@Override
154+
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
155+
return null;
156+
}
107157

108-
private final SSLSocketFactory sslSocketFactory = new TrustingSSLSocketFactory();
158+
@Override
159+
public String[] getServerAliases(String keyType, Principal[] issuers) {
160+
return null;
161+
}
109162

110-
@Override public SSLSocketFactory get() {
111-
return sslSocketFactory;
163+
@Override
164+
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
165+
return serverAlias;
166+
}
167+
168+
@Override
169+
public X509Certificate[] getCertificateChain(String alias) {
170+
return certificateChain;
171+
}
172+
173+
@Override
174+
public PrivateKey getPrivateKey(String alias) {
175+
return privateKey;
176+
}
177+
178+
private static KeyStore loadKeyStore(InputSupplier<InputStream> inputStreamSupplier) throws IOException {
179+
Closer closer = Closer.create();
180+
try {
181+
InputStream inputStream = closer.register(inputStreamSupplier.getInput());
182+
KeyStore keyStore = KeyStore.getInstance("JKS");
183+
keyStore.load(inputStream, KEYSTORE_PASSWORD);
184+
return keyStore;
185+
} catch (Throwable e) {
186+
throw closer.rethrow(e);
187+
} finally {
188+
closer.close();
112189
}
113190
}
191+
192+
private final static String[] ENABLED_CIPHER_SUITES = {"SSL_RSA_WITH_RC4_128_MD5"};
114193
}
4.38 KB
Binary file not shown.

0 commit comments

Comments
 (0)