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.security.AccessController;
36  import java.security.PrivilegedActionException;
37  import java.security.PrivilegedExceptionAction;
38  import java.util.Arrays;
39  import java.util.Collections;
40  import java.util.List;
41  import java.util.regex.Pattern;
42  
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 new Socket();
204     }
205 
206     @Override
207     public Socket createSocket(final Proxy proxy, final HttpContext context) throws IOException {
208         return proxy != null ? new Socket(proxy) : new Socket();
209     }
210 
211     @Override
212     public Socket connectSocket(
213             final TimeValue connectTimeout,
214             final Socket socket,
215             final HttpHost host,
216             final InetSocketAddress remoteAddress,
217             final InetSocketAddress localAddress,
218             final HttpContext context) throws IOException {
219         final Timeout timeout = connectTimeout != null ? Timeout.of(connectTimeout.getDuration(), connectTimeout.getTimeUnit()) : null;
220         return connectSocket(socket, host, remoteAddress, localAddress, timeout, timeout, context);
221     }
222 
223     @Override
224     public Socket connectSocket(
225             final Socket socket,
226             final HttpHost host,
227             final InetSocketAddress remoteAddress,
228             final InetSocketAddress localAddress,
229             final Timeout connectTimeout,
230             final Object attachment,
231             final HttpContext context) throws IOException {
232         Args.notNull(host, "HTTP host");
233         Args.notNull(remoteAddress, "Remote address");
234         final Socket sock = socket != null ? socket : createSocket(context);
235         if (localAddress != null) {
236             sock.bind(localAddress);
237         }
238         try {
239             if (LOG.isDebugEnabled()) {
240                 LOG.debug("Connecting socket to {} with timeout {}", remoteAddress, connectTimeout);
241             }
242             // Run this under a doPrivileged to support lib users that run under a SecurityManager this allows granting connect permissions
243             // only to this library
244             try {
245                 AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
246                     sock.connect(remoteAddress, Timeout.defaultsToDisabled(connectTimeout).toMillisecondsIntBound());
247                     return null;
248                 });
249             } catch (final PrivilegedActionException e) {
250                 Asserts.check(e.getCause() instanceof  IOException,
251                         "method contract violation only checked exceptions are wrapped: " + e.getCause());
252                 // only checked exceptions are wrapped - error and RTExceptions are rethrown by doPrivileged
253                 throw (IOException) e.getCause();
254             }
255         } catch (final IOException ex) {
256             Closer.closeQuietly(sock);
257             throw ex;
258         }
259         // Setup SSL layering if necessary
260         if (sock instanceof SSLSocket) {
261             final SSLSocket sslsock = (SSLSocket) sock;
262             executeHandshake(sslsock, host.getHostName(), attachment, context);
263             return sock;
264         }
265         return createLayeredSocket(sock, host.getHostName(), remoteAddress.getPort(), attachment, context);
266     }
267 
268     @Override
269     public Socket createLayeredSocket(
270             final Socket socket,
271             final String target,
272             final int port,
273             final HttpContext context) throws IOException {
274         return createLayeredSocket(socket, target, port, null, context);
275     }
276 
277     @Override
278     public Socket createLayeredSocket(
279             final Socket socket,
280             final String target,
281             final int port,
282             final Object attachment,
283             final HttpContext context) throws IOException {
284         final SSLSocket sslsock = (SSLSocket) this.socketFactory.createSocket(
285                 socket,
286                 target,
287                 port,
288                 true);
289         executeHandshake(sslsock, target, attachment, context);
290         return sslsock;
291     }
292 
293     private void executeHandshake(
294             final SSLSocket sslsock,
295             final String target,
296             final Object attachment,
297             final HttpContext context) throws IOException {
298         final TlsConfig tlsConfig = attachment instanceof TlsConfig ? (TlsConfig) attachment : TlsConfig.DEFAULT;
299         if (supportedProtocols != null) {
300             sslsock.setEnabledProtocols(supportedProtocols);
301         } else {
302             sslsock.setEnabledProtocols((TLS.excludeWeak(sslsock.getEnabledProtocols())));
303         }
304         if (supportedCipherSuites != null) {
305             sslsock.setEnabledCipherSuites(supportedCipherSuites);
306         } else {
307             sslsock.setEnabledCipherSuites(TlsCiphers.excludeWeak(sslsock.getEnabledCipherSuites()));
308         }
309         final Timeout handshakeTimeout = tlsConfig.getHandshakeTimeout();
310         if (handshakeTimeout != null) {
311             sslsock.setSoTimeout(handshakeTimeout.toMillisecondsIntBound());
312         }
313 
314         prepareSocket(sslsock, context);
315 
316         if (LOG.isDebugEnabled()) {
317             LOG.debug("Enabled protocols: {}", (Object) sslsock.getEnabledProtocols());
318             LOG.debug("Enabled cipher suites: {}", (Object) sslsock.getEnabledCipherSuites());
319             LOG.debug("Starting handshake ({})", handshakeTimeout);
320         }
321         sslsock.startHandshake();
322         verifyHostname(sslsock, target);
323     }
324 
325     private void verifyHostname(final SSLSocket sslsock, final String hostname) throws IOException {
326         try {
327             SSLSession session = sslsock.getSession();
328             if (session == null) {
329                 // In our experience this only happens under IBM 1.4.x when
330                 // spurious (unrelated) certificates show up in the server'
331                 // chain.  Hopefully this will unearth the real problem:
332                 final InputStream in = sslsock.getInputStream();
333                 in.available();
334                 // If ssl.getInputStream().available() didn't cause an
335                 // exception, maybe at least now the session is available?
336                 session = sslsock.getSession();
337                 if (session == null) {
338                     // If it's still null, probably a startHandshake() will
339                     // unearth the real problem.
340                     sslsock.startHandshake();
341                     session = sslsock.getSession();
342                 }
343             }
344             if (session == null) {
345                 throw new SSLHandshakeException("SSL session not available");
346             }
347             verifySession(hostname, session);
348         } catch (final IOException iox) {
349             // close the socket before re-throwing the exception
350             Closer.closeQuietly(sslsock);
351             throw iox;
352         }
353     }
354 
355     protected void verifySession(
356             final String hostname,
357             final SSLSession sslSession) throws SSLException {
358         tlsSessionValidator.verifySession(hostname, sslSession, hostnameVerifier);
359     }
360 
361 }