1. /*
  2. * $Header: /home/cvs/jakarta-commons/httpclient/src/contrib/org/apache/commons/httpclient/contrib/ssl/StrictSSLProtocolSocketFactory.java,v 1.5 2004/06/10 18:25:24 olegk Exp $
  3. * $Revision: 1.5 $
  4. * $Date: 2004/06/10 18:25:24 $
  5. *
  6. * ====================================================================
  7. *
  8. * Copyright 1999-2004 The Apache Software Foundation
  9. *
  10. * Licensed under the Apache License, Version 2.0 (the "License");
  11. * you may not use this file except in compliance with the License.
  12. * You may obtain a copy of the License at
  13. *
  14. * http://www.apache.org/licenses/LICENSE-2.0
  15. *
  16. * Unless required by applicable law or agreed to in writing, software
  17. * distributed under the License is distributed on an "AS IS" BASIS,
  18. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  19. * See the License for the specific language governing permissions and
  20. * limitations under the License.
  21. * ====================================================================
  22. *
  23. * This software consists of voluntary contributions made by many
  24. * individuals on behalf of the Apache Software Foundation. For more
  25. * information on the Apache Software Foundation, please see
  26. * <http://www.apache.org/>.
  27. *
  28. * [Additional notices, if required by prior licensing conditions]
  29. *
  30. * Alternatively, the contents of this file may be used under the
  31. * terms of the GNU Lesser General Public License Version 2 or later
  32. * (the "LGPL"), in which case the provisions of the LGPL are
  33. * applicable instead of those above. See terms of LGPL at
  34. * <http://www.gnu.org/copyleft/lesser.txt>.
  35. * If you wish to allow use of your version of this file only under
  36. * the terms of the LGPL and not to allow others to use your version
  37. * of this file under the Apache Software License, indicate your
  38. * decision by deleting the provisions above and replace them with
  39. * the notice and other provisions required by the LGPL. If you do
  40. * not delete the provisions above, a recipient may use your version
  41. * of this file under either the Apache Software License or the LGPL.
  42. */
  43. package org.apache.commons.httpclient.contrib.ssl;
  44. import java.io.IOException;
  45. import java.net.InetAddress;
  46. import java.net.Socket;
  47. import java.net.UnknownHostException;
  48. import javax.net.ssl.SSLPeerUnverifiedException;
  49. import javax.net.ssl.SSLSession;
  50. import javax.net.ssl.SSLSocket;
  51. import javax.net.ssl.SSLSocketFactory;
  52. import javax.security.cert.X509Certificate;
  53. import org.apache.commons.httpclient.ConnectTimeoutException;
  54. import org.apache.commons.httpclient.params.HttpConnectionParams;
  55. import org.apache.commons.httpclient.protocol.ControllerThreadSocketFactory;
  56. import org.apache.commons.httpclient.protocol.ReflectionSocketFactory;
  57. import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory;
  58. import org.apache.commons.logging.Log;
  59. import org.apache.commons.logging.LogFactory;
  60. /**
  61. * A <code>SecureProtocolSocketFactory</code> that uses JSSE to create
  62. * SSL sockets. It will also support host name verification to help preventing
  63. * man-in-the-middle attacks. Host name verification is turned <b>on</b> by
  64. * default but one will be able to turn it off, which might be a useful feature
  65. * during development. Host name verification will make sure the SSL sessions
  66. * server host name matches with the the host name returned in the
  67. * server certificates "Common Name" field of the "SubjectDN" entry.
  68. *
  69. * @author <a href="mailto:hauer@psicode.com">Sebastian Hauer</a>
  70. * <p>
  71. * DISCLAIMER: HttpClient developers DO NOT actively support this component.
  72. * The component is provided as a reference material, which may be inappropriate
  73. * for use without additional customization.
  74. * </p>
  75. */
  76. public class StrictSSLProtocolSocketFactory
  77. implements SecureProtocolSocketFactory {
  78. /** Log object for this class. */
  79. private static final Log LOG = LogFactory.getLog(StrictSSLProtocolSocketFactory.class);
  80. /** Host name verify flag. */
  81. private boolean verifyHostname = true;
  82. /**
  83. * Constructor for StrictSSLProtocolSocketFactory.
  84. * @param verifyHostname The host name verification flag. If set to
  85. * <code>true</code> the SSL sessions server host name will be compared
  86. * to the host name returned in the server certificates "Common Name"
  87. * field of the "SubjectDN" entry. If these names do not match a
  88. * Exception is thrown to indicate this. Enabling host name verification
  89. * will help to prevent from man-in-the-middle attacks. If set to
  90. * <code>false</code> host name verification is turned off.
  91. *
  92. * Code sample:
  93. *
  94. * <blockquote>
  95. * Protocol stricthttps = new Protocol(
  96. * "https", new StrictSSLProtocolSocketFactory(true), 443);
  97. *
  98. * HttpClient client = new HttpClient();
  99. * client.getHostConfiguration().setHost("localhost", 443, stricthttps);
  100. * </blockquote>
  101. *
  102. */
  103. public StrictSSLProtocolSocketFactory(boolean verifyHostname) {
  104. super();
  105. this.verifyHostname = verifyHostname;
  106. }
  107. /**
  108. * Constructor for StrictSSLProtocolSocketFactory.
  109. * Host name verification will be enabled by default.
  110. */
  111. public StrictSSLProtocolSocketFactory() {
  112. super();
  113. }
  114. /**
  115. * Set the host name verification flag.
  116. *
  117. * @param verifyHostname The host name verification flag. If set to
  118. * <code>true</code> the SSL sessions server host name will be compared
  119. * to the host name returned in the server certificates "Common Name"
  120. * field of the "SubjectDN" entry. If these names do not match a
  121. * Exception is thrown to indicate this. Enabling host name verification
  122. * will help to prevent from man-in-the-middle attacks. If set to
  123. * <code>false</code> host name verification is turned off.
  124. */
  125. public void setHostnameVerification(boolean verifyHostname) {
  126. this.verifyHostname = verifyHostname;
  127. }
  128. /**
  129. * Gets the status of the host name verification flag.
  130. *
  131. * @return Host name verification flag. Either <code>true</code> if host
  132. * name verification is turned on, or <code>false</code> if host name
  133. * verification is turned off.
  134. */
  135. public boolean getHostnameVerification() {
  136. return verifyHostname;
  137. }
  138. /**
  139. * @see SecureProtocolSocketFactory#createSocket(java.lang.String,int,java.net.InetAddress,int)
  140. */
  141. public Socket createSocket(String host, int port,
  142. InetAddress clientHost, int clientPort)
  143. throws IOException, UnknownHostException {
  144. SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault();
  145. SSLSocket sslSocket = (SSLSocket) sf.createSocket(host, port,
  146. clientHost,
  147. clientPort);
  148. verifyHostname(sslSocket);
  149. return sslSocket;
  150. }
  151. /**
  152. * Attempts to get a new socket connection to the given host within the given time limit.
  153. * <p>
  154. * This method employs several techniques to circumvent the limitations of older JREs that
  155. * do not support connect timeout. When running in JRE 1.4 or above reflection is used to
  156. * call Socket#connect(SocketAddress endpoint, int timeout) method. When executing in older
  157. * JREs a controller thread is executed. The controller thread attempts to create a new socket
  158. * within the given limit of time. If socket constructor does not return until the timeout
  159. * expires, the controller terminates and throws an {@link ConnectTimeoutException}
  160. * </p>
  161. *
  162. * @param host the host name/IP
  163. * @param port the port on the host
  164. * @param clientHost the local host name/IP to bind the socket to
  165. * @param clientPort the port on the local machine
  166. * @param params {@link HttpConnectionParams Http connection parameters}
  167. *
  168. * @return Socket a new socket
  169. *
  170. * @throws IOException if an I/O error occurs while creating the socket
  171. * @throws UnknownHostException if the IP address of the host cannot be
  172. * determined
  173. */
  174. public Socket createSocket(
  175. final String host,
  176. final int port,
  177. final InetAddress localAddress,
  178. final int localPort,
  179. final HttpConnectionParams params
  180. ) throws IOException, UnknownHostException, ConnectTimeoutException {
  181. if (params == null) {
  182. throw new IllegalArgumentException("Parameters may not be null");
  183. }
  184. int timeout = params.getConnectionTimeout();
  185. if (timeout == 0) {
  186. return createSocket(host, port, localAddress, localPort);
  187. } else {
  188. // To be eventually deprecated when migrated to Java 1.4 or above
  189. SSLSocket sslSocket = (SSLSocket) ReflectionSocketFactory.createSocket(
  190. "javax.net.ssl.SSLSocketFactory", host, port, localAddress, localPort, timeout);
  191. if (sslSocket == null) {
  192. sslSocket = (SSLSocket) ControllerThreadSocketFactory.createSocket(
  193. this, host, port, localAddress, localPort, timeout);
  194. }
  195. verifyHostname(sslSocket);
  196. return sslSocket;
  197. }
  198. }
  199. /**
  200. * @see SecureProtocolSocketFactory#createSocket(java.lang.String,int)
  201. */
  202. public Socket createSocket(String host, int port)
  203. throws IOException, UnknownHostException {
  204. SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault();
  205. SSLSocket sslSocket = (SSLSocket) sf.createSocket(host, port);
  206. verifyHostname(sslSocket);
  207. return sslSocket;
  208. }
  209. /**
  210. * @see SecureProtocolSocketFactory#createSocket(java.net.Socket,java.lang.String,int,boolean)
  211. */
  212. public Socket createSocket(Socket socket, String host, int port,
  213. boolean autoClose)
  214. throws IOException, UnknownHostException {
  215. SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault();
  216. SSLSocket sslSocket = (SSLSocket) sf.createSocket(socket, host,
  217. port, autoClose);
  218. verifyHostname(sslSocket);
  219. return sslSocket;
  220. }
  221. /**
  222. * Describe <code>verifyHostname</code> method here.
  223. *
  224. * @param socket a <code>SSLSocket</code> value
  225. * @exception SSLPeerUnverifiedException If there are problems obtaining
  226. * the server certificates from the SSL session, or the server host name
  227. * does not match with the "Common Name" in the server certificates
  228. * SubjectDN.
  229. * @exception UnknownHostException If we are not able to resolve
  230. * the SSL sessions returned server host name.
  231. */
  232. private void verifyHostname(SSLSocket socket)
  233. throws SSLPeerUnverifiedException, UnknownHostException {
  234. if (! verifyHostname)
  235. return;
  236. SSLSession session = socket.getSession();
  237. String hostname = session.getPeerHost();
  238. try {
  239. InetAddress addr = InetAddress.getByName(hostname);
  240. } catch (UnknownHostException uhe) {
  241. throw new UnknownHostException("Could not resolve SSL sessions "
  242. + "server hostname: " + hostname);
  243. }
  244. X509Certificate[] certs = session.getPeerCertificateChain();
  245. if (certs == null || certs.length == 0)
  246. throw new SSLPeerUnverifiedException("No server certificates found!");
  247. //get the servers DN in its string representation
  248. String dn = certs[0].getSubjectDN().getName();
  249. //might be useful to print out all certificates we receive from the
  250. //server, in case one has to debug a problem with the installed certs.
  251. if (LOG.isDebugEnabled()) {
  252. LOG.debug("Server certificate chain:");
  253. for (int i = 0; i < certs.length; i++) {
  254. LOG.debug("X509Certificate[" + i + "]=" + certs[i]);
  255. }
  256. }
  257. //get the common name from the first cert
  258. String cn = getCN(dn);
  259. if (hostname.equalsIgnoreCase(cn)) {
  260. if (LOG.isDebugEnabled()) {
  261. LOG.debug("Target hostname valid: " + cn);
  262. }
  263. } else {
  264. throw new SSLPeerUnverifiedException(
  265. "HTTPS hostname invalid: expected '" + hostname + "', received '" + cn + "'");
  266. }
  267. }
  268. /**
  269. * Parses a X.500 distinguished name for the value of the
  270. * "Common Name" field.
  271. * This is done a bit sloppy right now and should probably be done a bit
  272. * more according to <code>RFC 2253</code>.
  273. *
  274. * @param dn a X.500 distinguished name.
  275. * @return the value of the "Common Name" field.
  276. */
  277. private String getCN(String dn) {
  278. int i = 0;
  279. i = dn.indexOf("CN=");
  280. if (i == -1) {
  281. return null;
  282. }
  283. //get the remaining DN without CN=
  284. dn = dn.substring(i + 3);
  285. // System.out.println("dn=" + dn);
  286. char[] dncs = dn.toCharArray();
  287. for (i = 0; i < dncs.length; i++) {
  288. if (dncs[i] == ',' && i > 0 && dncs[i - 1] != '\\') {
  289. break;
  290. }
  291. }
  292. return dn.substring(0, i);
  293. }
  294. public boolean equals(Object obj) {
  295. if ((obj != null) && obj.getClass().equals(StrictSSLProtocolSocketFactory.class)) {
  296. return ((StrictSSLProtocolSocketFactory) obj).getHostnameVerification()
  297. == this.verifyHostname;
  298. } else {
  299. return false;
  300. }
  301. }
  302. public int hashCode() {
  303. return StrictSSLProtocolSocketFactory.class.hashCode();
  304. }
  305. }