SecureProtocolSocketFactory
that uses JSSE to create
* SSL sockets. It will also support host name verification to help preventing
* man-in-the-middle attacks. Host name verification is turned on by
* default but one will be able to turn it off, which might be a useful feature
* during development. Host name verification will make sure the SSL sessions
* server host name matches with the the host name returned in the
* server certificates "Common Name" field of the "SubjectDN" entry.
*
* @author Sebastian Hauer
* * DISCLAIMER: HttpClient developers DO NOT actively support this component. * The component is provided as a reference material, which may be inappropriate * for use without additional customization. *
*/ public class StrictSSLProtocolSocketFactory implements SecureProtocolSocketFactory { /** Log object for this class. */ private static final Log LOG = LogFactory.getLog(StrictSSLProtocolSocketFactory.class); /** Host name verify flag. */ private boolean verifyHostname = true; /** * Constructor for StrictSSLProtocolSocketFactory. * @param verifyHostname The host name verification flag. If set to *true
the SSL sessions server host name will be compared
* to the host name returned in the server certificates "Common Name"
* field of the "SubjectDN" entry. If these names do not match a
* Exception is thrown to indicate this. Enabling host name verification
* will help to prevent from man-in-the-middle attacks. If set to
* false
host name verification is turned off.
*
* Code sample:
*
* * Protocol stricthttps = new Protocol( * "https", new StrictSSLProtocolSocketFactory(true), 443); * * HttpClient client = new HttpClient(); * client.getHostConfiguration().setHost("localhost", 443, stricthttps); ** */ public StrictSSLProtocolSocketFactory(boolean verifyHostname) { super(); this.verifyHostname = verifyHostname; } /** * Constructor for StrictSSLProtocolSocketFactory. * Host name verification will be enabled by default. */ public StrictSSLProtocolSocketFactory() { super(); } /** * Set the host name verification flag. * * @param verifyHostname The host name verification flag. If set to *
true
the SSL sessions server host name will be compared
* to the host name returned in the server certificates "Common Name"
* field of the "SubjectDN" entry. If these names do not match a
* Exception is thrown to indicate this. Enabling host name verification
* will help to prevent from man-in-the-middle attacks. If set to
* false
host name verification is turned off.
*/
public void setHostnameVerification(boolean verifyHostname) {
this.verifyHostname = verifyHostname;
}
/**
* Gets the status of the host name verification flag.
*
* @return Host name verification flag. Either true
if host
* name verification is turned on, or false
if host name
* verification is turned off.
*/
public boolean getHostnameVerification() {
return verifyHostname;
}
/**
* @see SecureProtocolSocketFactory#createSocket(java.lang.String,int,java.net.InetAddress,int)
*/
public Socket createSocket(String host, int port,
InetAddress clientHost, int clientPort)
throws IOException, UnknownHostException {
SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket sslSocket = (SSLSocket) sf.createSocket(host, port,
clientHost,
clientPort);
verifyHostname(sslSocket);
return sslSocket;
}
/**
* Attempts to get a new socket connection to the given host within the given time limit.
* * This method employs several techniques to circumvent the limitations of older JREs that * do not support connect timeout. When running in JRE 1.4 or above reflection is used to * call Socket#connect(SocketAddress endpoint, int timeout) method. When executing in older * JREs a controller thread is executed. The controller thread attempts to create a new socket * within the given limit of time. If socket constructor does not return until the timeout * expires, the controller terminates and throws an {@link ConnectTimeoutException} *
* * @param host the host name/IP * @param port the port on the host * @param clientHost the local host name/IP to bind the socket to * @param clientPort the port on the local machine * @param params {@link HttpConnectionParams Http connection parameters} * * @return Socket a new socket * * @throws IOException if an I/O error occurs while creating the socket * @throws UnknownHostException if the IP address of the host cannot be * determined */ public Socket createSocket( final String host, final int port, final InetAddress localAddress, final int localPort, final HttpConnectionParams params ) throws IOException, UnknownHostException, ConnectTimeoutException { if (params == null) { throw new IllegalArgumentException("Parameters may not be null"); } int timeout = params.getConnectionTimeout(); Socket socket = null; SocketFactory socketfactory = SSLSocketFactory.getDefault(); if (timeout == 0) { socket = socketfactory.createSocket(host, port, localAddress, localPort); } else { socket = socketfactory.createSocket(); SocketAddress localaddr = new InetSocketAddress(localAddress, localPort); SocketAddress remoteaddr = new InetSocketAddress(host, port); socket.bind(localaddr); socket.connect(remoteaddr, timeout); } verifyHostname((SSLSocket)socket); return socket; } /** * @see SecureProtocolSocketFactory#createSocket(java.lang.String,int) */ public Socket createSocket(String host, int port) throws IOException, UnknownHostException { SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault(); SSLSocket sslSocket = (SSLSocket) sf.createSocket(host, port); verifyHostname(sslSocket); return sslSocket; } /** * @see SecureProtocolSocketFactory#createSocket(java.net.Socket,java.lang.String,int,boolean) */ public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException, UnknownHostException { SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault(); SSLSocket sslSocket = (SSLSocket) sf.createSocket(socket, host, port, autoClose); verifyHostname(sslSocket); return sslSocket; } /** * DescribeverifyHostname
method here.
*
* @param socket a SSLSocket
value
* @exception SSLPeerUnverifiedException If there are problems obtaining
* the server certificates from the SSL session, or the server host name
* does not match with the "Common Name" in the server certificates
* SubjectDN.
* @exception UnknownHostException If we are not able to resolve
* the SSL sessions returned server host name.
*/
private void verifyHostname(SSLSocket socket)
throws SSLPeerUnverifiedException, UnknownHostException {
if (! verifyHostname)
return;
SSLSession session = socket.getSession();
String hostname = session.getPeerHost();
try {
InetAddress addr = InetAddress.getByName(hostname);
} catch (UnknownHostException uhe) {
throw new UnknownHostException("Could not resolve SSL sessions "
+ "server hostname: " + hostname);
}
X509Certificate[] certs = session.getPeerCertificateChain();
if (certs == null || certs.length == 0)
throw new SSLPeerUnverifiedException("No server certificates found!");
//get the servers DN in its string representation
String dn = certs[0].getSubjectDN().getName();
//might be useful to print out all certificates we receive from the
//server, in case one has to debug a problem with the installed certs.
if (LOG.isDebugEnabled()) {
LOG.debug("Server certificate chain:");
for (int i = 0; i < certs.length; i++) {
LOG.debug("X509Certificate[" + i + "]=" + certs[i]);
}
}
//get the common name from the first cert
String cn = getCN(dn);
if (hostname.equalsIgnoreCase(cn)) {
if (LOG.isDebugEnabled()) {
LOG.debug("Target hostname valid: " + cn);
}
} else {
throw new SSLPeerUnverifiedException(
"HTTPS hostname invalid: expected '" + hostname + "', received '" + cn + "'");
}
}
/**
* Parses a X.500 distinguished name for the value of the
* "Common Name" field.
* This is done a bit sloppy right now and should probably be done a bit
* more according to RFC 2253
.
*
* @param dn a X.500 distinguished name.
* @return the value of the "Common Name" field.
*/
private String getCN(String dn) {
int i = 0;
i = dn.indexOf("CN=");
if (i == -1) {
return null;
}
//get the remaining DN without CN=
dn = dn.substring(i + 3);
// System.out.println("dn=" + dn);
char[] dncs = dn.toCharArray();
for (i = 0; i < dncs.length; i++) {
if (dncs[i] == ',' && i > 0 && dncs[i - 1] != '\\') {
break;
}
}
return dn.substring(0, i);
}
public boolean equals(Object obj) {
if ((obj != null) && obj.getClass().equals(StrictSSLProtocolSocketFactory.class)) {
return ((StrictSSLProtocolSocketFactory) obj).getHostnameVerification()
== this.verifyHostname;
} else {
return false;
}
}
public int hashCode() {
return StrictSSLProtocolSocketFactory.class.hashCode();
}
}