001: /*
002: * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/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 $
003: * $Revision: 480424 $
004: * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $
005: *
006: * ====================================================================
007: *
008: * Licensed to the Apache Software Foundation (ASF) under one or more
009: * contributor license agreements. See the NOTICE file distributed with
010: * this work for additional information regarding copyright ownership.
011: * The ASF licenses this file to You under the Apache License, Version 2.0
012: * (the "License"); you may not use this file except in compliance with
013: * the License. You may obtain a copy of the License at
014: *
015: * http://www.apache.org/licenses/LICENSE-2.0
016: *
017: * Unless required by applicable law or agreed to in writing, software
018: * distributed under the License is distributed on an "AS IS" BASIS,
019: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
020: * See the License for the specific language governing permissions and
021: * limitations under the License.
022: * ====================================================================
023: *
024: * This software consists of voluntary contributions made by many
025: * individuals on behalf of the Apache Software Foundation. For more
026: * information on the Apache Software Foundation, please see
027: * <http://www.apache.org/>.
028: *
029: * [Additional notices, if required by prior licensing conditions]
030: *
031: * Alternatively, the contents of this file may be used under the
032: * terms of the GNU Lesser General Public License Version 2 or later
033: * (the "LGPL"), in which case the provisions of the LGPL are
034: * applicable instead of those above. See terms of LGPL at
035: * <http://www.gnu.org/copyleft/lesser.txt>.
036: * If you wish to allow use of your version of this file only under
037: * the terms of the LGPL and not to allow others to use your version
038: * of this file under the Apache Software License, indicate your
039: * decision by deleting the provisions above and replace them with
040: * the notice and other provisions required by the LGPL. If you do
041: * not delete the provisions above, a recipient may use your version
042: * of this file under either the Apache Software License or the LGPL.
043: */
044:
045: package org.apache.commons.httpclient.contrib.ssl;
046:
047: import java.io.IOException;
048: import java.net.InetAddress;
049: import java.net.InetSocketAddress;
050: import java.net.Socket;
051: import java.net.SocketAddress;
052: import java.net.UnknownHostException;
053:
054: import javax.net.SocketFactory;
055: import javax.net.ssl.SSLPeerUnverifiedException;
056: import javax.net.ssl.SSLSession;
057: import javax.net.ssl.SSLSocket;
058: import javax.net.ssl.SSLSocketFactory;
059: import javax.security.cert.X509Certificate;
060:
061: import org.apache.commons.httpclient.ConnectTimeoutException;
062: import org.apache.commons.httpclient.params.HttpConnectionParams;
063: import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory;
064: import org.apache.commons.logging.Log;
065: import org.apache.commons.logging.LogFactory;
066:
067: /**
068: * A <code>SecureProtocolSocketFactory</code> that uses JSSE to create
069: * SSL sockets. It will also support host name verification to help preventing
070: * man-in-the-middle attacks. Host name verification is turned <b>on</b> by
071: * default but one will be able to turn it off, which might be a useful feature
072: * during development. Host name verification will make sure the SSL sessions
073: * server host name matches with the the host name returned in the
074: * server certificates "Common Name" field of the "SubjectDN" entry.
075: *
076: * @author <a href="mailto:hauer@psicode.com">Sebastian Hauer</a>
077: * <p>
078: * DISCLAIMER: HttpClient developers DO NOT actively support this component.
079: * The component is provided as a reference material, which may be inappropriate
080: * for use without additional customization.
081: * </p>
082: */
083: public class StrictSSLProtocolSocketFactory implements
084: SecureProtocolSocketFactory {
085:
086: /** Log object for this class. */
087: private static final Log LOG = LogFactory
088: .getLog(StrictSSLProtocolSocketFactory.class);
089:
090: /** Host name verify flag. */
091: private boolean verifyHostname = true;
092:
093: /**
094: * Constructor for StrictSSLProtocolSocketFactory.
095: * @param verifyHostname The host name verification flag. If set to
096: * <code>true</code> the SSL sessions server host name will be compared
097: * to the host name returned in the server certificates "Common Name"
098: * field of the "SubjectDN" entry. If these names do not match a
099: * Exception is thrown to indicate this. Enabling host name verification
100: * will help to prevent from man-in-the-middle attacks. If set to
101: * <code>false</code> host name verification is turned off.
102: *
103: * Code sample:
104: *
105: * <blockquote>
106: * Protocol stricthttps = new Protocol(
107: * "https", new StrictSSLProtocolSocketFactory(true), 443);
108: *
109: * HttpClient client = new HttpClient();
110: * client.getHostConfiguration().setHost("localhost", 443, stricthttps);
111: * </blockquote>
112: *
113: */
114: public StrictSSLProtocolSocketFactory(boolean verifyHostname) {
115: super ();
116: this .verifyHostname = verifyHostname;
117: }
118:
119: /**
120: * Constructor for StrictSSLProtocolSocketFactory.
121: * Host name verification will be enabled by default.
122: */
123: public StrictSSLProtocolSocketFactory() {
124: super ();
125: }
126:
127: /**
128: * Set the host name verification flag.
129: *
130: * @param verifyHostname The host name verification flag. If set to
131: * <code>true</code> the SSL sessions server host name will be compared
132: * to the host name returned in the server certificates "Common Name"
133: * field of the "SubjectDN" entry. If these names do not match a
134: * Exception is thrown to indicate this. Enabling host name verification
135: * will help to prevent from man-in-the-middle attacks. If set to
136: * <code>false</code> host name verification is turned off.
137: */
138: public void setHostnameVerification(boolean verifyHostname) {
139: this .verifyHostname = verifyHostname;
140: }
141:
142: /**
143: * Gets the status of the host name verification flag.
144: *
145: * @return Host name verification flag. Either <code>true</code> if host
146: * name verification is turned on, or <code>false</code> if host name
147: * verification is turned off.
148: */
149: public boolean getHostnameVerification() {
150: return verifyHostname;
151: }
152:
153: /**
154: * @see SecureProtocolSocketFactory#createSocket(java.lang.String,int,java.net.InetAddress,int)
155: */
156: public Socket createSocket(String host, int port,
157: InetAddress clientHost, int clientPort) throws IOException,
158: UnknownHostException {
159: SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory
160: .getDefault();
161: SSLSocket sslSocket = (SSLSocket) sf.createSocket(host, port,
162: clientHost, clientPort);
163: verifyHostname(sslSocket);
164:
165: return sslSocket;
166: }
167:
168: /**
169: * Attempts to get a new socket connection to the given host within the given time limit.
170: * <p>
171: * This method employs several techniques to circumvent the limitations of older JREs that
172: * do not support connect timeout. When running in JRE 1.4 or above reflection is used to
173: * call Socket#connect(SocketAddress endpoint, int timeout) method. When executing in older
174: * JREs a controller thread is executed. The controller thread attempts to create a new socket
175: * within the given limit of time. If socket constructor does not return until the timeout
176: * expires, the controller terminates and throws an {@link ConnectTimeoutException}
177: * </p>
178: *
179: * @param host the host name/IP
180: * @param port the port on the host
181: * @param clientHost the local host name/IP to bind the socket to
182: * @param clientPort the port on the local machine
183: * @param params {@link HttpConnectionParams Http connection parameters}
184: *
185: * @return Socket a new socket
186: *
187: * @throws IOException if an I/O error occurs while creating the socket
188: * @throws UnknownHostException if the IP address of the host cannot be
189: * determined
190: */
191: public Socket createSocket(final String host, final int port,
192: final InetAddress localAddress, final int localPort,
193: final HttpConnectionParams params) throws IOException,
194: UnknownHostException, ConnectTimeoutException {
195: if (params == null) {
196: throw new IllegalArgumentException(
197: "Parameters may not be null");
198: }
199: int timeout = params.getConnectionTimeout();
200: Socket socket = null;
201:
202: SocketFactory socketfactory = SSLSocketFactory.getDefault();
203: if (timeout == 0) {
204: socket = socketfactory.createSocket(host, port,
205: localAddress, localPort);
206: } else {
207: socket = socketfactory.createSocket();
208: SocketAddress localaddr = new InetSocketAddress(
209: localAddress, localPort);
210: SocketAddress remoteaddr = new InetSocketAddress(host, port);
211: socket.bind(localaddr);
212: socket.connect(remoteaddr, timeout);
213: }
214: verifyHostname((SSLSocket) socket);
215: return socket;
216: }
217:
218: /**
219: * @see SecureProtocolSocketFactory#createSocket(java.lang.String,int)
220: */
221: public Socket createSocket(String host, int port)
222: throws IOException, UnknownHostException {
223: SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory
224: .getDefault();
225: SSLSocket sslSocket = (SSLSocket) sf.createSocket(host, port);
226: verifyHostname(sslSocket);
227:
228: return sslSocket;
229: }
230:
231: /**
232: * @see SecureProtocolSocketFactory#createSocket(java.net.Socket,java.lang.String,int,boolean)
233: */
234: public Socket createSocket(Socket socket, String host, int port,
235: boolean autoClose) throws IOException, UnknownHostException {
236: SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory
237: .getDefault();
238: SSLSocket sslSocket = (SSLSocket) sf.createSocket(socket, host,
239: port, autoClose);
240: verifyHostname(sslSocket);
241:
242: return sslSocket;
243: }
244:
245: /**
246: * Describe <code>verifyHostname</code> method here.
247: *
248: * @param socket a <code>SSLSocket</code> value
249: * @exception SSLPeerUnverifiedException If there are problems obtaining
250: * the server certificates from the SSL session, or the server host name
251: * does not match with the "Common Name" in the server certificates
252: * SubjectDN.
253: * @exception UnknownHostException If we are not able to resolve
254: * the SSL sessions returned server host name.
255: */
256: private void verifyHostname(SSLSocket socket)
257: throws SSLPeerUnverifiedException, UnknownHostException {
258: if (!verifyHostname)
259: return;
260:
261: SSLSession session = socket.getSession();
262: String hostname = session.getPeerHost();
263: try {
264: InetAddress addr = InetAddress.getByName(hostname);
265: } catch (UnknownHostException uhe) {
266: throw new UnknownHostException(
267: "Could not resolve SSL sessions "
268: + "server hostname: " + hostname);
269: }
270:
271: X509Certificate[] certs = session.getPeerCertificateChain();
272: if (certs == null || certs.length == 0)
273: throw new SSLPeerUnverifiedException(
274: "No server certificates found!");
275:
276: //get the servers DN in its string representation
277: String dn = certs[0].getSubjectDN().getName();
278:
279: //might be useful to print out all certificates we receive from the
280: //server, in case one has to debug a problem with the installed certs.
281: if (LOG.isDebugEnabled()) {
282: LOG.debug("Server certificate chain:");
283: for (int i = 0; i < certs.length; i++) {
284: LOG.debug("X509Certificate[" + i + "]=" + certs[i]);
285: }
286: }
287: //get the common name from the first cert
288: String cn = getCN(dn);
289: if (hostname.equalsIgnoreCase(cn)) {
290: if (LOG.isDebugEnabled()) {
291: LOG.debug("Target hostname valid: " + cn);
292: }
293: } else {
294: throw new SSLPeerUnverifiedException(
295: "HTTPS hostname invalid: expected '" + hostname
296: + "', received '" + cn + "'");
297: }
298: }
299:
300: /**
301: * Parses a X.500 distinguished name for the value of the
302: * "Common Name" field.
303: * This is done a bit sloppy right now and should probably be done a bit
304: * more according to <code>RFC 2253</code>.
305: *
306: * @param dn a X.500 distinguished name.
307: * @return the value of the "Common Name" field.
308: */
309: private String getCN(String dn) {
310: int i = 0;
311: i = dn.indexOf("CN=");
312: if (i == -1) {
313: return null;
314: }
315: //get the remaining DN without CN=
316: dn = dn.substring(i + 3);
317: // System.out.println("dn=" + dn);
318: char[] dncs = dn.toCharArray();
319: for (i = 0; i < dncs.length; i++) {
320: if (dncs[i] == ',' && i > 0 && dncs[i - 1] != '\\') {
321: break;
322: }
323: }
324: return dn.substring(0, i);
325: }
326:
327: public boolean equals(Object obj) {
328: if ((obj != null)
329: && obj.getClass().equals(
330: StrictSSLProtocolSocketFactory.class)) {
331: return ((StrictSSLProtocolSocketFactory) obj)
332: .getHostnameVerification() == this .verifyHostname;
333: } else {
334: return false;
335: }
336: }
337:
338: public int hashCode() {
339: return StrictSSLProtocolSocketFactory.class.hashCode();
340: }
341:
342: }
|