| /* |
| * ==================================================================== |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * ==================================================================== |
| * |
| * This software consists of voluntary contributions made by many |
| * individuals on behalf of the Apache Software Foundation. For more |
| * information on the Apache Software Foundation, please see |
| * <http://www.apache.org/>. |
| * |
| */ |
| |
| package org.apache.hc.client5.http.ssl; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.InetSocketAddress; |
| import java.net.Socket; |
| import java.security.AccessController; |
| import java.security.PrivilegedActionException; |
| import java.security.PrivilegedExceptionAction; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.regex.Pattern; |
| |
| import javax.net.SocketFactory; |
| import javax.net.ssl.HostnameVerifier; |
| import javax.net.ssl.SSLContext; |
| import javax.net.ssl.SSLException; |
| import javax.net.ssl.SSLHandshakeException; |
| import javax.net.ssl.SSLSession; |
| import javax.net.ssl.SSLSocket; |
| |
| import org.apache.hc.client5.http.socket.LayeredConnectionSocketFactory; |
| import org.apache.hc.core5.annotation.Contract; |
| import org.apache.hc.core5.annotation.ThreadingBehavior; |
| import org.apache.hc.core5.http.HttpHost; |
| import org.apache.hc.core5.http.protocol.HttpContext; |
| import org.apache.hc.core5.http.ssl.TLS; |
| import org.apache.hc.core5.http.ssl.TlsCiphers; |
| import org.apache.hc.core5.io.Closer; |
| import org.apache.hc.core5.ssl.SSLContexts; |
| import org.apache.hc.core5.ssl.SSLInitializationException; |
| import org.apache.hc.core5.util.Args; |
| import org.apache.hc.core5.util.Asserts; |
| import org.apache.hc.core5.util.TimeValue; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Layered socket factory for TLS/SSL connections. |
| * <p> |
| * SSLSocketFactory can be used to validate the identity of the HTTPS server against a list of |
| * trusted certificates and to authenticate to the HTTPS server using a private key. |
| * |
| * @since 4.3 |
| */ |
| @Contract(threading = ThreadingBehavior.STATELESS) |
| public class SSLConnectionSocketFactory implements LayeredConnectionSocketFactory { |
| |
| private static final String WEAK_KEY_EXCHANGES |
| = "^(TLS|SSL)_(NULL|ECDH_anon|DH_anon|DH_anon_EXPORT|DHE_RSA_EXPORT|DHE_DSS_EXPORT|" |
| + "DSS_EXPORT|DH_DSS_EXPORT|DH_RSA_EXPORT|RSA_EXPORT|KRB5_EXPORT)_(.*)"; |
| private static final String WEAK_CIPHERS |
| = "^(TLS|SSL)_(.*)_WITH_(NULL|DES_CBC|DES40_CBC|DES_CBC_40|3DES_EDE_CBC|RC4_128|RC4_40|RC2_CBC_40)_(.*)"; |
| private static final List<Pattern> WEAK_CIPHER_SUITE_PATTERNS = Collections.unmodifiableList(Arrays.asList( |
| Pattern.compile(WEAK_KEY_EXCHANGES, Pattern.CASE_INSENSITIVE), |
| Pattern.compile(WEAK_CIPHERS, Pattern.CASE_INSENSITIVE))); |
| |
| private static final Logger LOG = LoggerFactory.getLogger(SSLConnectionSocketFactory.class); |
| |
| /** |
| * Obtains default SSL socket factory with an SSL context based on the standard JSSE |
| * trust material ({@code cacerts} file in the security properties directory). |
| * System properties are not taken into consideration. |
| * |
| * @return default SSL socket factory |
| */ |
| public static SSLConnectionSocketFactory getSocketFactory() throws SSLInitializationException { |
| return new SSLConnectionSocketFactory(SSLContexts.createDefault(), HttpsSupport.getDefaultHostnameVerifier()); |
| } |
| |
| /** |
| * Obtains default SSL socket factory with an SSL context based on system properties |
| * as described in |
| * <a href="http://docs.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html"> |
| * Java™ Secure Socket Extension (JSSE) Reference Guide</a>. |
| * |
| * @return default system SSL socket factory |
| */ |
| public static SSLConnectionSocketFactory getSystemSocketFactory() throws SSLInitializationException { |
| return new SSLConnectionSocketFactory( |
| (javax.net.ssl.SSLSocketFactory) javax.net.ssl.SSLSocketFactory.getDefault(), |
| HttpsSupport.getSystemProtocols(), |
| HttpsSupport.getSystemCipherSuits(), |
| HttpsSupport.getDefaultHostnameVerifier()); |
| } |
| |
| static boolean isWeakCipherSuite(final String cipherSuite) { |
| for (final Pattern pattern : WEAK_CIPHER_SUITE_PATTERNS) { |
| if (pattern.matcher(cipherSuite).matches()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private final javax.net.ssl.SSLSocketFactory socketFactory; |
| private final HostnameVerifier hostnameVerifier; |
| private final String[] supportedProtocols; |
| private final String[] supportedCipherSuites; |
| private final TlsSessionValidator tlsSessionValidator; |
| |
| public SSLConnectionSocketFactory(final SSLContext sslContext) { |
| this(sslContext, HttpsSupport.getDefaultHostnameVerifier()); |
| } |
| |
| /** |
| * @since 4.4 |
| */ |
| public SSLConnectionSocketFactory( |
| final SSLContext sslContext, final HostnameVerifier hostnameVerifier) { |
| this(Args.notNull(sslContext, "SSL context").getSocketFactory(), |
| null, null, hostnameVerifier); |
| } |
| |
| /** |
| * @since 4.4 |
| */ |
| public SSLConnectionSocketFactory( |
| final SSLContext sslContext, |
| final String[] supportedProtocols, |
| final String[] supportedCipherSuites, |
| final HostnameVerifier hostnameVerifier) { |
| this(Args.notNull(sslContext, "SSL context").getSocketFactory(), |
| supportedProtocols, supportedCipherSuites, hostnameVerifier); |
| } |
| |
| /** |
| * @since 4.4 |
| */ |
| public SSLConnectionSocketFactory( |
| final javax.net.ssl.SSLSocketFactory socketFactory, |
| final HostnameVerifier hostnameVerifier) { |
| this(socketFactory, null, null, hostnameVerifier); |
| } |
| |
| /** |
| * @since 4.4 |
| */ |
| public SSLConnectionSocketFactory( |
| final javax.net.ssl.SSLSocketFactory socketFactory, |
| final String[] supportedProtocols, |
| final String[] supportedCipherSuites, |
| final HostnameVerifier hostnameVerifier) { |
| this.socketFactory = Args.notNull(socketFactory, "SSL socket factory"); |
| this.supportedProtocols = supportedProtocols; |
| this.supportedCipherSuites = supportedCipherSuites; |
| this.hostnameVerifier = hostnameVerifier != null ? hostnameVerifier : HttpsSupport.getDefaultHostnameVerifier(); |
| this.tlsSessionValidator = new TlsSessionValidator(LOG); |
| } |
| |
| /** |
| * Performs any custom initialization for a newly created SSLSocket |
| * (before the SSL handshake happens). |
| * |
| * The default implementation is a no-op, but could be overridden to, e.g., |
| * call {@link javax.net.ssl.SSLSocket#setEnabledCipherSuites(String[])}. |
| * @throws IOException may be thrown if overridden |
| */ |
| protected void prepareSocket(final SSLSocket socket) throws IOException { |
| } |
| |
| @Override |
| public Socket createSocket(final HttpContext context) throws IOException { |
| return SocketFactory.getDefault().createSocket(); |
| } |
| |
| @Override |
| public Socket connectSocket( |
| final TimeValue connectTimeout, |
| final Socket socket, |
| final HttpHost host, |
| final InetSocketAddress remoteAddress, |
| final InetSocketAddress localAddress, |
| final HttpContext context) throws IOException { |
| Args.notNull(host, "HTTP host"); |
| Args.notNull(remoteAddress, "Remote address"); |
| final Socket sock = socket != null ? socket : createSocket(context); |
| if (localAddress != null) { |
| sock.bind(localAddress); |
| } |
| try { |
| if (TimeValue.isPositive(connectTimeout) && sock.getSoTimeout() == 0) { |
| sock.setSoTimeout(connectTimeout.toMillisecondsIntBound()); |
| } |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Connecting socket to {} with timeout {}", remoteAddress, connectTimeout); |
| } |
| // Run this under a doPrivileged to support lib users that run under a SecurityManager this allows granting connect permissions |
| // only to this library |
| try { |
| AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() { |
| @Override |
| public Object run() throws IOException { |
| sock.connect(remoteAddress, connectTimeout != null ? connectTimeout.toMillisecondsIntBound() : 0); |
| return null; |
| } |
| }); |
| } catch (final PrivilegedActionException e) { |
| Asserts.check(e.getCause() instanceof IOException, |
| "method contract violation only checked exceptions are wrapped: " + e.getCause()); |
| // only checked exceptions are wrapped - error and RTExceptions are rethrown by doPrivileged |
| throw (IOException) e.getCause(); |
| } |
| } catch (final IOException ex) { |
| Closer.closeQuietly(sock); |
| throw ex; |
| } |
| // Setup SSL layering if necessary |
| if (sock instanceof SSLSocket) { |
| final SSLSocket sslsock = (SSLSocket) sock; |
| LOG.debug("Starting handshake"); |
| sslsock.startHandshake(); |
| verifyHostname(sslsock, host.getHostName()); |
| return sock; |
| } |
| return createLayeredSocket(sock, host.getHostName(), remoteAddress.getPort(), context); |
| } |
| |
| @Override |
| public Socket createLayeredSocket( |
| final Socket socket, |
| final String target, |
| final int port, |
| final HttpContext context) throws IOException { |
| final SSLSocket sslsock = (SSLSocket) this.socketFactory.createSocket( |
| socket, |
| target, |
| port, |
| true); |
| if (supportedProtocols != null) { |
| sslsock.setEnabledProtocols(supportedProtocols); |
| } else { |
| sslsock.setEnabledProtocols((TLS.excludeWeak(sslsock.getEnabledProtocols()))); |
| } |
| if (supportedCipherSuites != null) { |
| sslsock.setEnabledCipherSuites(supportedCipherSuites); |
| } else { |
| sslsock.setEnabledCipherSuites(TlsCiphers.excludeWeak(sslsock.getEnabledCipherSuites())); |
| } |
| |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Enabled protocols: {}", (Object) sslsock.getEnabledProtocols()); |
| LOG.debug("Enabled cipher suites: {}", (Object) sslsock.getEnabledCipherSuites()); |
| } |
| |
| prepareSocket(sslsock); |
| LOG.debug("Starting handshake"); |
| sslsock.startHandshake(); |
| verifyHostname(sslsock, target); |
| return sslsock; |
| } |
| |
| private void verifyHostname(final SSLSocket sslsock, final String hostname) throws IOException { |
| try { |
| SSLSession session = sslsock.getSession(); |
| if (session == null) { |
| // In our experience this only happens under IBM 1.4.x when |
| // spurious (unrelated) certificates show up in the server' |
| // chain. Hopefully this will unearth the real problem: |
| final InputStream in = sslsock.getInputStream(); |
| in.available(); |
| // If ssl.getInputStream().available() didn't cause an |
| // exception, maybe at least now the session is available? |
| session = sslsock.getSession(); |
| if (session == null) { |
| // If it's still null, probably a startHandshake() will |
| // unearth the real problem. |
| sslsock.startHandshake(); |
| session = sslsock.getSession(); |
| } |
| } |
| if (session == null) { |
| throw new SSLHandshakeException("SSL session not available"); |
| } |
| verifySession(hostname, session); |
| } catch (final IOException iox) { |
| // close the socket before re-throwing the exception |
| Closer.closeQuietly(sslsock); |
| throw iox; |
| } |
| } |
| |
| protected void verifySession( |
| final String hostname, |
| final SSLSession sslSession) throws SSLException { |
| tlsSessionValidator.verifySession(hostname, sslSession, hostnameVerifier); |
| } |
| |
| } |