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