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