View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.hc.client5.http.ssl;
29  
30  import java.io.IOException;
31  import java.io.InputStream;
32  import java.net.InetSocketAddress;
33  import java.net.Socket;
34  import java.security.AccessController;
35  import java.security.PrivilegedActionException;
36  import java.security.PrivilegedExceptionAction;
37  import java.util.Arrays;
38  import java.util.Collections;
39  import java.util.List;
40  import java.util.regex.Pattern;
41  
42  import javax.net.SocketFactory;
43  import javax.net.ssl.HostnameVerifier;
44  import javax.net.ssl.SSLContext;
45  import javax.net.ssl.SSLException;
46  import javax.net.ssl.SSLHandshakeException;
47  import javax.net.ssl.SSLSession;
48  import javax.net.ssl.SSLSocket;
49  
50  import org.apache.hc.client5.http.config.TlsConfig;
51  import org.apache.hc.client5.http.socket.LayeredConnectionSocketFactory;
52  import org.apache.hc.core5.annotation.Contract;
53  import org.apache.hc.core5.annotation.ThreadingBehavior;
54  import org.apache.hc.core5.http.HttpHost;
55  import org.apache.hc.core5.http.protocol.HttpContext;
56  import org.apache.hc.core5.http.ssl.TLS;
57  import org.apache.hc.core5.http.ssl.TlsCiphers;
58  import org.apache.hc.core5.io.Closer;
59  import org.apache.hc.core5.ssl.SSLContexts;
60  import org.apache.hc.core5.ssl.SSLInitializationException;
61  import org.apache.hc.core5.util.Args;
62  import org.apache.hc.core5.util.Asserts;
63  import org.apache.hc.core5.util.TimeValue;
64  import org.apache.hc.core5.util.Timeout;
65  import org.slf4j.Logger;
66  import org.slf4j.LoggerFactory;
67  
68  /**
69   * Layered socket factory for TLS/SSL connections.
70   * <p>
71   * SSLSocketFactory can be used to validate the identity of the HTTPS server against a list of
72   * trusted certificates and to authenticate to the HTTPS server using a private key.
73   *
74   * @since 4.3
75   */
76  @Contract(threading = ThreadingBehavior.STATELESS)
77  public class SSLConnectionSocketFactory implements LayeredConnectionSocketFactory {
78  
79      private static final String WEAK_KEY_EXCHANGES
80              = "^(TLS|SSL)_(NULL|ECDH_anon|DH_anon|DH_anon_EXPORT|DHE_RSA_EXPORT|DHE_DSS_EXPORT|"
81              + "DSS_EXPORT|DH_DSS_EXPORT|DH_RSA_EXPORT|RSA_EXPORT|KRB5_EXPORT)_(.*)";
82      private static final String WEAK_CIPHERS
83              = "^(TLS|SSL)_(.*)_WITH_(NULL|DES_CBC|DES40_CBC|DES_CBC_40|3DES_EDE_CBC|RC4_128|RC4_40|RC2_CBC_40)_(.*)";
84      private static final List<Pattern> WEAK_CIPHER_SUITE_PATTERNS = Collections.unmodifiableList(Arrays.asList(
85              Pattern.compile(WEAK_KEY_EXCHANGES, Pattern.CASE_INSENSITIVE),
86              Pattern.compile(WEAK_CIPHERS, Pattern.CASE_INSENSITIVE)));
87  
88      private static final Logger LOG = LoggerFactory.getLogger(SSLConnectionSocketFactory.class);
89  
90      /**
91       * Obtains default SSL socket factory with an SSL context based on the standard JSSE
92       * trust material ({@code cacerts} file in the security properties directory).
93       * System properties are not taken into consideration.
94       *
95       * @return default SSL socket factory
96       */
97      public static SSLConnectionSocketFactory getSocketFactory() throws SSLInitializationException {
98          return new SSLConnectionSocketFactory(SSLContexts.createDefault(), HttpsSupport.getDefaultHostnameVerifier());
99      }
100 
101     /**
102      * Obtains default SSL socket factory with an SSL context based on system properties
103      * as described in
104      * <a href="http://docs.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html">
105      * Java&#x2122; Secure Socket Extension (JSSE) Reference Guide</a>.
106      *
107      * @return default system SSL socket factory
108      */
109     public static SSLConnectionSocketFactory getSystemSocketFactory() throws SSLInitializationException {
110         return new SSLConnectionSocketFactory(
111                 (javax.net.ssl.SSLSocketFactory) javax.net.ssl.SSLSocketFactory.getDefault(),
112                 HttpsSupport.getSystemProtocols(),
113                 HttpsSupport.getSystemCipherSuits(),
114                 HttpsSupport.getDefaultHostnameVerifier());
115     }
116 
117     static boolean isWeakCipherSuite(final String cipherSuite) {
118         for (final Pattern pattern : WEAK_CIPHER_SUITE_PATTERNS) {
119             if (pattern.matcher(cipherSuite).matches()) {
120                 return true;
121             }
122         }
123         return false;
124     }
125 
126     private final javax.net.ssl.SSLSocketFactory socketFactory;
127     private final HostnameVerifier hostnameVerifier;
128     private final String[] supportedProtocols;
129     private final String[] supportedCipherSuites;
130     private final TlsSessionValidator tlsSessionValidator;
131 
132     public SSLConnectionSocketFactory(final SSLContext sslContext) {
133         this(sslContext, HttpsSupport.getDefaultHostnameVerifier());
134     }
135 
136     /**
137      * @since 4.4
138      */
139     public SSLConnectionSocketFactory(
140             final SSLContext sslContext, final HostnameVerifier hostnameVerifier) {
141         this(Args.notNull(sslContext, "SSL context").getSocketFactory(),
142                 null, null, hostnameVerifier);
143     }
144 
145     /**
146      * @since 4.4
147      */
148     public SSLConnectionSocketFactory(
149             final SSLContext sslContext,
150             final String[] supportedProtocols,
151             final String[] supportedCipherSuites,
152             final HostnameVerifier hostnameVerifier) {
153         this(Args.notNull(sslContext, "SSL context").getSocketFactory(),
154                 supportedProtocols, supportedCipherSuites, hostnameVerifier);
155     }
156 
157     /**
158      * @since 4.4
159      */
160     public SSLConnectionSocketFactory(
161             final javax.net.ssl.SSLSocketFactory socketFactory,
162             final HostnameVerifier hostnameVerifier) {
163         this(socketFactory, null, null, hostnameVerifier);
164     }
165 
166     /**
167      * @since 4.4
168      */
169     public SSLConnectionSocketFactory(
170             final javax.net.ssl.SSLSocketFactory socketFactory,
171             final String[] supportedProtocols,
172             final String[] supportedCipherSuites,
173             final HostnameVerifier hostnameVerifier) {
174         this.socketFactory = Args.notNull(socketFactory, "SSL socket factory");
175         this.supportedProtocols = supportedProtocols;
176         this.supportedCipherSuites = supportedCipherSuites;
177         this.hostnameVerifier = hostnameVerifier != null ? hostnameVerifier : HttpsSupport.getDefaultHostnameVerifier();
178         this.tlsSessionValidator = new TlsSessionValidator(LOG);
179     }
180 
181     /**
182      * @deprecated Use {@link #prepareSocket(SSLSocket, HttpContext)}
183      */
184     @Deprecated
185     protected void prepareSocket(final SSLSocket socket) throws IOException {
186     }
187 
188     /**
189      * Performs any custom initialization for a newly created SSLSocket
190      * (before the SSL handshake happens).
191      *
192      * The default implementation is a no-op, but could be overridden to, e.g.,
193      * call {@link javax.net.ssl.SSLSocket#setEnabledCipherSuites(String[])}.
194      * @throws IOException may be thrown if overridden
195      */
196     @SuppressWarnings("deprecation")
197     protected void prepareSocket(final SSLSocket socket, final HttpContext context) throws IOException {
198         prepareSocket(socket);
199     }
200 
201     @Override
202     public Socket createSocket(final HttpContext context) throws IOException {
203         return SocketFactory.getDefault().createSocket();
204     }
205 
206     @Override
207     public Socket connectSocket(
208             final TimeValue connectTimeout,
209             final Socket socket,
210             final HttpHost host,
211             final InetSocketAddress remoteAddress,
212             final InetSocketAddress localAddress,
213             final HttpContext context) throws IOException {
214         final Timeout timeout = connectTimeout != null ? Timeout.of(connectTimeout.getDuration(), connectTimeout.getTimeUnit()) : null;
215         return connectSocket(socket, host, remoteAddress, localAddress, timeout, timeout, context);
216     }
217 
218     @Override
219     public Socket connectSocket(
220             final Socket socket,
221             final HttpHost host,
222             final InetSocketAddress remoteAddress,
223             final InetSocketAddress localAddress,
224             final Timeout connectTimeout,
225             final Object attachment,
226             final HttpContext context) throws IOException {
227         Args.notNull(host, "HTTP host");
228         Args.notNull(remoteAddress, "Remote address");
229         final Socket sock = socket != null ? socket : createSocket(context);
230         if (localAddress != null) {
231             sock.bind(localAddress);
232         }
233         try {
234             if (LOG.isDebugEnabled()) {
235                 LOG.debug("Connecting socket to {} with timeout {}", remoteAddress, connectTimeout);
236             }
237             // Run this under a doPrivileged to support lib users that run under a SecurityManager this allows granting connect permissions
238             // only to this library
239             try {
240                 AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
241                     sock.connect(remoteAddress, Timeout.defaultsToDisabled(connectTimeout).toMillisecondsIntBound());
242                     return null;
243                 });
244             } catch (final PrivilegedActionException e) {
245                 Asserts.check(e.getCause() instanceof  IOException,
246                         "method contract violation only checked exceptions are wrapped: " + e.getCause());
247                 // only checked exceptions are wrapped - error and RTExceptions are rethrown by doPrivileged
248                 throw (IOException) e.getCause();
249             }
250         } catch (final IOException ex) {
251             Closer.closeQuietly(sock);
252             throw ex;
253         }
254         // Setup SSL layering if necessary
255         if (sock instanceof SSLSocket) {
256             final SSLSocket sslsock = (SSLSocket) sock;
257             executeHandshake(sslsock, host.getHostName(), attachment, context);
258             return sock;
259         }
260         return createLayeredSocket(sock, host.getHostName(), remoteAddress.getPort(), attachment, context);
261     }
262 
263     @Override
264     public Socket createLayeredSocket(
265             final Socket socket,
266             final String target,
267             final int port,
268             final HttpContext context) throws IOException {
269         return createLayeredSocket(socket, target, port, null, context);
270     }
271 
272     @Override
273     public Socket createLayeredSocket(
274             final Socket socket,
275             final String target,
276             final int port,
277             final Object attachment,
278             final HttpContext context) throws IOException {
279         final SSLSocket sslsock = (SSLSocket) this.socketFactory.createSocket(
280                 socket,
281                 target,
282                 port,
283                 true);
284         executeHandshake(sslsock, target, attachment, context);
285         return sslsock;
286     }
287 
288     private void executeHandshake(
289             final SSLSocket sslsock,
290             final String target,
291             final Object attachment,
292             final HttpContext context) throws IOException {
293         final TlsConfig tlsConfig = attachment instanceof TlsConfig ? (TlsConfig) attachment : TlsConfig.DEFAULT;
294         if (supportedProtocols != null) {
295             sslsock.setEnabledProtocols(supportedProtocols);
296         } else {
297             sslsock.setEnabledProtocols((TLS.excludeWeak(sslsock.getEnabledProtocols())));
298         }
299         if (supportedCipherSuites != null) {
300             sslsock.setEnabledCipherSuites(supportedCipherSuites);
301         } else {
302             sslsock.setEnabledCipherSuites(TlsCiphers.excludeWeak(sslsock.getEnabledCipherSuites()));
303         }
304         final Timeout handshakeTimeout = tlsConfig.getHandshakeTimeout();
305         if (handshakeTimeout != null) {
306             sslsock.setSoTimeout(handshakeTimeout.toMillisecondsIntBound());
307         }
308 
309         prepareSocket(sslsock, context);
310 
311         if (LOG.isDebugEnabled()) {
312             LOG.debug("Enabled protocols: {}", (Object) sslsock.getEnabledProtocols());
313             LOG.debug("Enabled cipher suites: {}", (Object) sslsock.getEnabledCipherSuites());
314             LOG.debug("Starting handshake ({})", handshakeTimeout);
315         }
316         sslsock.startHandshake();
317         verifyHostname(sslsock, target);
318     }
319 
320     private void verifyHostname(final SSLSocket sslsock, final String hostname) throws IOException {
321         try {
322             SSLSession session = sslsock.getSession();
323             if (session == null) {
324                 // In our experience this only happens under IBM 1.4.x when
325                 // spurious (unrelated) certificates show up in the server'
326                 // chain.  Hopefully this will unearth the real problem:
327                 final InputStream in = sslsock.getInputStream();
328                 in.available();
329                 // If ssl.getInputStream().available() didn't cause an
330                 // exception, maybe at least now the session is available?
331                 session = sslsock.getSession();
332                 if (session == null) {
333                     // If it's still null, probably a startHandshake() will
334                     // unearth the real problem.
335                     sslsock.startHandshake();
336                     session = sslsock.getSession();
337                 }
338             }
339             if (session == null) {
340                 throw new SSLHandshakeException("SSL session not available");
341             }
342             verifySession(hostname, session);
343         } catch (final IOException iox) {
344             // close the socket before re-throwing the exception
345             Closer.closeQuietly(sslsock);
346             throw iox;
347         }
348     }
349 
350     protected void verifySession(
351             final String hostname,
352             final SSLSession sslSession) throws SSLException {
353         tlsSessionValidator.verifySession(hostname, sslSession, hostnameVerifier);
354     }
355 
356 }