Skip to content

Commit 899fd7a

Browse files
author
Stephane Landelle
committed
Move the hostname verification to after the SSL handshake has completed (Netty Only), back port #525
1 parent dc138e2 commit 899fd7a

File tree

3 files changed

+122
-75
lines changed

3 files changed

+122
-75
lines changed

src/main/java/com/ning/http/client/providers/netty/NettyConnectListener.java

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import com.ning.http.client.ProxyServer;
2222
import com.ning.http.client.Request;
2323
import com.ning.http.client.uri.UriComponents;
24-
import com.ning.http.util.AllowAllHostnameVerifier;
24+
import com.ning.http.util.Base64;
2525
import com.ning.http.util.ProxyUtils;
2626

2727
import org.jboss.netty.buffer.ChannelBuffer;
@@ -34,22 +34,21 @@
3434
import org.slf4j.LoggerFactory;
3535

3636
import javax.net.ssl.HostnameVerifier;
37+
import javax.net.ssl.SSLEngine;
38+
import javax.net.ssl.SSLSession;
3739

3840
import java.io.IOException;
3941
import java.net.ConnectException;
40-
import java.net.InetSocketAddress;
4142
import java.nio.channels.ClosedChannelException;
42-
import java.util.concurrent.atomic.AtomicBoolean;
4343

