Skip to content

Commit 3c9152e

Browse files
committed
Merge pull request AsyncHttpClient#510 from wsargent/fix-197
Fix for AsyncHttpClient#197 -- use a hostname verifier that does hostname verification
2 parents c9b5391 + bbdc1b3 commit 3c9152e

File tree

11 files changed

+256
-8
lines changed

11 files changed

+256
-8
lines changed

api/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import org.asynchttpclient.filter.IOExceptionFilter;
2222
import org.asynchttpclient.filter.RequestFilter;
2323
import org.asynchttpclient.filter.ResponseFilter;
24-
import org.asynchttpclient.util.AllowAllHostnameVerifier;
24+
import org.asynchttpclient.util.DefaultHostnameVerifier;
2525
import org.asynchttpclient.util.ProxyUtils;
2626

2727
import javax.net.ssl.HostnameVerifier;
@@ -596,7 +596,7 @@ public static class Builder {
596596
private boolean allowSslConnectionPool = true;
597597
private boolean useRawUrl = false;
598598
private boolean removeQueryParamOnRedirect = true;
599-
private HostnameVerifier hostnameVerifier = new AllowAllHostnameVerifier();
599+
private HostnameVerifier hostnameVerifier = new DefaultHostnameVerifier();
600600
private int ioThreadMultiplier = 2;
601601
private boolean strict302Handling;
602602
private boolean spdyEnabled;

api/src/main/java/org/asynchttpclient/AsyncHttpClientConfigBean.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.asynchttpclient.filter.IOExceptionFilter;
1616
import org.asynchttpclient.filter.RequestFilter;
1717
import org.asynchttpclient.filter.ResponseFilter;
18+
import org.asynchttpclient.util.DefaultHostnameVerifier;
1819
import org.asynchttpclient.util.ProxyUtils;
1920

2021
import javax.net.ssl.HostnameVerifier;
@@ -70,12 +71,7 @@ void configureDefaults() {
7071
allowSslConnectionPool = true;
7172
useRawUrl = false;
7273
removeQueryParamOnRedirect = true;
73-
hostnameVerifier = new HostnameVerifier() {
74-
75-
public boolean verify(String s, SSLSession sslSession) {
76-
return true;
77-
}
78-
};
74+
hostnameVerifier = new DefaultHostnameVerifier();
7975
}
8076

8177
void configureExecutors() {
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* To the extent possible under law, Kevin Locke has waived all copyright and
3+
* related or neighboring rights to this work.
4+
* <p/>
5+
* A legal description of this waiver is available in <a href="https://gist.github.com/kevinoid/3829665">LICENSE.txt</a>
6+
*/
7+
package org.asynchttpclient.util;
8+
9+
import javax.net.ssl.HostnameVerifier;
10+
import javax.net.ssl.SSLPeerUnverifiedException;
11+
import javax.net.ssl.SSLSession;
12+
import javax.security.auth.kerberos.KerberosPrincipal;
13+
import java.security.Principal;
14+
import java.security.cert.Certificate;
15+
import java.security.cert.CertificateException;
16+
import java.security.cert.X509Certificate;
17+
import java.util.logging.Level;
18+
import java.util.logging.Logger;
19+
20+
/**
21+
* Uses the internal HostnameChecker to verify the server's hostname matches with the
22+
* certificate. This is a requirement for HTTPS, but the raw SSLEngine does not have
23+
* this functionality. As such, it has to be added in manually. For a more complete
24+
* description of hostname verification and why it's important,
25+
* please read
26+
* <a href="http://tersesystems.com/2014/03/23/fixing-hostname-verification/">Fixing
27+
* Hostname Verification</a>.
28+
* <p/>
29+
* This code is based on Kevin Locke's <a href="http://kevinlocke.name/bits/2012/10/03/ssl-certificate-verification-in-dispatch-and-asynchttpclient/">guide</a> .
30+
* <p/>
31+
*/
32+
public class DefaultHostnameVerifier implements HostnameVerifier {
33+
34+
private HostnameChecker checker;
35+
36+
private HostnameVerifier extraHostnameVerifier;
37+
38+
// Logger to log exceptions.
39+
private static final Logger log = Logger.getLogger(DefaultHostnameVerifier.class.getName());
40+
41+
/**
42+
* A hostname verifier that uses the {{sun.security.util.HostnameChecker}} under the hood.
43+
*/
44+
public DefaultHostnameVerifier() {
45+
this.checker = new ProxyHostnameChecker();
46+
}
47+
48+
/**
49+
* A hostname verifier that takes an external hostname checker. Useful for testing.
50+
*
51+
* @param checker a hostnamechecker.
52+
*/
53+
public DefaultHostnameVerifier(HostnameChecker checker) {
54+
this.checker = checker;
55+
}
56+
57+
/**
58+
* A hostname verifier that falls back to another hostname verifier if not found.
59+
*
60+
* @param extraHostnameVerifier another hostname verifier.
61+
*/
62+
public DefaultHostnameVerifier(HostnameVerifier extraHostnameVerifier) {
63+
this.checker = new ProxyHostnameChecker();
64+
this.extraHostnameVerifier = extraHostnameVerifier;
65+
}
66+
67+
/**
68+
* A hostname verifier with a hostname checker, that falls back to another hostname verifier if not found.
69+
*
70+
* @param checker a custom HostnameChecker.
71+
* @param extraHostnameVerifier another hostname verifier.
72+
*/
73+
public DefaultHostnameVerifier(HostnameChecker checker, HostnameVerifier extraHostnameVerifier) {
74+
this.checker = checker;
75+
this.extraHostnameVerifier = extraHostnameVerifier;
76+
}
77+
78+
/**
79+
* Matches the hostname against the peer certificate in the session.
80+
*
81+
* @param hostname the IP address or hostname of the expected server.
82+
* @param session the SSL session containing the certificates with the ACTUAL hostname/ipaddress.
83+
* @return true if the hostname matches, false otherwise.
84+
*/
85+
private boolean hostnameMatches(String hostname, SSLSession session) {
86+
log.log(Level.FINE, "hostname = {0}, session = {1}", new Object[] { hostname, Base64.encode(session.getId()) });
87+
88+
try {
89+
final Certificate[] peerCertificates = session.getPeerCertificates();
90+
if (peerCertificates.length == 0) {
91+
log.log(Level.FINE, "No peer certificates");
92+
return false;
93+
}
94+
95+
if (peerCertificates[0] instanceof X509Certificate) {
96+
X509Certificate peerCertificate = (X509Certificate) peerCertificates[0];
97+
log.log(Level.FINE, "peerCertificate = {0}", peerCertificate);
98+
try {
99+
checker.match(hostname, peerCertificate);
100+
// Certificate matches hostname if no exception is thrown.
101+
return true;
102+
} catch (CertificateException ex) {
103+
log.log(Level.FINE, "Certificate does not match hostname", ex);
104+
}
105+
} else {
106+
log.log(Level.FINE, "Peer does not have any certificates or they aren't X.509");
107+
}
108+
return false;
109+
} catch (SSLPeerUnverifiedException ex) {
110+
log.log(Level.FINE, "Not using certificates for peers, try verifying the principal");
111+
try {
112+
Principal peerPrincipal = session.getPeerPrincipal();
113+
log.log(Level.FINE, "peerPrincipal = {0}", peerPrincipal);
114+
if (peerPrincipal instanceof KerberosPrincipal) {
115+
return checker.match(hostname, (KerberosPrincipal) peerPrincipal);
116+
} else {
117+
log.log(Level.FINE, "Can't verify principal, not Kerberos");
118+
}
119+
} catch (SSLPeerUnverifiedException ex2) {
120+
// Can't verify principal, no principal
121+
log.log(Level.FINE, "Can't verify principal, no principal", ex2);
122+
}
123+
return false;
124+
}
125+
}
126+
127+
/**
128+
* Verifies the hostname against the peer certificates in a session. Falls back to extraHostnameVerifier if
129+
* there is no match.
130+
*
131+
* @param hostname the IP address or hostname of the expected server.
132+
* @param session the SSL session containing the certificates with the ACTUAL hostname/ipaddress.
133+
* @return true if the hostname matches, false otherwise.
134+
*/
135+
public boolean verify(String hostname, SSLSession session) {
136+
if (hostnameMatches(hostname, session)) {
137+
return true;
138+
} else {
139+
return extraHostnameVerifier != null && extraHostnameVerifier.verify(hostname, session);
140+
}
141+
}
142+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (c) Will Sargent. All rights reserved.
3+
*
4+
* This program is licensed to you under the Apache License Version 2.0,
5+
* and you may not use this file except in compliance with the Apache License Version 2.0.
6+
* You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
7+
*
8+
* Unless required by applicable law or agreed to in writing,
9+
* software distributed under the Apache License Version 2.0 is distributed on an
10+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
12+
*/
13+
package org.asynchttpclient.util;
14+
15+
import java.security.Principal;
16+
import java.security.cert.CertificateException;
17+
import java.security.cert.X509Certificate;
18+
19+
/**
20+
* Hostname checker interface.
21+
*/
22+
public interface HostnameChecker {
23+
24+
public void match(String hostname, X509Certificate peerCertificate) throws CertificateException;
25+
26+
public boolean match(String hostname, Principal principal);
27+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright (c) Will Sargent. All rights reserved.
3+
*
4+
* This program is licensed to you under the Apache License Version 2.0,
5+
* and you may not use this file except in compliance with the Apache License Version 2.0.
6+
* You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
7+
*
8+
* Unless required by applicable law or agreed to in writing,
9+
* software distributed under the Apache License Version 2.0 is distributed on an
10+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
12+
*/
13+
package org.asynchttpclient.util;
14+
15+
import java.lang.reflect.InvocationTargetException;
16+
import java.lang.reflect.Method;
17+
import java.security.Principal;
18+
import java.security.cert.CertificateException;
19+
import java.security.cert.X509Certificate;
20+
21+
/**
22+
* A HostnameChecker proxy.
23+
*/
24+
public class ProxyHostnameChecker implements HostnameChecker {
25+
26+
public final static byte TYPE_TLS = 1;
27+
28+
private final Object checker = getHostnameChecker();
29+
30+
public ProxyHostnameChecker() {
31+
}
32+
33+
private Object getHostnameChecker() {
34+
final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
35+
try {
36+
final Class<Object> hostnameCheckerClass = (Class<Object>) classLoader.loadClass("sun.security.util.HostnameChecker");
37+
final Method instanceMethod = hostnameCheckerClass.getMethod("getInstance", Byte.TYPE);
38+
return instanceMethod.invoke(null, TYPE_TLS);
39+
} catch (ClassNotFoundException e) {
40+
throw new IllegalStateException(e);
41+
} catch (NoSuchMethodException e) {
42+
throw new IllegalStateException(e);
43+
} catch (InvocationTargetException e) {
44+
throw new IllegalStateException(e);
45+
} catch (IllegalAccessException e) {
46+
throw new IllegalStateException(e);
47+
}
48+
}
49+
50+
public void match(String hostname, X509Certificate peerCertificate) throws CertificateException {
51+
try {
52+
final Class<?> hostnameCheckerClass = checker.getClass();
53+
final Method checkMethod = hostnameCheckerClass.getMethod("match", String.class, X509Certificate.class);
54+
checkMethod.invoke(checker, hostname, peerCertificate);
55+
} catch (NoSuchMethodException e) {
56+
throw new IllegalStateException(e);
57+
} catch (InvocationTargetException e) {
58+
Throwable t = e.getCause();
59+
if (t instanceof CertificateException) {
60+
throw (CertificateException) t;
61+
} else {
62+
throw new IllegalStateException(e);
63+
}
64+
} catch (IllegalAccessException e) {
65+
throw new IllegalStateException(e);
66+
}
67+
}
68+
69+
public boolean match(String hostname, Principal principal) {
70+
try {
71+
final Class<?> hostnameCheckerClass = checker.getClass();
72+
final Method checkMethod = hostnameCheckerClass.getMethod("match", String.class, Principal.class);
73+
return (Boolean) checkMethod.invoke(null, hostname, principal);
74+
} catch (NoSuchMethodException e) {
75+
throw new IllegalStateException(e);
76+
} catch (InvocationTargetException e) {
77+
throw new IllegalStateException(e);
78+
} catch (IllegalAccessException e) {
79+
throw new IllegalStateException(e);
80+
}
81+
}
82+
83+
}
1.58 KB
Binary file not shown.
913 Bytes
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)