1 | /* |
2 | * $Header$ |
3 | * $Revision: 10 $ |
4 | * $Date: 2006-02-16 19:35:22 +0700 (Thu, 16 Feb 2006) $ |
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 | |
44 | package org.apache.commons.httpclient.contrib.ssl; |
45 | |
46 | import java.io.IOException; |
47 | import java.net.InetAddress; |
48 | import java.net.Socket; |
49 | import java.net.UnknownHostException; |
50 | |
51 | import javax.net.ssl.SSLPeerUnverifiedException; |
52 | import javax.net.ssl.SSLSession; |
53 | import javax.net.ssl.SSLSocket; |
54 | import javax.net.ssl.SSLSocketFactory; |
55 | import javax.security.cert.X509Certificate; |
56 | |
57 | import org.apache.commons.httpclient.ConnectTimeoutException; |
58 | import org.apache.commons.httpclient.params.HttpConnectionParams; |
59 | import org.apache.commons.httpclient.protocol.ControllerThreadSocketFactory; |
60 | import org.apache.commons.httpclient.protocol.ReflectionSocketFactory; |
61 | import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory; |
62 | import org.apache.commons.logging.Log; |
63 | import org.apache.commons.logging.LogFactory; |
64 | |
65 | |
66 | /** |
67 | * A <code>SecureProtocolSocketFactory</code> that uses JSSE to create |
68 | * SSL sockets. It will also support host name verification to help preventing |
69 | * man-in-the-middle attacks. Host name verification is turned <b>on</b> by |
70 | * default but one will be able to turn it off, which might be a useful feature |
71 | * during development. Host name verification will make sure the SSL sessions |
72 | * server host name matches with the the host name returned in the |
73 | * server certificates "Common Name" field of the "SubjectDN" entry. |
74 | * |
75 | * @author <a href="mailto:hauer@psicode.com">Sebastian Hauer</a> |
76 | * <p> |
77 | * DISCLAIMER: HttpClient developers DO NOT actively support this component. |
78 | * The component is provided as a reference material, which may be inappropriate |
79 | * for use without additional customization. |
80 | * </p> |
81 | */ |
82 | public class StrictSSLProtocolSocketFactory |
83 | implements SecureProtocolSocketFactory { |
84 | |
85 | /** Log object for this class. */ |
86 | private static final Log LOG = LogFactory.getLog(StrictSSLProtocolSocketFactory.class); |
87 | |
88 | /** Host name verify flag. */ |
89 | private boolean verifyHostname = true; |
90 | |
91 | |
92 | /** |
93 | * Constructor for StrictSSLProtocolSocketFactory. |
94 | * @param verifyHostname The host name verification flag. If set to |
95 | * <code>true</code> the SSL sessions server host name will be compared |
96 | * to the host name returned in the server certificates "Common Name" |
97 | * field of the "SubjectDN" entry. If these names do not match a |
98 | * Exception is thrown to indicate this. Enabling host name verification |
99 | * 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 | /** |
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) |
158 | throws IOException, UnknownHostException { |
159 | SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault(); |
160 | SSLSocket sslSocket = (SSLSocket) sf.createSocket(host, port, |
161 | clientHost, |
162 | 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 localAddress the local host name/IP to bind the socket to |
182 | * @param localPort 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( |
192 | final String host, |
193 | final int port, |
194 | final InetAddress localAddress, |
195 | final int localPort, |
196 | final HttpConnectionParams params |
197 | ) throws IOException, UnknownHostException, ConnectTimeoutException { |
198 | if (params == null) { |
199 | throw new IllegalArgumentException("Parameters may not be null"); |
200 | } |
201 | int timeout = params.getConnectionTimeout(); |
202 | if (timeout == 0) { |
203 | return createSocket(host, port, localAddress, localPort); |
204 | } else { |
205 | // To be eventually deprecated when migrated to Java 1.4 or above |
206 | SSLSocket sslSocket = (SSLSocket) ReflectionSocketFactory.createSocket( |
207 | "javax.net.ssl.SSLSocketFactory", host, port, localAddress, localPort, timeout); |
208 | if (sslSocket == null) { |
209 | sslSocket = (SSLSocket) ControllerThreadSocketFactory.createSocket( |
210 | this, host, port, localAddress, localPort, timeout); |
211 | } |
212 | verifyHostname(sslSocket); |
213 | return sslSocket; |
214 | } |
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.getDefault(); |
223 | SSLSocket sslSocket = (SSLSocket) sf.createSocket(host, port); |
224 | verifyHostname(sslSocket); |
225 | |
226 | return sslSocket; |
227 | } |
228 | |
229 | /** |
230 | * @see SecureProtocolSocketFactory#createSocket(java.net.Socket,java.lang.String,int,boolean) |
231 | */ |
232 | public Socket createSocket(Socket socket, String host, int port, |
233 | boolean autoClose) |
234 | throws IOException, UnknownHostException { |
235 | SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault(); |
236 | SSLSocket sslSocket = (SSLSocket) sf.createSocket(socket, host, |
237 | port, autoClose); |
238 | verifyHostname(sslSocket); |
239 | |
240 | return sslSocket; |
241 | } |
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("Could not resolve SSL sessions " |
266 | + "server hostname: " + hostname); |
267 | } |
268 | |
269 | X509Certificate[] certs = session.getPeerCertificateChain(); |
270 | if (certs == null || certs.length == 0) |
271 | throw new SSLPeerUnverifiedException("No server certificates found!"); |
272 | |
273 | //get the servers DN in its string representation |
274 | String dn = certs[0].getSubjectDN().getName(); |
275 | |
276 | //might be useful to print out all certificates we receive from the |
277 | //server, in case one has to debug a problem with the installed certs. |
278 | if (LOG.isDebugEnabled()) { |
279 | LOG.debug("Server certificate chain:"); |
280 | for (int i = 0; i < certs.length; i++) { |
281 | LOG.debug("X509Certificate[" + i + "]=" + certs[i]); |
282 | } |
283 | } |
284 | //get the common name from the first cert |
285 | String cn = getCN(dn); |
286 | if (hostname.equalsIgnoreCase(cn)) { |
287 | if (LOG.isDebugEnabled()) { |
288 | LOG.debug("Target hostname valid: " + cn); |
289 | } |
290 | } else { |
291 | throw new SSLPeerUnverifiedException( |
292 | "HTTPS hostname invalid: expected '" + hostname + "', received '" + cn + "'"); |
293 | } |
294 | } |
295 | |
296 | |
297 | /** |
298 | * Parses a X.500 distinguished name for the value of the |
299 | * "Common Name" field. |
300 | * This is done a bit sloppy right now and should probably be done a bit |
301 | * more according to <code>RFC 2253</code>. |
302 | * |
303 | * @param dn a X.500 distinguished name. |
304 | * @return the value of the "Common Name" field. |
305 | */ |
306 | private String getCN(String dn) { |
307 | int i = 0; |
308 | i = dn.indexOf("CN="); |
309 | if (i == -1) { |
310 | return null; |
311 | } |
312 | //get the remaining DN without CN= |
313 | dn = dn.substring(i + 3); |
314 | // System.out.println("dn=" + dn); |
315 | char[] dncs = dn.toCharArray(); |
316 | for (i = 0; i < dncs.length; i++) { |
317 | if (dncs[i] == ',' && i > 0 && dncs[i - 1] != '\\') { |
318 | break; |
319 | } |
320 | } |
321 | return dn.substring(0, i); |
322 | } |
323 | |
324 | public boolean equals(Object obj) { |
325 | if ((obj != null) && obj.getClass().equals(StrictSSLProtocolSocketFactory.class)) { |
326 | return ((StrictSSLProtocolSocketFactory) obj).getHostnameVerification() |
327 | == this.verifyHostname; |
328 | } else { |
329 | return false; |
330 | } |
331 | } |
332 | |
333 | public int hashCode() { |
334 | return StrictSSLProtocolSocketFactory.class.hashCode(); |
335 | } |
336 | |
337 | } |