4444
/**
4545
* Non Blocking connect.
4646
*/
4747
final class NettyConnectListener<T> implements ChannelFutureListener {
48-
private final static Logger logger = LoggerFactory.getLogger(NettyConnectListener.class);
48+
private static final Logger LOGGER = LoggerFactory.getLogger(NettyConnectListener.class);
4949
private final AsyncHttpClientConfig config;
5050
private final NettyResponseFuture<T> future;
5151
private final HttpRequest nettyRequest;
52-
private final AtomicBoolean handshakeDone = new AtomicBoolean(false);
5352

5453
private NettyConnectListener(AsyncHttpClientConfig config,
5554
NettyResponseFuture<T> future) {
@@ -66,38 +65,51 @@ public final void operationComplete(ChannelFuture f) throws Exception {
6665
if (f.isSuccess()) {
6766
Channel channel = f.getChannel();
6867
channel.getPipeline().getContext(NettyAsyncHttpProvider.class).setAttachment(future);
69-
SslHandler sslHandler = (SslHandler) channel.getPipeline().get(NettyAsyncHttpProvider.SSL_HANDLER);
70-
if (!handshakeDone.getAndSet(true) && (sslHandler != null)) {
71-
((SslHandler) channel.getPipeline().get(NettyAsyncHttpProvider.SSL_HANDLER)).handshake().addListener(this);
72-
return;
73-
}
74-
75-
HostnameVerifier v = config.getHostnameVerifier();
76-
if (sslHandler != null && !(v instanceof AllowAllHostnameVerifier)) {
77-
// TODO: channel.getRemoteAddress()).getHostName() is very expensive. Should cache the result.
78-
if (!v.verify(InetSocketAddress.class.cast(channel.getRemoteAddress()).getHostName(),
79-
sslHandler.getEngine().getSession())) {
80-
throw new ConnectException("HostnameVerifier exception.");
81-
}
68+
final SslHandler sslHandler = (SslHandler) channel.getPipeline().get(NettyAsyncHttpProvider.SSL_HANDLER);
69+
70+
final HostnameVerifier hostnameVerifier = config.getHostnameVerifier();
71+
if (hostnameVerifier != null && sslHandler != null) {
72+
final String host = future.getURI().getHost();
73+
sslHandler.handshake().addListener(new ChannelFutureListener() {
74+
@Override
75+
public void operationComplete(ChannelFuture handshakeFuture) throws Exception {
76+
if (handshakeFuture.isSuccess()) {
77+
Channel channel = (Channel) handshakeFuture.getChannel();
78+
SSLEngine engine = sslHandler.getEngine();
79+
SSLSession session = engine.getSession();
80+
81+
LOGGER.debug("onFutureSuccess: session = {}, id = {}, isValid = {}, host = {}", session.toString(),
82+
Base64.encode(session.getId()), session.isValid(), host);
83+
if (!hostnameVerifier.verify(host, session)) {
84+
ConnectException exception = new ConnectException("HostnameVerifier exception");
85+
future.abort(exception);
86+
throw exception;
87+
} else {
88+
future.provider().writeRequest(channel, config, future);
89+
}
90+
}
91+
}
92+
});
93+
} else {
94+
future.provider().writeRequest(f.getChannel(), config, future);
8295
}
8396

84-
future.provider().writeRequest(f.getChannel(), config, future);
8597
} else {
8698
Throwable cause = f.getCause();
8799

88100
boolean canRetry = future.canRetry();
89-
logger.debug("Trying to recover a dead cached channel {} with a retry value of {} ", f.getChannel(), canRetry);
101+
LOGGER.debug("Trying to recover a dead cached channel {} with a retry value of {} ", f.getChannel(), canRetry);
90102
if (canRetry && cause != null && (NettyAsyncHttpProvider.abortOnDisconnectException(cause)
91103
|| cause instanceof ClosedChannelException
92104
|| future.getState() != NettyResponseFuture.STATE.NEW)) {
93105

94-
logger.debug("Retrying {} ", nettyRequest);
106+
LOGGER.debug("Retrying {} ", nettyRequest);
95107
if (future.provider().remotelyClosed(f.getChannel(), future)) {
96108
return;
97109
}
98110
}
99111

100-
logger.debug("Failed to recover from exception: {} with channel {}", cause, f.getChannel());
112+
LOGGER.debug("Failed to recover from exception: {} with channel {}", cause, f.getChannel());
101113

102114
boolean printCause = f.getCause() != null && cause.getMessage() != null;
103115
ConnectException e = new ConnectException(printCause ? cause.getMessage() + " to " + future.getURI().toString() : future.getURI().toString());

src/test/java/com/ning/http/client/async/BasicHttpsTest.java

Lines changed: 88 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.ning.http.client.AsyncHttpClient;
1919
import com.ning.http.client.AsyncHttpClientConfig.Builder;
2020
import com.ning.http.client.Response;
21+
2122
import org.eclipse.jetty.server.Request;
2223
import org.eclipse.jetty.server.Server;
2324
import org.eclipse.jetty.server.handler.AbstractHandler;
@@ -34,16 +35,19 @@
3435
import javax.net.ssl.SSLContext;
3536
import javax.net.ssl.SSLHandshakeException;
3637
import javax.net.ssl.TrustManager;
38+
import javax.net.ssl.TrustManagerFactory;
3739
import javax.net.ssl.X509TrustManager;
3840
import javax.servlet.ServletException;
3941
import javax.servlet.http.HttpServletRequest;
4042
import javax.servlet.http.HttpServletResponse;
43+
4144
import java.io.File;
4245
import java.io.IOException;
4346
import java.io.InputStream;
4447
import java.net.ConnectException;
4548
import java.net.ServerSocket;
4649
import java.net.URL;
50+
import java.security.GeneralSecurityException;
4751
import java.security.KeyStore;
4852
import java.security.SecureRandom;
4953
import java.security.cert.CertificateException;
@@ -207,7 +211,7 @@ public void setUpGlobal() throws Exception {
207211

208212
@Test(groups = { "standalone", "default_provider" })
209213
public void zeroCopyPostTest() throws Throwable {
210-
final AsyncHttpClient client = getAsyncHttpClient(new Builder().setSSLContext(createSSLContext()).build());
214+
final AsyncHttpClient client = getAsyncHttpClient(new Builder().setSSLContext(createSSLContext(new AtomicBoolean(true))).build());
211215
try {
212216
ClassLoader cl = getClass().getClassLoader();
213217
// override system properties
@@ -226,7 +230,7 @@ public void zeroCopyPostTest() throws Throwable {
226230

227231
@Test(groups = { "standalone", "default_provider" })
228232
public void multipleSSLRequestsTest() throws Throwable {
229-
final AsyncHttpClient c = getAsyncHttpClient(new Builder().setSSLContext(createSSLContext()).build());
233+
final AsyncHttpClient c = getAsyncHttpClient(new Builder().setSSLContext(createSSLContext(new AtomicBoolean(true))).build());
230234
try {
231235
String body = "hello there";
232236

@@ -246,7 +250,7 @@ public void multipleSSLRequestsTest() throws Throwable {
246250

247251
@Test(groups = { "standalone", "default_provider" })
248252
public void multipleSSLWithoutCacheTest() throws Throwable {
249-
final AsyncHttpClient c = getAsyncHttpClient(new Builder().setSSLContext(createSSLContext()).setAllowSslConnectionPool(false).build());
253+
final AsyncHttpClient c = getAsyncHttpClient(new Builder().setSSLContext(createSSLContext(new AtomicBoolean(true))).setAllowSslConnectionPool(false).build());
250254
try {
251255
String body = "hello there";
252256
c.preparePost(getTargetUrl()).setBody(body).setHeader("Content-Type", "text/html").execute();
@@ -262,55 +266,72 @@ public void multipleSSLWithoutCacheTest() throws Throwable {
262266
}
263267

264268
@Test(groups = { "standalone", "default_provider" })
265-
public void reconnectsAfterFailedCertificationPath() throws Throwable {
266-
final AsyncHttpClient c = getAsyncHttpClient(new Builder().setSSLContext(createSSLContext()).build());
269+
public void reconnectsAfterFailedCertificationPath() throws Exception {
270+
271+
AtomicBoolean trust = new AtomicBoolean(false);
272+
AsyncHttpClient c = getAsyncHttpClient(new Builder().setSSLContext(createSSLContext(trust)).build());
267273
try {
268-
final String body = "hello there";
274+
String body = "hello there";
269275

270-
TRUST_SERVER_CERT.set(false);
276+
// first request fails because server certificate is rejected
277+
Throwable cause = null;
271278
try {
272-
// first request fails because server certificate is rejected
273-
try {
274-
c.preparePost(getTargetUrl()).setBody(body).setHeader("Content-Type", "text/html").execute().get(TIMEOUT, TimeUnit.SECONDS);
275-
} catch (final ExecutionException e) {
276-
Throwable cause = e.getCause();
277-
if (cause instanceof ConnectException) {
278-
assertNotNull(cause.getCause());
279-
assertTrue(cause.getCause() instanceof SSLHandshakeException);
280-
} else {
281-
assertTrue(cause instanceof SSLHandshakeException);
282-
}
279+
c.preparePost(getTargetUrl()).setBody(body).setHeader("Content-Type", "text/html").execute().get(TIMEOUT, TimeUnit.SECONDS);
280+
} catch (final ExecutionException e) {
281+
cause = e.getCause();
282+
if (cause instanceof ConnectException) {
283+
//assertNotNull(cause.getCause());
284+
assertTrue(cause.getCause() instanceof SSLHandshakeException, "Expected an SSLHandshakeException, got a " + cause.getCause());
285+
} else {
286+
assertTrue(cause instanceof IOException, "Expected an IOException, got a " + cause);
283287
}
288+
} catch (Exception e) {
289+
System.err.println("WTF"+ e.getMessage());
290+
}
291+
assertNotNull(cause);
284292

285-
TRUST_SERVER_CERT.set(true);
286-
287-
// second request should succeed
288-
final Response response = c.preparePost(getTargetUrl()).setBody(body).setHeader("Content-Type", "text/html").execute().get(TIMEOUT, TimeUnit.SECONDS);
293+
// second request should succeed
294+
trust.set(true);
295+
Response response = c.preparePost(getTargetUrl()).setBody(body).setHeader("Content-Type", "text/html").execute().get(TIMEOUT, TimeUnit.SECONDS);
289296

290-
assertEquals(response.getResponseBody(), body);
291-
} finally {
292-
TRUST_SERVER_CERT.set(true);
293-
}
297+
assertEquals(response.getResponseBody(), body);
294298
} finally {
295299
c.close();
296300
}
297301
}
298302

299-
private static SSLContext createSSLContext() {
303+
private static KeyManager[] createKeyManagers() throws GeneralSecurityException, IOException {
304+
InputStream keyStoreStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("ssltest-cacerts.jks");
305+
char[] keyStorePassword = "changeit".toCharArray();
306+
KeyStore ks = KeyStore.getInstance("JKS");
307+
ks.load(keyStoreStream, keyStorePassword);
308+
assert(ks.size() > 0);
309+
310+
// Set up key manager factory to use our key store
311+
char[] certificatePassword = "changeit".toCharArray();
312+
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
313+
kmf.init(ks, certificatePassword);
314+
315+
// Initialize the SSLContext to work with our key managers.
316+
return kmf.getKeyManagers();
317+
}
318+
319+
private static TrustManager[] createTrustManagers() throws GeneralSecurityException, IOException {
320+
InputStream keyStoreStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("ssltest-keystore.jks");
321+
char[] keyStorePassword = "changeit".toCharArray();
322+
KeyStore ks = KeyStore.getInstance("JKS");
323+
ks.load(keyStoreStream, keyStorePassword);
324+
assert(ks.size() > 0);
325+
326+
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
327+
tmf.init(ks);
328+
return tmf.getTrustManagers();
329+
}
330+
331+
public static SSLContext createSSLContext(AtomicBoolean trust) {
300332
try {
301-
InputStream keyStoreStream = BasicHttpsTest.class.getResourceAsStream("ssltest-cacerts.jks");
302-
char[] keyStorePassword = "changeit".toCharArray();
303-
KeyStore ks = KeyStore.getInstance("JKS");
304-
ks.load(keyStoreStream, keyStorePassword);
305-
306-
// Set up key manager factory to use our key store
307-
char[] certificatePassword = "changeit".toCharArray();
308-
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
309-
kmf.init(ks, certificatePassword);
310-
311-
// Initialize the SSLContext to work with our key managers.
312-
KeyManager[] keyManagers = kmf.getKeyManagers();
313-
TrustManager[] trustManagers = new TrustManager[] { DUMMY_TRUST_MANAGER };
333+
KeyManager[] keyManagers = createKeyManagers();
334+
TrustManager[] trustManagers = new TrustManager[] { dummyTrustManager(trust, (X509TrustManager) createTrustManagers()[0]) };
314335
SecureRandom secureRandom = new SecureRandom();
315336

316337
SSLContext sslContext = SSLContext.getInstance("TLS");
@@ -322,20 +343,39 @@ private static SSLContext createSSLContext() {
322343
}
323344
}
324345

325-
private static final AtomicBoolean TRUST_SERVER_CERT = new AtomicBoolean(true);
326-
private static final TrustManager DUMMY_TRUST_MANAGER = new X509TrustManager() {
327-
public X509Certificate[] getAcceptedIssuers() {
328-
return new X509Certificate[0];
346+
public static class DummyTrustManager implements X509TrustManager {
347+
348+
private final X509TrustManager tm;
349+
private final AtomicBoolean trust;
350+
351+
public DummyTrustManager(final AtomicBoolean trust, final X509TrustManager tm) {
352+
this.trust = trust;
353+
this.tm = tm;
329354
}
330355

331-
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
356+
@Override
357+
public void checkClientTrusted(X509Certificate[] chain, String authType)
358+
throws CertificateException {
359+
tm.checkClientTrusted(chain, authType);
332360
}
333361

334-
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
335-
if (!TRUST_SERVER_CERT.get()) {
362+
@Override
363+
public void checkServerTrusted(X509Certificate[] chain, String authType)
364+
throws CertificateException {
365+
if (!trust.get()) {
336366
throw new CertificateException("Server certificate not trusted.");
337367
}
368+
tm.checkServerTrusted(chain, authType);
338369
}
339-
};
340370

371+
@Override
372+
public X509Certificate[] getAcceptedIssuers() {
373+
return tm.getAcceptedIssuers();
374+
}
375+
}
376+
377+
private static TrustManager dummyTrustManager(final AtomicBoolean trust, final X509TrustManager tm) {
378+
return new DummyTrustManager(trust, tm);
379+
380+
}
341381
}

src/test/java/com/ning/http/client/async/grizzly/GrizzlyBasicHttpsTest.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,4 @@ public class GrizzlyBasicHttpsTest extends BasicHttpsTest {
2424
public AsyncHttpClient getAsyncHttpClient(AsyncHttpClientConfig config) {
2525
return ProviderUtil.grizzlyProvider(config);
2626
}
27-
28-
@Override
29-
public void zeroCopyPostTest() throws Throwable {
30-
super.zeroCopyPostTest(); // To change body of overridden methods use File | Settings | File Templates.
31-
}
3227
}

0 commit comments

Comments
 (0)