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