diff --git a/README.adoc b/README.adoc index f1d6da5..8bef60e 100644 --- a/README.adoc +++ b/README.adoc @@ -5,7 +5,7 @@ image::https://raw.githubusercontent.com/fujitsu/launcher/image/logo/launcher-lo == Overview Launcher is an implementation of https://microprofile.io/[MicroProfile]. -Launcher 2.0 beta 1 conforms to a subset of MicroProfile 3.0. +Launcher 2.0 conforms to MicroProfile 3.0. Currently supported APIs are as follows: * CDI 2.0 diff --git a/launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/http2/AlpnSupport.java b/launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/http2/AlpnSupport.java new file mode 100644 index 0000000..3577509 --- /dev/null +++ b/launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/http2/AlpnSupport.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2012, 2017 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019 Fujitsu Limited. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.grizzly.http2; + +import java.io.IOException; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.net.ssl.SSLEngine; +import org.glassfish.grizzly.CloseListener; +import org.glassfish.grizzly.Closeable; +import org.glassfish.grizzly.Connection; +import org.glassfish.grizzly.Grizzly; +import org.glassfish.grizzly.ICloseType; +import org.glassfish.grizzly.Transport; +import org.glassfish.grizzly.npn.AlpnClientNegotiator; +import org.glassfish.grizzly.npn.AlpnServerNegotiator; +import org.glassfish.grizzly.npn.NegotiationSupport; +import org.glassfish.grizzly.ssl.SSLBaseFilter; +import org.glassfish.grizzly.ssl.SSLFilter; +import org.glassfish.grizzly.ssl.SSLUtils; + +/** + * Grizzly TLS Next Protocol Negotiation support class. + * + */ +public class AlpnSupport { + private final static Logger LOGGER = Grizzly.logger(AlpnSupport.class); + + private final static Map SSL_TO_CONNECTION_MAP = + new WeakHashMap<>(); + + private static final AlpnSupport INSTANCE; + + private static final double JAVA_VERSION = Double.parseDouble(System.getProperty("java.specification.version")); + + static { + + boolean isAlpnAvailable = false; + + if (JAVA_VERSION > 1.8) { + isAlpnAvailable = true; + } else { + try { + ClassLoader.getSystemClassLoader().loadClass("sun.security.ssl.GrizzlyNPN"); + isAlpnAvailable = true; + } catch (Throwable e) { + LOGGER.log(Level.FINE, "TLS ALPN extension is not found:", e); + } + } + + INSTANCE = isAlpnAvailable ? new AlpnSupport() : null; + } + + public static boolean isEnabled() { + return INSTANCE != null; + } + + public static AlpnSupport getInstance() { + if (!isEnabled()) { + throw new IllegalStateException("TLS ALPN is disabled"); + } + + return INSTANCE; + } + + public static Connection getConnection(final SSLEngine engine) { + synchronized (SSL_TO_CONNECTION_MAP) { + return SSL_TO_CONNECTION_MAP.get(engine); + } + } + + private static void setConnection(final SSLEngine engine, + final Connection connection) { + synchronized (SSL_TO_CONNECTION_MAP) { + SSL_TO_CONNECTION_MAP.put(engine, connection); + } + } + + private final Map serverSideNegotiators = + new WeakHashMap<>(); + private final ReadWriteLock serverSideLock = new ReentrantReadWriteLock(); + + private final Map clientSideNegotiators = + new WeakHashMap<>(); + private final ReadWriteLock clientSideLock = new ReentrantReadWriteLock(); + + private final SSLFilter.HandshakeListener handshakeListener = + new SSLFilter.HandshakeListener() { + + @Override + public void onStart(final Connection connection) { + final SSLEngine sslEngine = SSLUtils.getSSLEngine(connection); + assert sslEngine != null; + + if (sslEngine.getUseClientMode()) { + AlpnClientNegotiator negotiator; + clientSideLock.readLock().lock(); + + try { + negotiator = clientSideNegotiators.get(connection); + if (negotiator == null) { + negotiator = clientSideNegotiators.get(connection.getTransport()); + } + } finally { + clientSideLock.readLock().unlock(); + } + + if (negotiator != null) { + // add a CloseListener to ensure we remove the + // negotiator associated with this SSLEngine + connection.addCloseListener(new CloseListener() { + @Override + public void onClosed(Closeable closeable, ICloseType type) throws IOException { + NegotiationSupport.removeAlpnClientNegotiator(sslEngine); + SSL_TO_CONNECTION_MAP.remove(sslEngine); + } + }); + setConnection(sslEngine, connection); + NegotiationSupport.addNegotiator(sslEngine, negotiator); + } + } else { + AlpnServerNegotiator negotiator; + serverSideLock.readLock().lock(); + + try { + negotiator = serverSideNegotiators.get(connection); + if (negotiator == null) { + negotiator = serverSideNegotiators.get(connection.getTransport()); + } + } finally { + serverSideLock.readLock().unlock(); + } + + if (negotiator != null) { + + // add a CloseListener to ensure we remove the + // negotiator associated with this SSLEngine + connection.addCloseListener(new CloseListener() { + @Override + public void onClosed(Closeable closeable, ICloseType type) throws IOException { + NegotiationSupport.removeAlpnServerNegotiator(sslEngine); + SSL_TO_CONNECTION_MAP.remove(sslEngine); + } + }); + setConnection(sslEngine, connection); + NegotiationSupport.addNegotiator(sslEngine, negotiator); + } + } + + } + + @Override + public void onComplete(final Connection connection) { + } + + @Override + public void onFailure(Connection connection, Throwable t) { + } + }; + + private AlpnSupport() { + } + + public void configure(final SSLBaseFilter sslFilter) { + sslFilter.addHandshakeListener(handshakeListener); + } + + public void setServerSideNegotiator(final Transport transport, + final AlpnServerNegotiator negotiator) { + putServerSideNegotiator(transport, negotiator); + } + + public void setServerSideNegotiator(final Connection connection, + final AlpnServerNegotiator negotiator) { + putServerSideNegotiator(connection, negotiator); + } + + + public void setClientSideNegotiator(final Transport transport, + final AlpnClientNegotiator negotiator) { + putClientSideNegotiator(transport, negotiator); + } + + public void setClientSideNegotiator(final Connection connection, + final AlpnClientNegotiator negotiator) { + putClientSideNegotiator(connection, negotiator); + } + + private void putServerSideNegotiator(final Object object, + final AlpnServerNegotiator negotiator) { + serverSideLock.writeLock().lock(); + + try { + serverSideNegotiators.put(object, negotiator); + } finally { + serverSideLock.writeLock().unlock(); + } + } + + private void putClientSideNegotiator(final Object object, + final AlpnClientNegotiator negotiator) { + clientSideLock.writeLock().lock(); + + try { + clientSideNegotiators.put(object, negotiator); + } finally { + clientSideLock.writeLock().unlock(); + } + } + +} diff --git a/launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/http2/Http2ServerFilter.java b/launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/http2/Http2ServerFilter.java new file mode 100644 index 0000000..14a9278 --- /dev/null +++ b/launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/http2/Http2ServerFilter.java @@ -0,0 +1,1073 @@ +/* + * Copyright (c) 2015, 2017 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019 Fujitsu Limited. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.grizzly.http2; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.glassfish.grizzly.Buffer; +import org.glassfish.grizzly.Connection; +import org.glassfish.grizzly.Grizzly; +import org.glassfish.grizzly.Transport; +import org.glassfish.grizzly.attributes.Attribute; +import org.glassfish.grizzly.attributes.AttributeBuilder; +import org.glassfish.grizzly.filterchain.Filter; +import org.glassfish.grizzly.filterchain.FilterChainContext; +import org.glassfish.grizzly.filterchain.FilterChainEvent; +import org.glassfish.grizzly.filterchain.NextAction; +import org.glassfish.grizzly.filterchain.ShutdownEvent; +import org.glassfish.grizzly.http.HttpBrokenContentException; +import org.glassfish.grizzly.http.HttpContent; +import org.glassfish.grizzly.http.HttpContext; +import org.glassfish.grizzly.http.HttpEvents; +import org.glassfish.grizzly.http.HttpHeader; +import org.glassfish.grizzly.http.HttpPacket; +import org.glassfish.grizzly.http.HttpRequestPacket; +import org.glassfish.grizzly.http.HttpResponsePacket; +import org.glassfish.grizzly.http.Method; +import org.glassfish.grizzly.http.Protocol; +import org.glassfish.grizzly.http.server.http2.PushEvent; +import org.glassfish.grizzly.http.util.FastHttpDateFormat; +import org.glassfish.grizzly.http.util.Header; +import org.glassfish.grizzly.http.util.HttpStatus; +import org.glassfish.grizzly.http2.NetLogger.Context; +import org.glassfish.grizzly.http2.frames.ErrorCode; +import org.glassfish.grizzly.http2.frames.HeaderBlockHead; +import org.glassfish.grizzly.http2.frames.HeadersFrame; +import org.glassfish.grizzly.http2.frames.Http2Frame; +import org.glassfish.grizzly.http2.frames.PushPromiseFrame; +import org.glassfish.grizzly.http2.frames.SettingsFrame; +import org.glassfish.grizzly.impl.FutureImpl; +import org.glassfish.grizzly.memory.Buffers; +import org.glassfish.grizzly.ssl.SSLUtils; + +import javax.net.ssl.SSLEngine; + +import static org.glassfish.grizzly.http2.Termination.IN_FIN_TERMINATION; + +/** + * + * @author oleksiys + */ +public class Http2ServerFilter extends Http2BaseFilter { + private final static Logger LOGGER = Grizzly.logger(Http2ServerFilter.class); + + private static final String[] CIPHER_SUITE_BLACK_LIST = { + "TLS_NULL_WITH_NULL_NULL", + "TLS_RSA_WITH_NULL_MD5", + "TLS_RSA_WITH_NULL_SHA", + "TLS_RSA_EXPORT_WITH_RC4_40_MD5", + "TLS_RSA_WITH_RC4_128_MD5", + "TLS_RSA_WITH_RC4_128_SHA", + "TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5", + "TLS_RSA_WITH_IDEA_CBC_SHA", + "TLS_RSA_EXPORT_WITH_DES40_CBC_SHA", + "TLS_RSA_WITH_DES_CBC_SHA", + "TLS_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA", + "TLS_DH_DSS_WITH_DES_CBC_SHA", + "TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA", + "TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA", + "TLS_DH_RSA_WITH_DES_CBC_SHA", + "TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", + "TLS_DHE_DSS_WITH_DES_CBC_SHA", + "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA", + "TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", + "TLS_DHE_RSA_WITH_DES_CBC_SHA", + "TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_DH_anon_EXPORT_WITH_RC4_40_MD5", + "TLS_DH_anon_WITH_RC4_128_MD5", + "TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA", + "TLS_DH_anon_WITH_DES_CBC_SHA", + "TLS_DH_anon_WITH_3DES_EDE_CBC_SHA", + "TLS_KRB5_WITH_DES_CBC_SHA", + "TLS_KRB5_WITH_3DES_EDE_CBC_SHA", + "TLS_KRB5_WITH_RC4_128_SHA", + "TLS_KRB5_WITH_IDEA_CBC_SHA", + "TLS_KRB5_WITH_DES_CBC_MD5", + "TLS_KRB5_WITH_3DES_EDE_CBC_MD5", + "TLS_KRB5_WITH_RC4_128_MD5", + "TLS_KRB5_WITH_IDEA_CBC_MD5", + "TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA", + "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA", + "TLS_KRB5_EXPORT_WITH_RC4_40_SHA", + "TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5", + "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5", + "TLS_KRB5_EXPORT_WITH_RC4_40_MD5", + "TLS_PSK_WITH_NULL_SHA", + "TLS_DHE_PSK_WITH_NULL_SHA", + "TLS_RSA_PSK_WITH_NULL_SHA", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_DH_DSS_WITH_AES_128_CBC_SHA", + "TLS_DH_RSA_WITH_AES_128_CBC_SHA", + "TLS_DHE_DSS_WITH_AES_128_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_DH_anon_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_DH_DSS_WITH_AES_256_CBC_SHA", + "TLS_DH_RSA_WITH_AES_256_CBC_SHA", + "TLS_DHE_DSS_WITH_AES_256_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_DH_anon_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_NULL_SHA256", + "TLS_RSA_WITH_AES_128_CBC_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA256", + "TLS_DH_DSS_WITH_AES_128_CBC_SHA256", + "TLS_DH_RSA_WITH_AES_128_CBC_SHA256", + "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256", + "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_DH_DSS_WITH_AES_256_CBC_SHA256", + "TLS_DH_RSA_WITH_AES_256_CBC_SHA256", + "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256", + "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", + "TLS_DH_anon_WITH_AES_128_CBC_SHA256", + "TLS_DH_anon_WITH_AES_256_CBC_SHA256", + "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA", + "TLS_PSK_WITH_RC4_128_SHA", + "TLS_PSK_WITH_3DES_EDE_CBC_SHA", + "TLS_PSK_WITH_AES_128_CBC_SHA", + "TLS_PSK_WITH_AES_256_CBC_SHA", + "TLS_DHE_PSK_WITH_RC4_128_SHA", + "TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA", + "TLS_DHE_PSK_WITH_AES_128_CBC_SHA", + "TLS_DHE_PSK_WITH_AES_256_CBC_SHA", + "TLS_RSA_PSK_WITH_RC4_128_SHA", + "TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA", + "TLS_RSA_PSK_WITH_AES_128_CBC_SHA", + "TLS_RSA_PSK_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_SEED_CBC_SHA", + "TLS_DH_DSS_WITH_SEED_CBC_SHA", + "TLS_DH_RSA_WITH_SEED_CBC_SHA", + "TLS_DHE_DSS_WITH_SEED_CBC_SHA", + "TLS_DHE_RSA_WITH_SEED_CBC_SHA", + "TLS_DH_anon_WITH_SEED_CBC_SHA", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_DH_RSA_WITH_AES_128_GCM_SHA256", + "TLS_DH_RSA_WITH_AES_256_GCM_SHA384", + "TLS_DH_DSS_WITH_AES_128_GCM_SHA256", + "TLS_DH_DSS_WITH_AES_256_GCM_SHA384", + "TLS_DH_anon_WITH_AES_128_GCM_SHA256", + "TLS_DH_anon_WITH_AES_256_GCM_SHA384", + "TLS_PSK_WITH_AES_128_GCM_SHA256", + "TLS_PSK_WITH_AES_256_GCM_SHA384", + "TLS_RSA_PSK_WITH_AES_128_GCM_SHA256", + "TLS_RSA_PSK_WITH_AES_256_GCM_SHA384", + "TLS_PSK_WITH_AES_128_CBC_SHA256", + "TLS_PSK_WITH_AES_256_CBC_SHA384", + "TLS_PSK_WITH_NULL_SHA256", + "TLS_PSK_WITH_NULL_SHA384", + "TLS_DHE_PSK_WITH_AES_128_CBC_SHA256", + "TLS_DHE_PSK_WITH_AES_256_CBC_SHA384", + "TLS_DHE_PSK_WITH_NULL_SHA256", + "TLS_DHE_PSK_WITH_NULL_SHA384", + "TLS_RSA_PSK_WITH_AES_128_CBC_SHA256", + "TLS_RSA_PSK_WITH_AES_256_CBC_SHA384", + "TLS_RSA_PSK_WITH_NULL_SHA256", + "TLS_RSA_PSK_WITH_NULL_SHA384", + "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_EMPTY_RENEGOTIATION_INFO_SCSV", + "TLS_ECDH_ECDSA_WITH_NULL_SHA", + "TLS_ECDH_ECDSA_WITH_RC4_128_SHA", + "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_NULL_SHA", + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", + "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDH_RSA_WITH_NULL_SHA", + "TLS_ECDH_RSA_WITH_RC4_128_SHA", + "TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_NULL_SHA", + "TLS_ECDHE_RSA_WITH_RC4_128_SHA", + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDH_anon_WITH_NULL_SHA", + "TLS_ECDH_anon_WITH_RC4_128_SHA", + "TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDH_anon_WITH_AES_128_CBC_SHA", + "TLS_ECDH_anon_WITH_AES_256_CBC_SHA", + "TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA", + "TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA", + "TLS_SRP_SHA_WITH_AES_128_CBC_SHA", + "TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA", + "TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA", + "TLS_SRP_SHA_WITH_AES_256_CBC_SHA", + "TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA", + "TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_PSK_WITH_RC4_128_SHA", + "TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_PSK_WITH_NULL_SHA", + "TLS_ECDHE_PSK_WITH_NULL_SHA256", + "TLS_ECDHE_PSK_WITH_NULL_SHA384", + "TLS_RSA_WITH_ARIA_128_CBC_SHA256", + "TLS_RSA_WITH_ARIA_256_CBC_SHA384", + "TLS_DH_DSS_WITH_ARIA_128_CBC_SHA256", + "TLS_DH_DSS_WITH_ARIA_256_CBC_SHA384", + "TLS_DH_RSA_WITH_ARIA_128_CBC_SHA256", + "TLS_DH_RSA_WITH_ARIA_256_CBC_SHA384", + "TLS_DHE_DSS_WITH_ARIA_128_CBC_SHA256", + "TLS_DHE_DSS_WITH_ARIA_256_CBC_SHA384", + "TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256", + "TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384", + "TLS_DH_anon_WITH_ARIA_128_CBC_SHA256", + "TLS_DH_anon_WITH_ARIA_256_CBC_SHA384", + "TLS_ECDHE_ECDSA_WITH_ARIA_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_ARIA_256_CBC_SHA384", + "TLS_ECDH_ECDSA_WITH_ARIA_128_CBC_SHA256", + "TLS_ECDH_ECDSA_WITH_ARIA_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_ARIA_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_ARIA_256_CBC_SHA384", + "TLS_ECDH_RSA_WITH_ARIA_128_CBC_SHA256", + "TLS_ECDH_RSA_WITH_ARIA_256_CBC_SHA384", + "TLS_RSA_WITH_ARIA_128_GCM_SHA256", + "TLS_RSA_WITH_ARIA_256_GCM_SHA384", + "TLS_DH_RSA_WITH_ARIA_128_GCM_SHA256", + "TLS_DH_RSA_WITH_ARIA_256_GCM_SHA384", + "TLS_DH_DSS_WITH_ARIA_128_GCM_SHA256", + "TLS_DH_DSS_WITH_ARIA_256_GCM_SHA384", + "TLS_DH_anon_WITH_ARIA_128_GCM_SHA256", + "TLS_DH_anon_WITH_ARIA_256_GCM_SHA384", + "TLS_ECDH_ECDSA_WITH_ARIA_128_GCM_SHA256", + "TLS_ECDH_ECDSA_WITH_ARIA_256_GCM_SHA384", + "TLS_ECDH_RSA_WITH_ARIA_128_GCM_SHA256", + "TLS_ECDH_RSA_WITH_ARIA_256_GCM_SHA384", + "TLS_PSK_WITH_ARIA_128_CBC_SHA256", + "TLS_PSK_WITH_ARIA_256_CBC_SHA384", + "TLS_DHE_PSK_WITH_ARIA_128_CBC_SHA256", + "TLS_DHE_PSK_WITH_ARIA_256_CBC_SHA384", + "TLS_RSA_PSK_WITH_ARIA_128_CBC_SHA256", + "TLS_RSA_PSK_WITH_ARIA_256_CBC_SHA384", + "TLS_PSK_WITH_ARIA_128_GCM_SHA256", + "TLS_PSK_WITH_ARIA_256_GCM_SHA384", + "TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256", + "TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384", + "TLS_ECDHE_PSK_WITH_ARIA_128_CBC_SHA256", + "TLS_ECDHE_PSK_WITH_ARIA_256_CBC_SHA384", + "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_DH_DSS_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_DH_DSS_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_DH_anon_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_DH_anon_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_RSA_WITH_AES_128_CCM", + "TLS_RSA_WITH_AES_256_CCM", + "TLS_RSA_WITH_AES_128_CCM_8", + "TLS_RSA_WITH_AES_256_CCM_8", + "TLS_PSK_WITH_AES_128_CCM", + "TLS_PSK_WITH_AES_256_CCM", + "TLS_PSK_WITH_AES_128_CCM_8", + "TLS_PSK_WITH_AES_256_CCM_8" + }; + + static { + Arrays.sort(CIPHER_SUITE_BLACK_LIST); + } + + // flag, which enables/disables payload support for HTTP methods, + // for which HTTP spec doesn't clearly state whether they support payload. + // Known "undefined" methods are: GET, HEAD, DELETE + private boolean allowPayloadForUndefinedHttpMethods; + + private final Attribute CIPHER_CHECKED = + AttributeBuilder.DEFAULT_ATTRIBUTE_BUILDER.createAttribute("BLACK_LIST_CIPHER_SUITE_CHEKCED"); + + private Collection activeConnections = new HashSet<>(1024); + private AtomicBoolean shuttingDown = new AtomicBoolean(); + + /** + * Create a new {@link Http2ServerFilter} using the specified {@link Http2Configuration}. + * Configuration may be changed post-construction by calling {@link #getConfiguration()}. + */ + public Http2ServerFilter(final Http2Configuration configuration) { + super(configuration); + } + + + /** + * The flag, which enables/disables payload support for HTTP methods, + * for which HTTP spec doesn't clearly state whether they support payload. + * Known "undefined" methods are: GET, HEAD, DELETE. + * + * @return true if "undefined" methods support payload, or false otherwise + */ + @SuppressWarnings("unused") + public boolean isAllowPayloadForUndefinedHttpMethods() { + return allowPayloadForUndefinedHttpMethods; + } + + /** + * The flag, which enables/disables payload support for HTTP methods, + * for which HTTP spec doesn't clearly state whether they support payload. + * Known "undefined" methods are: GET, HEAD, DELETE. + * + * @param allowPayloadForUndefinedHttpMethods true if "undefined" methods support payload, or false otherwise + */ + @SuppressWarnings("unused") + public void setAllowPayloadForUndefinedHttpMethods(boolean allowPayloadForUndefinedHttpMethods) { + this.allowPayloadForUndefinedHttpMethods = allowPayloadForUndefinedHttpMethods; + } + + @Override + public NextAction handleAccept(final FilterChainContext ctx) throws IOException { + if (!shuttingDown.get()) { + activeConnections.add(ctx.getConnection()); + } + return ctx.getInvokeAction(); + } + + @Override + public NextAction handleClose(final FilterChainContext ctx) throws IOException { + if (!shuttingDown.get()) { + activeConnections.remove(ctx.getConnection()); + } + return ctx.getInvokeAction(); + } + + @SuppressWarnings("unchecked") + @Override + public NextAction handleRead(final FilterChainContext ctx) + throws IOException { + + // if it's a stream chain (the stream is already assigned) - just + // bypass the parsing part + if (checkIfHttp2StreamChain(ctx)) { + return ctx.getInvokeAction(); + } + + final Connection connection = ctx.getConnection(); + Http2State http2State = Http2State.get(connection); + + if (http2State != null && http2State.isNeverHttp2()) { + // NOT HTTP2 connection and never will be + return ctx.getInvokeAction(); + } + + final HttpContent httpContent = ctx.getMessage(); + final HttpHeader httpHeader = httpContent.getHttpHeader(); + + if (http2State == null) { // Not HTTP/2 (yet?) + assert httpHeader.isRequest(); + + if (httpHeader.isSecure() && !AlpnSupport.isEnabled()) { + // ALPN should've set the Http2State, but in our case it's null. + // It means ALPN was bypassed - SSL without ALPN shouldn't work. + // Don't try HTTP/2 in this case. + Http2State.create(connection).setNeverHttp2(); + return ctx.getInvokeAction(); + } + + final HttpRequestPacket httpRequest = + (HttpRequestPacket) httpHeader; + + if (!Method.PRI.equals(httpRequest.getMethod())) { + final boolean isLast = httpContent.isLast(); + if (tryHttpUpgrade(ctx, httpRequest, isLast) && isLast) { + enableOpReadNow(ctx); + } + + return ctx.getInvokeAction(); + } + + // PRI method + // DIRECT HTTP/2.0 request + http2State = doDirectUpgrade(ctx); + } + + final Http2Session http2Session = + obtainHttp2Session(http2State, ctx, true); + + if (httpHeader.isSecure() && !getConfiguration().isDisableCipherCheck() && !CIPHER_CHECKED.isSet(connection)) { + CIPHER_CHECKED.set(connection, connection); + final SSLEngine engine = SSLUtils.getSSLEngine(connection); + if (engine != null) { + if (Arrays.binarySearch(CIPHER_SUITE_BLACK_LIST, engine.getSession().getCipherSuite()) >= 0) { + http2Session.terminate(ErrorCode.INADEQUATE_SECURITY, null); + return ctx.getStopAction(); + } + } + } + + final Buffer framePayload; + if (!http2Session.isHttp2InputEnabled()) { // Preface is not received yet + + if (http2State.isHttpUpgradePhase()) { + // It's plain HTTP/1.1 data coming with upgrade request + if (httpContent.isLast()) { + http2State.setDirectUpgradePhase(); // expecting preface + enableOpReadNow(ctx); + } + + return ctx.getInvokeAction(); + } + + final HttpRequestPacket httpRequest = (HttpRequestPacket) httpHeader; + + // PRI message hasn't been checked + try { + if (!checkPRI(httpRequest, httpContent)) { + // Not enough PRI content read + return ctx.getStopAction(httpContent); + } + } catch (Exception e) { + httpRequest.getProcessingState().setError(true); + httpRequest.getProcessingState().setKeepAlive(false); + + final HttpResponsePacket httpResponse = httpRequest.getResponse(); + httpResponse.setStatus(HttpStatus.BAD_REQUEST_400); + ctx.write(httpResponse); + connection.closeSilently(); + + return ctx.getStopAction(); + } + + final Buffer payload = httpContent.getContent(); + framePayload = payload.split(payload.position() + PRI_PAYLOAD.length); + } else { + framePayload = httpContent.getContent(); + } + + httpContent.recycle(); + + // Prime the initial value of push. Will be overridden if the settings contain a + // new value. + if (connection.getAttributes().getAttribute(HTTP2_PUSH_ENABLED) == null) { + connection.getAttributes().setAttribute(HTTP2_PUSH_ENABLED, Boolean.TRUE); + } + + final List framesList = + frameCodec.parse(http2Session, + http2State.getFrameParsingState(), + framePayload); + + if (!processFrames(ctx, http2Session, framesList)) { + return ctx.getSuspendAction(); + } + + return ctx.getStopAction(); + } + + @Override + public NextAction handleEvent(final FilterChainContext ctx, + final FilterChainEvent event) throws IOException { + + final Object type = event.type(); + + if (type == ShutdownEvent.TYPE) { + if (shuttingDown.compareAndSet(false, true)) { + ((ShutdownEvent) event).addShutdownTask(new Callable() { + @Override + public Filter call() throws Exception { + final Collection activeConnections = shuttingDown(); + if (!activeConnections.isEmpty()) { + final List futures = new ArrayList<>(activeConnections.size()); + for (final Connection c : activeConnections) { + if (c.isOpen()) { + final Http2Session session = Http2Session.get(c); + if (session != null) { + futures.add(session.terminateGracefully()); + } + } + } + for (final FutureImpl f : futures) { + f.get(); + } + } + return Http2ServerFilter.this; + } + }); + } + } + + if (type == HttpEvents.IncomingHttpUpgradeEvent.TYPE) { + final HttpHeader header + = ((HttpEvents.IncomingHttpUpgradeEvent) event).getHttpHeader(); + if (header.isRequest()) { + //@TODO temporary not optimal solution, because we check the req here and in the handleRead() + if (checkRequestHeadersOnUpgrade((HttpRequestPacket) header)) { + // for the HTTP/2 upgrade request we want to obey HTTP/1.1 + // content modifiers (transfer and content encodings) + header.setIgnoreContentModifiers(false); + + return ctx.getStopAction(); + } + } + + return ctx.getInvokeAction(); + } + + final Http2State state = Http2State.get(ctx.getConnection()); + + if (state == null || state.isNeverHttp2()) { + return ctx.getInvokeAction(); + } + + if (type == PushEvent.TYPE) { + doPush(ctx, (PushEvent) event); + return ctx.getSuspendAction(); + } + + if (type == HttpEvents.ResponseCompleteEvent.TYPE) { + final HttpContext httpContext = HttpContext.get(ctx); + final Http2Stream stream = (Http2Stream) httpContext.getContextStorage(); + stream.onProcessingComplete(); + + final Http2Session http2Session = stream.getHttp2Session(); + + if (!http2Session.isHttp2InputEnabled()) { + // it's the first HTTP/1.1 -> HTTP/2.0 upgrade request. + // We have to notify regular HTTP codec filter as well + state.finishHttpUpgradePhase(); // mark HTTP upgrade as finished (in case it's still on) + + return ctx.getInvokeAction(); + } + + // it's pure HTTP/2.0 request processing + return ctx.getStopAction(); + } + + return super.handleEvent(ctx, event); + } + + @Override + protected void onPrefaceReceived(Http2Session http2Session) { + // In ALPN case server will send the preface only after receiving preface + // from a client + http2Session.sendPreface(); + } + + private Http2State doDirectUpgrade(final FilterChainContext ctx) { + final Connection connection = ctx.getConnection(); + + final Http2Session http2Session = + new Http2Session(connection, true, this); + + // Create HTTP/2.0 connection for the given Grizzly Connection + final Http2State http2State = Http2State.create(connection); + http2State.setHttp2Session(http2Session); + http2State.setDirectUpgradePhase(); + http2Session.setupFilterChains(ctx, true); + + // server preface + http2Session.sendPreface(); + + return http2State; + } + + Collection shuttingDown() { + shuttingDown.compareAndSet(false, true); + return activeConnections; + } + + private boolean tryHttpUpgrade(final FilterChainContext ctx, + final HttpRequestPacket httpRequest, final boolean isLast) + throws Http2StreamException { + + if (!checkHttpMethodOnUpgrade(httpRequest)) { + return false; + } + + if (!checkRequestHeadersOnUpgrade(httpRequest)) { + return false; + } + + final boolean http2Upgrade = isHttp2UpgradingVersion(httpRequest); + + if (!http2Upgrade) { + // Not HTTP/2.0 HTTP packet + return false; + } + + final SettingsFrame settingsFrame = + getHttp2UpgradeSettings(httpRequest); + + if (settingsFrame == null) { + // Not HTTP/2.0 HTTP packet + return false; + } + + final Connection connection = ctx.getConnection(); + + final Http2Session http2Session = + new Http2Session(connection, true, this); + // Create HTTP/2.0 connection for the given Grizzly Connection + final Http2State http2State = Http2State.create(connection); + http2State.setHttp2Session(http2Session); + http2Session.setupFilterChains(ctx, true); + + if (isLast) { + http2State.setDirectUpgradePhase(); // expecting preface + } + + try { + applySettings(http2Session, settingsFrame); + } catch (Http2SessionException e) { + Http2State.remove(connection); + return false; + } + + // Send 101 Switch Protocols back to the client + final HttpResponsePacket httpResponse = httpRequest.getResponse(); + httpResponse.setStatus(HttpStatus.SWITCHING_PROTOCOLS_101); + httpResponse.setHeader(Header.Connection, "Upgrade"); + httpResponse.setHeader(Header.Upgrade, HTTP2_CLEAR); + httpResponse.setIgnoreContentModifiers(true); + + ctx.write(httpResponse); + + // un-commit the response + httpResponse.setCommitted(false); + + + + // server preface + http2Session.sendPreface(); + + // reset the response object + httpResponse.setStatus(HttpStatus.OK_200); + httpResponse.getHeaders().clear(); + httpRequest.setProtocol(Protocol.HTTP_2_0); + httpResponse.setProtocol(Protocol.HTTP_2_0); + + httpRequest.getUpgradeDC().recycle(); + httpResponse.getProcessingState().setKeepAlive(true); + + if (http2Session.isGoingAway()) { + Http2State.remove(connection); + return false; + } + // create a virtual stream for this request + final Http2Stream stream = http2Session.acceptUpgradeStream( + httpRequest, 0, !httpRequest.isExpectContent()); + + // replace the HttpContext + final HttpContext httpContext = HttpContext.newInstance(stream, + stream, stream, httpRequest); + httpRequest.getProcessingState().setHttpContext(httpContext); + // add read-only HTTP2Stream attribute + httpRequest.setAttribute(Http2Stream.HTTP2_STREAM_ATTRIBUTE, stream); + httpContext.attach(ctx); + + return true; + } + + private boolean checkHttpMethodOnUpgrade( + final HttpRequestPacket httpRequest) { + + return httpRequest.getMethod() != Method.CONNECT; + } + + private boolean checkPRI(final HttpRequestPacket httpRequest, + final HttpContent httpContent) { + if (!Method.PRI.equals(httpRequest.getMethod())) { + // If it's not PRI after upgrade is completed - it must be an error + throw new HttpBrokenContentException(); + } + + // Check the PRI message payload + final Buffer payload = httpContent.getContent(); + if (payload.remaining() < PRI_PAYLOAD.length) { + return false; + } + + final int pos = payload.position(); + for (int i = 0; i < PRI_PAYLOAD.length; i++) { + if (payload.get(pos + i) != PRI_PAYLOAD[i]) { + // Unexpected PRI payload + throw new HttpBrokenContentException(); + } + } + + return true; + } + + @Override + protected void processCompleteHeader( + final Http2Session http2Session, + final FilterChainContext context, + final HeaderBlockHead firstHeaderFrame) throws IOException { + + if (!ignoreFrameForStreamId(http2Session, firstHeaderFrame.getStreamId())) { + processInRequest(http2Session, context, (HeadersFrame) firstHeaderFrame); + } + } + + private void processInRequest(final Http2Session http2Session, + final FilterChainContext context, final HeadersFrame headersFrame) + throws IOException { + + final Http2Request request = Http2Request.create(); + request.setConnection(context.getConnection()); + + Http2Stream stream = http2Session.getStream(headersFrame.getStreamId()); + if (stream != null) { + final Http2Stream.State state = stream.getState(); + if (state == Http2Stream.State.HALF_CLOSED_REMOTE || state == Http2Stream.State.CLOSED) { + if (headersFrame.isEndStream()) { + throw new Http2SessionException(ErrorCode.STREAM_CLOSED); + } + throw new Http2StreamException(stream.getId(), ErrorCode.STREAM_CLOSED); + } + + if (!headersFrame.isEndStream()) { + throw new Http2StreamException(stream.getId(), + ErrorCode.PROTOCOL_ERROR, + "Received second HEADERS frame, but was not marked fin."); + } + + try { + stream.onRcvHeaders(headersFrame.isEndStream()); + final Map capture = ((NetLogger.isActive()) ? new HashMap<>() : null); + DecoderUtils.decodeTrailerHeaders(http2Session, stream.getRequest(), capture); + NetLogger.log(Context.RX, http2Session, headersFrame, capture); + } catch (IOException ioe) { + throw new Http2SessionException(ErrorCode.COMPRESSION_ERROR, ioe.getCause().getMessage()); + } catch (HeaderDecodingException hde) { + if (hde.getErrorType() == HeaderDecodingException.ErrorType.SESSION) { + throw new Http2SessionException(hde.getErrorCode(), hde.getMessage()); + } else { + throw new Http2StreamException(stream.getId(), hde.getErrorCode(), hde.getMessage()); + } + } + if (headersFrame.isTruncated()) { + if (LOGGER.isLoggable(Level.WARNING)) { + LOGGER.log(Level.WARNING, + "[{0}, {1}] Trailer headers truncated. Some headers may not be available.", + new Object[] { http2Session.toString(), headersFrame.getStreamId()}); + } + } + stream.flushInputData(); + stream.inputBuffer.close(IN_FIN_TERMINATION); + return; + } + + stream = http2Session.acceptStream(request, + headersFrame.getStreamId(), + headersFrame.getStreamDependency(), + headersFrame.isExclusive(), + 0); + if (stream == null) { // GOAWAY has been sent, so ignoring this request + request.recycle(); + return; + } + + try { + final Map capture = ((NetLogger.isActive()) ? new LinkedHashMap<>() : null); + DecoderUtils.decodeRequestHeaders(http2Session, request, capture); + NetLogger.log(Context.RX, http2Session, headersFrame, capture); + } catch (IOException ioe) { + throw new Http2SessionException(ErrorCode.COMPRESSION_ERROR, ioe.getCause().getMessage()); + } catch (HeaderDecodingException hde) { + if (hde.getErrorType() == HeaderDecodingException.ErrorType.SESSION) { + throw new Http2SessionException(hde.getErrorCode(), hde.getMessage()); + } else { + throw new Http2StreamException(stream.getId(), hde.getErrorCode(), hde.getMessage()); + } + } + if (headersFrame.isTruncated()) { + final HttpResponsePacket response = request.getResponse(); + HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE.setValues(response); + final HttpHeader header = response.getHttpHeader(); + header.setContentLength(0); + header.setExpectContent(false); + processOutgoingHttpHeader(context, http2Session, header, response); + return; + } + onHttpHeadersParsed(request, context); + request.getHeaders().mark(); + + prepareIncomingRequest(stream, request); + + final boolean isEOS = headersFrame.isEndStream(); + stream.onRcvHeaders(isEOS); + + // stream HEADERS frame will be transformed to HTTP request packet + if (isEOS) { + request.setExpectContent(false); + } + + final boolean isExpectContent = request.isExpectContent(); + if (!isExpectContent) { + stream.inputBuffer.terminate(IN_FIN_TERMINATION); + } + + sendUpstream(http2Session, + stream, + request.httpContentBuilder().content(Buffers.EMPTY_BUFFER).last(!isExpectContent).build()); + } + + + + /** + * + * @param ctx the current {@link FilterChainContext} + * @param http2Session the {@link Http2Session} associated with this {@link HttpHeader} + * @param httpHeader the out-going {@link HttpHeader} + * @param entireHttpPacket the complete {@link HttpPacket} + * + * @throws IOException if an error occurs sending the packet + */ + @Override + @SuppressWarnings("unchecked") + protected void processOutgoingHttpHeader(final FilterChainContext ctx, + final Http2Session http2Session, + final HttpHeader httpHeader, + final HttpPacket entireHttpPacket) throws IOException { + + final HttpResponsePacket response = (HttpResponsePacket) httpHeader; + final Http2Stream stream = Http2Stream.getStreamFor(response); + assert stream != null; + + if (!response.isCommitted()) { + prepareOutgoingResponse(response); + } + + final FilterChainContext.TransportContext transportContext = ctx.getTransportContext(); + + stream.getOutputSink().writeDownStream(entireHttpPacket, + ctx, + transportContext.getCompletionHandler(), + transportContext.getMessageCloner()); + } + + private void doPush(final FilterChainContext ctx, final PushEvent pushEvent) { + final Http2Session http2Session = Http2Session.get(ctx.getConnection()); + if (http2Session == null) { + throw new IllegalStateException("Unable to find valid Http2Session"); + } + + try { + final HttpRequestPacket source = (HttpRequestPacket) pushEvent.getHttpRequest(); + Http2Stream parentStream = (Http2Stream) source.getAttribute(Http2Stream.HTTP2_PARENT_STREAM_ATTRIBUTE); + if (parentStream == null) { + parentStream = Http2Stream.getStreamFor(pushEvent.getHttpRequest()); + } + + if (parentStream == null) { + return; + } + final String eventPath = pushEvent.getPath(); + String path = eventPath; + String query = null; + final int idx = eventPath.indexOf('?'); + if (idx != -1) { + path = eventPath.substring(0, idx); + query = eventPath.substring(idx + 1); + } + final Http2Request request = Http2Request.create(); + request.setAttribute(Http2Stream.HTTP2_PARENT_STREAM_ATTRIBUTE, parentStream); + request.setConnection(ctx.getConnection()); + request.getRequestURIRef().init(path); + request.getQueryStringDC().setString(query); + request.setProtocol(Protocol.HTTP_2_0); + request.setMethod(pushEvent.getMethod()); + request.setSecure(pushEvent.getHttpRequest().isSecure()); + request.getHeaders().copyFrom(pushEvent.getHeaders()); + request.setExpectContent(false); + + prepareOutgoingRequest(request); + prepareOutgoingResponse(request.getResponse()); + final Http2Stream pushStream; + + http2Session.getNewClientStreamLock().lock(); + try { + pushStream = http2Session.openStream( + request, + http2Session.getNextLocalStreamId(), parentStream.getId(), + false, 0); + pushStream.inputBuffer.terminate(IN_FIN_TERMINATION); + + http2Session.getDeflaterLock().lock(); + try { + boolean logging = NetLogger.isActive(); + final Map capture = ((logging) ? new LinkedHashMap<>() : null); + List pushPromiseFrames = + http2Session.encodeHttpRequestAsPushPromiseFrames( + ctx, pushStream.getRequest(), parentStream.getId(), + pushStream.getId(), null, capture); + if (logging) { + for (Http2Frame http2Frame : pushPromiseFrames) { + if (http2Frame.getType() == PushPromiseFrame.TYPE) { + NetLogger.log(Context.TX, http2Session, (PushPromiseFrame) http2Frame, capture); + break; + } + } + } + http2Session.getOutputSink().writeDownStream(pushPromiseFrames); + + } finally { + pushStream.onSendPushPromise(); + http2Session.getDeflaterLock().unlock(); + } + } finally { + http2Session.getNewClientStreamLock().unlock(); + } + + request.getProcessingState().setHttpContext( + HttpContext.newInstance(pushStream, pushStream, pushStream, request)); + // now send the request upstream... + + submit(ctx.getConnection(), new Runnable() { + @Override + public void run() { + http2Session.sendMessageUpstream(pushStream, + HttpContent + .builder(request) + .content(Buffers.EMPTY_BUFFER) + .build()); + } + }); + + + } catch (Exception e) { + LOGGER.log(Level.SEVERE, + "Unable to push resource identified by path [{0}]", pushEvent.getPath()); + LOGGER.log(Level.SEVERE, e.getMessage(), e); + } finally { + pushEvent.recycle(); + ctx.resume(ctx.getStopAction()); + } + + + } + + private void submit(final Connection c, final Runnable runnable) { + if (threadPool != null) { + threadPool.submit(runnable); + } else { + final Transport t = c.getTransport(); + final ExecutorService workerThreadPool = t.getWorkerThreadPool(); + if (workerThreadPool != null) { + workerThreadPool.submit(runnable); + } else { + t.getKernelThreadPool().submit(runnable); + } + } + } + + private void prepareOutgoingResponse(final HttpResponsePacket response) { + response.setProtocol(Protocol.HTTP_2_0); + + String contentType = response.getContentType(); + if (contentType != null) { + response.getHeaders().setValue(Header.ContentType).setString(contentType); + } + + if (response.getContentLength() != -1) { + // FixedLengthTransferEncoding will set proper Content-Length header + FIXED_LENGTH_ENCODING.prepareSerialize(null, response, null); + } + + if (!response.containsHeader(Header.Date)) { + response.getHeaders().addValue(Header.Date) + .setBytes(FastHttpDateFormat.getCurrentDateBytes()); + } + } + + private void enableOpReadNow(final FilterChainContext ctx) { + // make sure we won't enable OP_READ once upper layer complete HTTP request processing + final FilterChainContext newContext = ctx.copy(); + ctx.getInternalContext().removeAllLifeCycleListeners(); + + // enable read now to start accepting HTTP2 frames + newContext.resume(newContext.getStopAction()); + } + +} diff --git a/launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/ssl/SSLBaseFilter.java b/launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/ssl/SSLBaseFilter.java new file mode 100644 index 0000000..0af0e3e --- /dev/null +++ b/launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/ssl/SSLBaseFilter.java @@ -0,0 +1,1201 @@ +/* + * Copyright (c) 2012, 2017 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019 Fujitsu Limited. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.grizzly.ssl; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Filter; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLEngineResult.Status; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; +import org.glassfish.grizzly.Buffer; +import org.glassfish.grizzly.CompletionHandler; +import org.glassfish.grizzly.Connection; +import org.glassfish.grizzly.Context; +import org.glassfish.grizzly.FileTransfer; +import org.glassfish.grizzly.Grizzly; +import org.glassfish.grizzly.GrizzlyFuture; +import org.glassfish.grizzly.IOEvent; +import org.glassfish.grizzly.IOEventLifeCycleListener.Adapter; +import org.glassfish.grizzly.ProcessorExecutor; +import org.glassfish.grizzly.ReadResult; +import org.glassfish.grizzly.Transport; +import org.glassfish.grizzly.asyncqueue.MessageCloner; +import org.glassfish.grizzly.filterchain.BaseFilter; +import org.glassfish.grizzly.filterchain.FilterChain; +import org.glassfish.grizzly.filterchain.FilterChainContext; +import org.glassfish.grizzly.filterchain.FilterChainContext.Operation; +import org.glassfish.grizzly.filterchain.FilterChainContext.TransportContext; +import org.glassfish.grizzly.filterchain.FilterChainEvent; +import org.glassfish.grizzly.filterchain.NextAction; +import org.glassfish.grizzly.filterchain.TransportFilter; +import org.glassfish.grizzly.impl.FutureImpl; +import org.glassfish.grizzly.memory.Buffers; +import org.glassfish.grizzly.memory.CompositeBuffer; +import org.glassfish.grizzly.memory.MemoryManager; +import org.glassfish.grizzly.ssl.SSLConnectionContext.Allocator; +import org.glassfish.grizzly.ssl.SSLConnectionContext.SslResult; +import org.glassfish.grizzly.utils.Futures; + +import static org.glassfish.grizzly.ssl.SSLUtils.*; + +/** + * SSL {@link Filter} to operate with SSL encrypted data. + * + * @author Alexey Stashok + */ +public class SSLBaseFilter extends BaseFilter { + private static final Logger LOGGER = Grizzly.logger(SSLBaseFilter.class); + protected static final MessageCloner COPY_CLONER = new OnWriteCopyCloner(); + + private static final Allocator MM_ALLOCATOR = new Allocator() { + @Override + @SuppressWarnings("unchecked") + public Buffer grow(final SSLConnectionContext sslCtx, + final Buffer oldBuffer, final int newSize) { + final MemoryManager mm = sslCtx.getConnection().getMemoryManager(); + + return oldBuffer == null ? + mm.allocate(newSize) : + mm.reallocate(oldBuffer, newSize); + } + }; + + private static final Allocator OUTPUT_BUFFER_ALLOCATOR = + new Allocator() { + @Override + public Buffer grow(final SSLConnectionContext sslCtx, + final Buffer oldBuffer, final int newSize) { + + return allocateOutputBuffer(newSize); + } + }; + + private final SSLEngineConfigurator serverSSLEngineConfigurator; + private final boolean renegotiateOnClientAuthWant; + private volatile boolean renegotiationDisabled; + + protected final Set handshakeListeners = + Collections.newSetFromMap(new ConcurrentHashMap<>(2)); + + private long handshakeTimeoutMillis = -1; + + private SSLTransportFilterWrapper optimizedTransportFilter; + + // ------------------------------------------------------------ Constructors + + + public SSLBaseFilter() { + this(null); + } + + /** + * Build SSLFilter with the given {@link SSLEngineConfigurator}. + * + * @param serverSSLEngineConfigurator SSLEngine configurator for server side connections + */ + public SSLBaseFilter(SSLEngineConfigurator serverSSLEngineConfigurator) { + this(serverSSLEngineConfigurator, true); + } + + + /** + * Build SSLFilter with the given {@link SSLEngineConfigurator}. + * + * @param serverSSLEngineConfigurator SSLEngine configurator for server side connections + * @param renegotiateOnClientAuthWant true, if SSLBaseFilter has to force client authentication + * during re-handshake, in case the client didn't send its credentials + * during the initial handshake in response to "wantClientAuth" flag. + * In this case "needClientAuth" flag will be raised and re-handshake + * will be initiated + */ + public SSLBaseFilter(SSLEngineConfigurator serverSSLEngineConfigurator, + boolean renegotiateOnClientAuthWant) { + + this.renegotiateOnClientAuthWant = renegotiateOnClientAuthWant; + this.serverSSLEngineConfigurator = + ((serverSSLEngineConfigurator != null) + ? serverSSLEngineConfigurator + : new SSLEngineConfigurator( + SSLContextConfigurator.DEFAULT_CONFIG.createSSLContext(true), + false, + false, + false)); + } + + /** + * @return true, if SSLBaseFilter has to force client authentication + * during re-handshake, in case the client didn't send its credentials + * during the initial handshake in response to "wantClientAuth" flag. + * In this case "needClientAuth" flag will be raised and re-handshake + * will be initiated + */ + public boolean isRenegotiateOnClientAuthWant() { + return renegotiateOnClientAuthWant; + } + + /** + * @return {@link SSLEngineConfigurator} used by the filter to create new + * {@link SSLEngine} for server-side {@link Connection}s + */ + public SSLEngineConfigurator getServerSSLEngineConfigurator() { + return serverSSLEngineConfigurator; + } + + public void addHandshakeListener(final HandshakeListener listener) { + handshakeListeners.add(listener); + } + + @SuppressWarnings("unused") + public void removeHandshakeListener(final HandshakeListener listener) { + handshakeListeners.remove(listener); + } + + /** + * @param timeUnit {@link TimeUnit} + * @return the handshake timeout, -1 if blocking handshake mode + * is disabled (default). + */ + @SuppressWarnings("unused") + public long getHandshakeTimeout(final TimeUnit timeUnit) { + if (handshakeTimeoutMillis < 0) { + return -1; + } + + return timeUnit.convert(handshakeTimeoutMillis, TimeUnit.MILLISECONDS); + } + + /** + * Sets the handshake timeout. + * @param handshakeTimeout timeout value, or -1 means for + * non-blocking handshake mode. + * @param timeUnit {@link TimeUnit} + */ + @SuppressWarnings("unused") + public void setHandshakeTimeout(final long handshakeTimeout, + final TimeUnit timeUnit) { + if (handshakeTimeout < 0) { + handshakeTimeoutMillis = -1; + } else { + this.handshakeTimeoutMillis = + TimeUnit.MILLISECONDS.convert(handshakeTimeout, timeUnit); + } + } + + /** + * Completely disables renegotiation. + * + * @param renegotiationDisabled true to disable renegotiation. + */ + public void setRenegotiationDisabled(boolean renegotiationDisabled) { + this.renegotiationDisabled = renegotiationDisabled; + } + + protected SSLTransportFilterWrapper getOptimizedTransportFilter( + final TransportFilter childFilter) { + if (optimizedTransportFilter == null || + optimizedTransportFilter.wrappedFilter != childFilter) { + optimizedTransportFilter = createOptimizedTransportFilter(childFilter); + } + + return optimizedTransportFilter; + } + + protected SSLTransportFilterWrapper createOptimizedTransportFilter( + final TransportFilter childFilter) { + return new SSLTransportFilterWrapper(childFilter, this); + } + + @Override + public void onRemoved(final FilterChain filterChain) { + if (optimizedTransportFilter != null) { + final int sslTransportFilterIdx = filterChain.indexOf(optimizedTransportFilter); + if (sslTransportFilterIdx >= 0) { + SSLTransportFilterWrapper wrapper = + (SSLTransportFilterWrapper) filterChain.get(sslTransportFilterIdx); + filterChain.set(sslTransportFilterIdx, wrapper.wrappedFilter); + } + } + } + + @Override + public void onAdded(FilterChain filterChain) { + final int sslTransportFilterIdx = + filterChain.indexOfType(SSLTransportFilterWrapper.class); + + if (sslTransportFilterIdx == -1) { + final int transportFilterIdx = + filterChain.indexOfType(TransportFilter.class); + if (transportFilterIdx >= 0) { + filterChain.set(transportFilterIdx, + getOptimizedTransportFilter( + (TransportFilter) filterChain.get(transportFilterIdx))); + } + } + } + + // ----------------------------------------------------- Methods from Filter + + + @Override + public NextAction handleEvent(final FilterChainContext ctx, + final FilterChainEvent event) + throws IOException { + if (event.type() == CertificateEvent.TYPE) { + final CertificateEvent ce = (CertificateEvent) event; + try { + return ctx.getSuspendAction(); + } finally { + getPeerCertificateChain(obtainSslConnectionContext(ctx.getConnection()), + ctx, + ce.needClientAuth, + ce.certsFuture); + } + } + return ctx.getInvokeAction(); + } + + @Override + public NextAction handleRead(final FilterChainContext ctx) + throws IOException { + final Connection connection = ctx.getConnection(); + final SSLConnectionContext sslCtx = obtainSslConnectionContext(connection); + SSLEngine sslEngine = sslCtx.getSslEngine(); + + if (sslEngine != null && !isHandshaking(sslEngine)) { + return unwrapAll(ctx, sslCtx); + } else { + if (sslEngine == null) { + sslEngine = serverSSLEngineConfigurator.createSSLEngine(); + sslEngine.beginHandshake(); + sslCtx.configure(sslEngine); + notifyHandshakeStart(connection); + } + + final Buffer buffer; + buffer = ((handshakeTimeoutMillis >= 0) + ? doHandshakeSync(sslCtx, + ctx, + (Buffer) ctx.getMessage(), + handshakeTimeoutMillis) + : makeInputRemainder(sslCtx, + ctx, + doHandshakeStep(sslCtx, + ctx, + (Buffer) ctx.getMessage()))); + + final boolean hasRemaining = buffer != null && buffer.hasRemaining(); + + final boolean isHandshaking = isHandshaking(sslEngine); + if (!isHandshaking) { + notifyHandshakeComplete(connection, sslEngine); + final FilterChain connectionFilterChain = sslCtx.getNewConnectionFilterChain(); + sslCtx.setNewConnectionFilterChain(null); + if (connectionFilterChain != null) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, "Applying new FilterChain after" + + "SSLHandshake. Connection={0} filterchain={1}", + new Object[]{connection, connectionFilterChain}); + } + + connection.setProcessor(connectionFilterChain); + + if (hasRemaining) { + NextAction suspendAction = ctx.getSuspendAction(); + ctx.setMessage(buffer); + ctx.suspend(); + final FilterChainContext newContext = + obtainProtocolChainContext(ctx, connectionFilterChain); + ProcessorExecutor.execute(newContext.getInternalContext()); + return suspendAction; + } else { + return ctx.getStopAction(); + } + } + + if (hasRemaining) { + ctx.setMessage(buffer); + return unwrapAll(ctx, sslCtx); + } + } + + return ctx.getStopAction(buffer); + } + } + + @SuppressWarnings("unchecked") + @Override + public NextAction handleWrite(final FilterChainContext ctx) throws IOException { + if (ctx.getMessage() instanceof FileTransfer) { + throw new IllegalStateException("TLS operations not supported with SendFile messages"); + } + + final Connection connection = ctx.getConnection(); + + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized(connection) { + final Buffer output = + wrapAll(ctx, obtainSslConnectionContext(connection)); + + final TransportContext transportContext = + ctx.getTransportContext(); + + ctx.write(null, output, + transportContext.getCompletionHandler(), + transportContext.getPushBackHandler(), + COPY_CLONER, + transportContext.isBlocking()); + + return ctx.getStopAction(); + } + } + + // ------------------------------------------------------- Protected Methods + + protected NextAction unwrapAll(final FilterChainContext ctx, + final SSLConnectionContext sslCtx) throws SSLException { + Buffer input = ctx.getMessage(); + + Buffer output = null; + + boolean isClosed = false; + + _outter: + do { + final int len = getSSLPacketSize(input); + + if (len == -1 || input.remaining() < len) { + break; + } + + final SslResult result = + sslCtx.unwrap(len, input, output, MM_ALLOCATOR); + + output = result.getOutput(); + + if (result.isError()) { + output.dispose(); + throw result.getError(); + } + + if (isHandshaking(sslCtx.getSslEngine())) { + // is it re-handshake or graceful ssl termination + if (result.getSslEngineResult().getStatus() != Status.CLOSED) { + input = rehandshake(ctx, sslCtx); + } else { + input = silentRehandshake(ctx, sslCtx); // silent SSL termination + isClosed = true; + } + + if (input == null) { + break; + } + } + + switch (result.getSslEngineResult().getStatus()) { + case OK: + if (input.hasRemaining()) { + break; + } + + break _outter; + case CLOSED: + isClosed = true; + break _outter; + default: + // Should not reach this point + throw new IllegalStateException("Unexpected status: " + + result.getSslEngineResult().getStatus()); + } + } while (true); + + if (output != null) { + output.trim(); + + if (output.hasRemaining() || isClosed) { + ctx.setMessage(output); + return ctx.getInvokeAction(makeInputRemainder(sslCtx, ctx, input)); + } + } + + return ctx.getStopAction(makeInputRemainder(sslCtx, ctx, input)); + } + + @SuppressWarnings("MethodMayBeStatic") + protected Buffer wrapAll(final FilterChainContext ctx, + final SSLConnectionContext sslCtx) throws SSLException { + + final Buffer input = ctx.getMessage(); + + final Buffer output = sslCtx.wrapAll(input, OUTPUT_BUFFER_ALLOCATOR); + + input.tryDispose(); + + return output; + } + +// protected Buffer doHandshakeStep1(final SSLConnectionContext sslCtx, +// final FilterChainContext ctx, +// Buffer inputBuffer) +// throws IOException { +// +// final SSLEngine sslEngine = sslCtx.getSslEngine(); +// final Connection connection = ctx.getConnection(); +// +// final boolean isLoggingFinest = LOGGER.isLoggable(Level.FINEST); +// try { +// HandshakeStatus handshakeStatus = sslEngine.getHandshakeStatus(); +// +// while (true) { +// +// if (isLoggingFinest) { +// LOGGER.log(Level.FINEST, "Loop Engine: {0} handshakeStatus={1}", +// new Object[]{sslEngine, sslEngine.getHandshakeStatus()}); +// } +// +// switch (handshakeStatus) { +// case NEED_UNWRAP: { +// +// if (isLoggingFinest) { +// LOGGER.log(Level.FINEST, "NEED_UNWRAP Engine: {0}", sslEngine); +// } +// +// if (inputBuffer == null || !inputBuffer.hasRemaining()) { +// return inputBuffer; +// } +// +// final int expectedLength = getSSLPacketSize(inputBuffer); +// if (expectedLength == -1 +// || inputBuffer.remaining() < expectedLength) { +// return inputBuffer; +// } +// +// final SSLEngineResult sslEngineResult = +// handshakeUnwrap(connection, sslCtx, inputBuffer, null); +// +// inputBuffer.shrink(); +// +// final SSLEngineResult.Status status = sslEngineResult.getStatus(); +// +// if (status == SSLEngineResult.Status.BUFFER_UNDERFLOW || +// status == SSLEngineResult.Status.BUFFER_OVERFLOW) { +// throw new SSLException("SSL unwrap error: " + status); +// } +// +// handshakeStatus = sslEngine.getHandshakeStatus(); +// break; +// } +// +// case NEED_WRAP: { +// if (isLoggingFinest) { +// LOGGER.log(Level.FINEST, "NEED_WRAP Engine: {0}", sslEngine); +// } +// +// inputBuffer = makeInputRemainder(sslCtx, ctx, inputBuffer); +// final Buffer buffer = handshakeWrap(connection, sslCtx, null); +// +// try { +// ctx.write(buffer); +// +// handshakeStatus = sslEngine.getHandshakeStatus(); +// } catch (Exception e) { +// buffer.dispose(); +// throw new IOException("Unexpected exception", e); +// } +// +// break; +// } +// +// case NEED_TASK: { +// if (isLoggingFinest) { +// LOGGER.log(Level.FINEST, "NEED_TASK Engine: {0}", sslEngine); +// } +// executeDelegatedTask(sslEngine); +// handshakeStatus = sslEngine.getHandshakeStatus(); +// break; +// } +// +// case FINISHED: +// case NOT_HANDSHAKING: { +// return inputBuffer; +// } +// } +// +// if (handshakeStatus == HandshakeStatus.FINISHED) { +// return inputBuffer; +// } +// } +// } catch (IOException ioe) { +// notifyHandshakeFailed(connection, ioe); +// throw ioe; +// } +// } + + protected Buffer doHandshakeSync(final SSLConnectionContext sslCtx, + final FilterChainContext ctx, + Buffer inputBuffer, + final long timeoutMillis) throws IOException { + + final Connection connection = ctx.getConnection(); + final SSLEngine sslEngine = sslCtx.getSslEngine(); + + final Buffer tmpAppBuffer = allocateOutputBuffer(sslCtx.getAppBufferSize()); + + final long oldReadTimeout = connection.getReadTimeout(TimeUnit.MILLISECONDS); + + try { + connection.setReadTimeout(timeoutMillis, TimeUnit.MILLISECONDS); + + inputBuffer = makeInputRemainder(sslCtx, ctx, + doHandshakeStep(sslCtx, ctx, inputBuffer, tmpAppBuffer)); + + while (isHandshaking(sslEngine)) { + final ReadResult rr = ctx.read(); + final Buffer newBuf = (Buffer) rr.getMessage(); + inputBuffer = Buffers.appendBuffers(ctx.getMemoryManager(), + inputBuffer, newBuf); + inputBuffer = makeInputRemainder(sslCtx, ctx, + doHandshakeStep(sslCtx, ctx, inputBuffer, tmpAppBuffer)); + } + } finally { + tmpAppBuffer.dispose(); + connection.setReadTimeout(oldReadTimeout, TimeUnit.MILLISECONDS); + } + + return inputBuffer; + } + + protected Buffer doHandshakeStep(final SSLConnectionContext sslCtx, + final FilterChainContext ctx, + Buffer inputBuffer) throws IOException { + return doHandshakeStep(sslCtx, ctx, inputBuffer, null); + } + + protected Buffer doHandshakeStep(final SSLConnectionContext sslCtx, + final FilterChainContext ctx, + Buffer inputBuffer, + final Buffer tmpAppBuffer0) + throws IOException { + + final Connection connection = ctx.getConnection(); + + final boolean isLoggingFinest = LOGGER.isLoggable(Level.FINEST); + Buffer tmpInputToDispose = null; + Buffer tmpNetBuffer = null; + + Buffer tmpAppBuffer = tmpAppBuffer0; + + try { + HandshakeStatus handshakeStatus = sslCtx.getSslEngine().getHandshakeStatus(); + + _exitWhile: + + while (true) { + + if (isLoggingFinest) { + LOGGER.log(Level.FINEST, "Loop Engine: {0} handshakeStatus={1}", + new Object[] { sslCtx.getSslEngine(), sslCtx.getSslEngine().getHandshakeStatus() }); + } + + switch (handshakeStatus) { + case NEED_UNWRAP: { + + if (isLoggingFinest) { + LOGGER.log(Level.FINEST, "NEED_UNWRAP Engine: {0}", sslCtx.getSslEngine()); + } + + if (inputBuffer == null || !inputBuffer.hasRemaining()) { + break _exitWhile; + } + + final int expectedLength = getSSLPacketSize(inputBuffer); + if (expectedLength == -1 + || inputBuffer.remaining() < expectedLength) { + break _exitWhile; + } + + if (tmpAppBuffer == null) { + tmpAppBuffer = allocateOutputBuffer(sslCtx.getAppBufferSize()); + } + + final SSLEngineResult sslEngineResult = + handshakeUnwrap(expectedLength, sslCtx, inputBuffer, tmpAppBuffer); + + if (!inputBuffer.hasRemaining()) { + tmpInputToDispose = inputBuffer; + inputBuffer = null; + } + + final Status status = sslEngineResult.getStatus(); + + if (status == Status.BUFFER_UNDERFLOW || + status == Status.BUFFER_OVERFLOW) { + throw new SSLException("SSL unwrap error: " + status); + } + + handshakeStatus = sslCtx.getSslEngine().getHandshakeStatus(); + break; + } + + case NEED_WRAP: { + if (isLoggingFinest) { + LOGGER.log(Level.FINEST, "NEED_WRAP Engine: {0}", sslCtx.getSslEngine()); + } + + tmpNetBuffer = handshakeWrap( + connection, sslCtx, tmpNetBuffer); + handshakeStatus = sslCtx.getSslEngine().getHandshakeStatus(); + + break; + } + + case NEED_TASK: { + if (isLoggingFinest) { + LOGGER.log(Level.FINEST, "NEED_TASK Engine: {0}", sslCtx.getSslEngine()); + } + executeDelegatedTask(sslCtx.getSslEngine()); + handshakeStatus = sslCtx.getSslEngine().getHandshakeStatus(); + break; + } + + case FINISHED: + case NOT_HANDSHAKING: { + break _exitWhile; + } + } + + if (handshakeStatus == HandshakeStatus.FINISHED) { + break; + } + } + } catch (IOException ioe) { + notifyHandshakeFailed(connection, ioe); + throw ioe; + } finally { + if (tmpAppBuffer0 == null && tmpAppBuffer != null) { + tmpAppBuffer.dispose(); + } + + if (tmpInputToDispose != null) { + tmpInputToDispose.tryDispose(); + inputBuffer = null; + } else if (inputBuffer != null) { + inputBuffer.shrink(); + } + + if (tmpNetBuffer != null) { + if (inputBuffer != null) { + inputBuffer = makeInputRemainder(sslCtx, ctx, inputBuffer); + } + + ctx.write(tmpNetBuffer); + } + } + + return inputBuffer; + } + + /** + * Performs an SSL renegotiation. + * + * @param sslCtx the {@link SSLConnectionContext} associated with this + * this renegotiation request. + * @param context the {@link FilterChainContext} associated with this + * this renegotiation request. + * + * @throws IOException if an error occurs during SSL renegotiation. + */ + protected void renegotiate(final SSLConnectionContext sslCtx, + final FilterChainContext context) + throws IOException { + + if (renegotiationDisabled) { + return; + } + final SSLEngine sslEngine = sslCtx.getSslEngine(); + if (sslEngine.getWantClientAuth() && !renegotiateOnClientAuthWant) { + return; + } + final boolean authConfigured = + (sslEngine.getWantClientAuth() + || sslEngine.getNeedClientAuth()); + if (!authConfigured) { + sslEngine.setNeedClientAuth(true); + } + + sslEngine.getSession().invalidate(); + + try { + sslEngine.beginHandshake(); + } catch (SSLHandshakeException e) { + // If we catch SSLHandshakeException at this point it may be due + // to an older SSL peer that hasn't made its SSL/TLS renegotiation + // secure. This will be the case with Oracle's VM older than + // 1.6.0_22 or native applications using OpenSSL libraries + // older than 0.9.8m. + // + // What we're trying to accomplish here is an attempt to detect + // this issue and log a useful message for the end user instead + // of an obscure exception stack trace in the server's log. + // Note that this probably will only work on Oracle's VM. + if (e.toString().toLowerCase().contains("insecure renegotiation")) { + if (LOGGER.isLoggable(Level.SEVERE)) { + LOGGER.severe("Secure SSL/TLS renegotiation is not " + + "supported by the peer. This is most likely due" + + " to the peer using an older SSL/TLS " + + "implementation that does not implement RFC 5746."); + } + // we could return null here and let the caller + // decided what to do, but since the SSLEngine will + // close the channel making further actions useless, + // we'll report the entire cause. + } + throw e; + } + + try { + rehandshake(context, sslCtx); + } finally { + if (!authConfigured) { + sslEngine.setNeedClientAuth(false); + } + } + } + + private Buffer silentRehandshake(final FilterChainContext context, + final SSLConnectionContext sslCtx) throws SSLException { + try { + return doHandshakeSync( + sslCtx, context, null, handshakeTimeoutMillis); + } catch (Throwable t) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, "Error during graceful ssl connection close", t); + } + + if (t instanceof SSLException) { + throw (SSLException) t; + } + + throw new SSLException("Error during re-handshaking", t); + } + } + + private Buffer rehandshake(final FilterChainContext context, + final SSLConnectionContext sslCtx) throws SSLException { + final Connection c = context.getConnection(); + + notifyHandshakeStart(c); + + try { + final Buffer buffer = doHandshakeSync( + sslCtx, context, null, handshakeTimeoutMillis); + + notifyHandshakeComplete(c, sslCtx.getSslEngine()); + + return buffer; + } catch (Throwable t) { + notifyHandshakeFailed(c, t); + + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, "Error during re-handshaking", t); + } + + if (t instanceof SSLException) { + throw (SSLException) t; + } + + throw new SSLException("Error during re-handshaking", t); + } + } + + /** + *

+ * Obtains the certificate chain for this SSL session. If no certificates + * are available, and needClientAuth is true, an SSL renegotiation + * will be be triggered to request the certificates from the client. + *

+ * + * @param sslCtx the {@link SSLConnectionContext} associated with this + * certificate request. + * @param context the {@link FilterChainContext} associated with this + * this certificate request. + * @param needClientAuth determines whether or not SSL renegotiation will + * be attempted to obtain the certificate chain. + * @param certFuture the future that will be provided the result of the + * peer certificate processing. + */ + protected void getPeerCertificateChain(final SSLConnectionContext sslCtx, + final FilterChainContext context, + final boolean needClientAuth, + final FutureImpl certFuture) { + + Certificate[] certs = getPeerCertificates(sslCtx); + if (certs != null) { + certFuture.result(certs); + return; + } + + if (needClientAuth) { + final Transport transport = context.getConnection().getTransport(); + ExecutorService threadPool = transport.getWorkerThreadPool(); + if (threadPool == null) { + threadPool = transport.getKernelThreadPool(); + } + threadPool.submit(new Runnable() { + @Override + public void run() { + try { + try { + renegotiate(sslCtx, context); + } catch (IOException ioe) { + certFuture.failure(ioe); + return; + } + Certificate[] certs = getPeerCertificates(sslCtx); + + if (certs == null) { + certFuture.result(null); + return; + } + + X509Certificate[] x509Certs = extractX509Certs(certs); + + if (x509Certs == null || x509Certs.length < 1) { + certFuture.result(null); + return; + } + + certFuture.result(x509Certs); + } finally { + context.resume(context.getStopAction()); + } + } + }); + } + } + + protected SSLConnectionContext obtainSslConnectionContext( + final Connection connection) { + SSLConnectionContext sslCtx = SSL_CTX_ATTR.get(connection); + if (sslCtx == null) { + sslCtx = createSslConnectionContext(connection); + SSL_CTX_ATTR.set(connection, sslCtx); + } + + return sslCtx; + } + + @SuppressWarnings("MethodMayBeStatic") + protected SSLConnectionContext createSslConnectionContext( + final Connection connection) { + return new SSLConnectionContext(connection); + } + + private static FilterChainContext obtainProtocolChainContext( + final FilterChainContext ctx, + final FilterChain completeProtocolFilterChain) { + + final FilterChainContext newFilterChainContext = + completeProtocolFilterChain.obtainFilterChainContext( + ctx.getConnection(), + ctx.getStartIdx(), + completeProtocolFilterChain.size(), + ctx.getFilterIdx()); + + newFilterChainContext.setAddressHolder(ctx.getAddressHolder()); + newFilterChainContext.setMessage(ctx.getMessage()); + newFilterChainContext.getInternalContext().setIoEvent(IOEvent.READ); + newFilterChainContext.getInternalContext().addLifeCycleListener( + new InternalProcessingHandler(ctx)); + + return newFilterChainContext; + } + + + // --------------------------------------------------------- Private Methods + + private static X509Certificate[] extractX509Certs(final Certificate[] certs) { + final X509Certificate[] x509Certs = new X509Certificate[certs.length]; + for(int i = 0, len = certs.length; i < len; i++) { + if( certs[i] instanceof X509Certificate ) { + x509Certs[i] = (X509Certificate)certs[i]; + } else { + try { + final byte [] buffer = certs[i].getEncoded(); + final CertificateFactory cf = + CertificateFactory.getInstance("X.509"); + ByteArrayInputStream stream = new ByteArrayInputStream(buffer); + x509Certs[i] = (X509Certificate) + cf.generateCertificate(stream); + } catch(Exception ex) { + LOGGER.log(Level.INFO, + "Error translating cert " + certs[i], + ex); + return null; + } + } + + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, "Cert #{0} = {1}", new Object[] {i, x509Certs[i]}); + } + } + return x509Certs; + } + + private static Certificate[] getPeerCertificates(final SSLConnectionContext sslCtx) { + try { + return sslCtx.getSslEngine().getSession().getPeerCertificates(); + } catch( Throwable t ) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE,"Error getting client certs", t); + } + return null; + } + } + + protected void notifyHandshakeStart(final Connection connection) { + if (!handshakeListeners.isEmpty()) { + for (final HandshakeListener listener : handshakeListeners) { + listener.onStart(connection); + } + } + } + + protected void notifyHandshakeComplete(final Connection connection, + final SSLEngine sslEngine) { + + if (!handshakeListeners.isEmpty()) { + for (final HandshakeListener listener : handshakeListeners) { + listener.onComplete(connection); + } + } + } + + protected void notifyHandshakeFailed(final Connection connection, + final Throwable t) { + if (!handshakeListeners.isEmpty()) { + for (final HandshakeListener listener : handshakeListeners) { + listener.onFailure(connection, t); + } + } + } + + // ----------------------------------------------------------- Inner Classes + + public static class CertificateEvent implements FilterChainEvent { + + static final String TYPE = "CERT_EVENT"; + + final FutureImpl certsFuture; + + final boolean needClientAuth; + + + // -------------------------------------------------------- Constructors + + + public CertificateEvent(final boolean needClientAuth) { + this.needClientAuth = needClientAuth; + certsFuture = Futures.createSafeFuture(); + } + + + // --------------------------------------- Methods from FilterChainEvent + + + @Override + public final Object type() { + return TYPE; + } + + + // ------------------------------------------------------ Public Methods + + + /** + * Invoke this method to trigger processing to abtain certificates from + * the remote peer. Do not fire this event down stream manually. + * + * Register a {@link CompletionHandler} with the returned + * {@link GrizzlyFuture} to be notified when the result is available + * to prevent blocking. + * + * @param ctx the current {@link FilterChainContext} + * + * @return a {@link GrizzlyFuture} representing the processing of + * the remote peer certificates. + */ + public GrizzlyFuture trigger(final FilterChainContext ctx) { + ctx.getFilterChain().fireEventDownstream(ctx.getConnection(), + this, + null); + return certsFuture; + } + + } // END CertificateEvent + + private static class InternalProcessingHandler extends Adapter { + private final FilterChainContext parentContext; + + private InternalProcessingHandler(final FilterChainContext parentContext) { + this.parentContext = parentContext; + } + + @Override + public void onComplete(final Context context, Object data) throws IOException { + parentContext.resume(parentContext.getStopAction()); + } + + } // END InternalProcessingHandler + + public interface HandshakeListener { + void onStart(Connection connection); + void onComplete(Connection connection); + void onFailure(Connection connection, Throwable t); + } + + protected static class SSLTransportFilterWrapper extends TransportFilter { + protected final TransportFilter wrappedFilter; + protected final SSLBaseFilter sslBaseFilter; + + public SSLTransportFilterWrapper(final TransportFilter transportFilter, + final SSLBaseFilter sslBaseFilter) { + this.wrappedFilter = transportFilter; + this.sslBaseFilter = sslBaseFilter; + } + + @Override + public NextAction handleAccept(FilterChainContext ctx) throws IOException { + return wrappedFilter.handleAccept(ctx); + } + + @Override + public NextAction handleConnect(FilterChainContext ctx) throws IOException { + return wrappedFilter.handleConnect(ctx); + } + + @Override + public NextAction handleRead(final FilterChainContext ctx) throws IOException { + final Connection connection = ctx.getConnection(); + final SSLConnectionContext sslCtx = + sslBaseFilter.obtainSslConnectionContext(connection); + + if (sslCtx.getSslEngine() == null) { + final SSLEngine sslEngine = sslBaseFilter.serverSSLEngineConfigurator.createSSLEngine(); + sslEngine.beginHandshake(); + sslCtx.configure(sslEngine); + sslBaseFilter.notifyHandshakeStart(connection); + } + + ctx.setMessage(allowDispose(allocateInputBuffer(sslCtx))); + + return wrappedFilter.handleRead(ctx); + } + + @Override + public NextAction handleWrite(FilterChainContext ctx) throws IOException { + return wrappedFilter.handleWrite(ctx); + } + + @Override + public NextAction handleEvent(FilterChainContext ctx, FilterChainEvent event) throws IOException { + return wrappedFilter.handleEvent(ctx, event); + } + + @Override + public NextAction handleClose(FilterChainContext ctx) throws IOException { + return wrappedFilter.handleClose(ctx); + } + + @Override + public void onAdded(FilterChain filterChain) { + wrappedFilter.onAdded(filterChain); + } + + @Override + public void onFilterChainChanged(FilterChain filterChain) { + wrappedFilter.onFilterChainChanged(filterChain); + } + + @Override + public void onRemoved(FilterChain filterChain) { + wrappedFilter.onRemoved(filterChain); + } + + @Override + public void exceptionOccurred(FilterChainContext ctx, Throwable error) { + wrappedFilter.exceptionOccurred(ctx, error); + } + + @Override + public FilterChainContext createContext(Connection connection, Operation operation) { + return wrappedFilter.createContext(connection, operation); + } + } + + private static final class OnWriteCopyCloner implements MessageCloner { + @Override + public Buffer clone(final Connection connection, + final Buffer originalMessage) { + final SSLConnectionContext sslCtx = getSslConnectionContext(connection); + + final int copyThreshold = sslCtx.getNetBufferSize() / 2; + + final Buffer lastOutputBuffer = sslCtx.resetLastOutputBuffer(); + + final int totalRemaining = originalMessage.remaining(); + + if (totalRemaining < copyThreshold) { + return move(connection.getMemoryManager(), + originalMessage); + } + if (lastOutputBuffer.remaining() < copyThreshold) { + final Buffer tmpBuf = + copy(connection.getMemoryManager(), + originalMessage); + + if (originalMessage.isComposite()) { + ((CompositeBuffer) originalMessage).replace( + lastOutputBuffer, tmpBuf); + } else { + assert originalMessage == lastOutputBuffer; + } + + lastOutputBuffer.tryDispose(); + + return tmpBuf; + } + + + return originalMessage; + } + } +} diff --git a/launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/ssl/SSLEngineConfigurator.java b/launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/ssl/SSLEngineConfigurator.java new file mode 100644 index 0000000..0359d29 --- /dev/null +++ b/launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/ssl/SSLEngineConfigurator.java @@ -0,0 +1,471 @@ +/* + * Copyright (c) 2008, 2017 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019 Fujitsu Limited. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.grizzly.ssl; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; + +import org.glassfish.grizzly.Grizzly; + +/** + * Utility class, which helps to configure {@link SSLEngine}. + * + * @author Alexey Stashok + */ +public class SSLEngineConfigurator implements SSLEngineFactory { + private static final Logger LOGGER = Grizzly.logger(SSLEngineConfigurator.class); + + private static final double JAVA_VERSION = Double.parseDouble(System.getProperty("java.specification.version")); + + private final Object sync = new Object(); + + protected volatile SSLContextConfigurator sslContextConfiguration; + + protected volatile SSLContext sslContext; + + /** + * The list of cipher suite + */ + protected String[] enabledCipherSuites = null; + /** + * the list of protocols + */ + protected String[] enabledProtocols = null; + /** + * Client mode when handshaking. + */ + protected boolean clientMode; + /** + * Require client Authentication. + */ + protected boolean needClientAuth; + /** + * True when requesting authentication. + */ + protected boolean wantClientAuth; + /** + * Has the enabled protocol configured. + */ + private boolean isProtocolConfigured = false; + /** + * Has the enabled Cipher configured. + */ + private boolean isCipherConfigured = false; + + /** + * Create SSL Engine configuration basing on passed {@link SSLContext}. + * + * @param sslContext {@link SSLContext}. + */ + public SSLEngineConfigurator(SSLContext sslContext) { + this(sslContext, true, false, false); + } + + /** + * Create SSL Engine configuration basing on passed {@link SSLContext}, + * using passed client mode, need/want client auth parameters. + * + * @param sslContext {@link SSLContext}. + * @param clientMode + * @param needClientAuth + * @param wantClientAuth + */ + public SSLEngineConfigurator(final SSLContext sslContext, + final boolean clientMode, final boolean needClientAuth, + final boolean wantClientAuth) { + if (sslContext == null) + throw new IllegalArgumentException("SSLContext can not be null"); + + this.sslContextConfiguration = null; + this.sslContext = sslContext; + this.clientMode = clientMode; + this.needClientAuth = needClientAuth; + this.wantClientAuth = wantClientAuth; + } + + /** + * Create SSL Engine configuration basing on passed {@link SSLContextConfigurator}. + * This constructor makes possible to initialize SSLEngine and SSLContext in lazy + * fashion on first {@link #createSSLEngine()} call. + * + * @param sslContextConfiguration {@link SSLContextConfigurator}. + */ + public SSLEngineConfigurator(SSLContextConfigurator sslContextConfiguration) { + this(sslContextConfiguration, true, false, false); + } + + /** + * Create SSL Engine configuration basing on passed {@link SSLContextConfigurator}. + * This constructor makes possible to initialize SSLEngine and SSLContext in lazy + * fashion on first {@link #createSSLEngine()} call. + * + * @param sslContextConfiguration {@link SSLContextConfigurator}. + * @param clientMode + * @param needClientAuth + * @param wantClientAuth + */ + public SSLEngineConfigurator(SSLContextConfigurator sslContextConfiguration, + boolean clientMode, + boolean needClientAuth, boolean wantClientAuth) { + + if (sslContextConfiguration == null) + throw new IllegalArgumentException("SSLContextConfigurator can not be null"); + + this.sslContextConfiguration = sslContextConfiguration; + this.clientMode = clientMode; + this.needClientAuth = needClientAuth; + this.wantClientAuth = wantClientAuth; + } + + public SSLEngineConfigurator(SSLEngineConfigurator pattern) { + this.sslContextConfiguration = pattern.sslContextConfiguration; + this.sslContext = pattern.sslContext; + this.clientMode = pattern.clientMode; + this.needClientAuth = pattern.needClientAuth; + this.wantClientAuth = pattern.wantClientAuth; + + this.enabledCipherSuites = pattern.enabledCipherSuites != null + ? Arrays.copyOf(pattern.enabledCipherSuites, pattern.enabledCipherSuites.length) + : null; + + this.enabledProtocols = pattern.enabledProtocols != null + ? Arrays.copyOf(pattern.enabledProtocols, pattern.enabledProtocols.length) + : null; + + this.isCipherConfigured = pattern.isCipherConfigured; + this.isProtocolConfigured = pattern.isProtocolConfigured; + } + + protected SSLEngineConfigurator() { + } + + /** + * Create and configure {@link SSLEngine} using this context configuration. + * + * @return {@link SSLEngine}. + */ + public SSLEngine createSSLEngine() { + return createSSLEngine(null, -1); + } + + /** + * Create and configure {@link SSLEngine} using this context configuration + * using advisory peer information. + *

+ * Applications using this factory method are providing hints + * for an internal session reuse strategy. + *

+ * Some cipher suites (such as Kerberos) require remote hostname + * information, in which case peerHost needs to be specified. + * + * @param peerHost the non-authoritative name of the host + * @param peerPort the non-authoritative port + * + * @return {@link SSLEngine}. + */ + @Override + public SSLEngine createSSLEngine(final String peerHost, final int peerPort) { + if (sslContext == null) { + synchronized(sync) { + if (sslContext == null) { + sslContext = sslContextConfiguration.createSSLContext(true); + } + } + } + + final SSLEngine sslEngine = sslContext.createSSLEngine(peerHost, peerPort); + configure(sslEngine); + + return sslEngine; + } + + /** + * Configure passed {@link SSLEngine}, using current configurator settings + * + * @param sslEngine {@link SSLEngine} to configure. + * @return configured {@link SSLEngine}. + */ + public SSLEngine configure(final SSLEngine sslEngine) { + if (enabledCipherSuites != null) { + if (!isCipherConfigured) { + enabledCipherSuites = configureEnabledCiphers(sslEngine, + enabledCipherSuites); + isCipherConfigured = true; + } + sslEngine.setEnabledCipherSuites(enabledCipherSuites); + } + + if (enabledProtocols != null) { + if (!isProtocolConfigured) { + enabledProtocols = + configureEnabledProtocols(sslEngine, + enabledProtocols); + isProtocolConfigured = true; + } + sslEngine.setEnabledProtocols(enabledProtocols); + } + + sslEngine.setUseClientMode(clientMode); + if (wantClientAuth) { + sslEngine.setWantClientAuth(wantClientAuth); + } + if (needClientAuth) { + sslEngine.setNeedClientAuth(needClientAuth); + } + + if (JAVA_VERSION > 1.8) { + try { + String[] enabledApplicationProtocols = new String[] { "h2", "http/1.1" }; + SSLParameters sslParameters = sslEngine.getSSLParameters(); + Method setApplicationProtocols = + SSLParameters.class.getDeclaredMethod("setApplicationProtocols", String[].class); + setApplicationProtocols.invoke(sslParameters, (Object) enabledApplicationProtocols); + sslEngine.setSSLParameters(sslParameters); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException + | NoSuchMethodException | SecurityException e) { + LOGGER.log(Level.WARNING, + "Failed to configure application protocols. HTTP/2 will not be available.", e); + } + } + + return sslEngine; + } + + /** + * Will {@link SSLEngine} be configured to work in client mode. + * + * @return true, if {@link SSLEngine} will be configured to work + * in client mode, or false for server mode. + */ + public boolean isClientMode() { + return clientMode; + } + + /** + * Set {@link SSLEngine} to be configured to work in client mode. + * + * @param clientMode true, if {@link SSLEngine} will be configured + * to work in client mode, or false for server + * mode. + * @return this SSLEngineConfigurator + */ + public SSLEngineConfigurator setClientMode(boolean clientMode) { + this.clientMode = clientMode; + return this; + } + + + public boolean isNeedClientAuth() { + return needClientAuth; + } + + public SSLEngineConfigurator setNeedClientAuth(boolean needClientAuth) { + this.needClientAuth = needClientAuth; + return this; + } + + public boolean isWantClientAuth() { + return wantClientAuth; + } + + public SSLEngineConfigurator setWantClientAuth(boolean wantClientAuth) { + this.wantClientAuth = wantClientAuth; + return this; + } + + /** + * @return an array of enabled cipher suites. Modifications made on the array + * content won't be propagated to SSLEngineConfigurator + */ + public String[] getEnabledCipherSuites() { + return enabledCipherSuites != null + ? Arrays.copyOf(enabledCipherSuites, enabledCipherSuites.length) + : null; + } + + /** + * Sets a list of enabled cipher suites. + * Note: further modifications made on the passed array won't be propagated + * to SSLEngineConfigurator. + * + * @param enabledCipherSuites list of enabled cipher suites + * @return this SSLEngineConfigurator + */ + public SSLEngineConfigurator setEnabledCipherSuites( + final String[] enabledCipherSuites) { + this.enabledCipherSuites = enabledCipherSuites != null + ? Arrays.copyOf(enabledCipherSuites, enabledCipherSuites.length) + : null; + return this; + } + + /** + * @return an array of enabled protocols. Modifications made on the array + * content won't be propagated to SSLEngineConfigurator + */ + public String[] getEnabledProtocols() { + return enabledProtocols != null + ? Arrays.copyOf(enabledProtocols, enabledProtocols.length) + : null; + } + + /** + * Sets a list of enabled protocols. + * Note: further modifications made on the passed array won't be propagated + * to SSLEngineConfigurator. + * + * @param enabledProtocols list of enabled protocols + * @return this SSLEngineConfigurator + */ + public SSLEngineConfigurator setEnabledProtocols( + final String[] enabledProtocols) { + this.enabledProtocols = enabledProtocols != null + ? Arrays.copyOf(enabledProtocols, enabledProtocols.length) + : null; + return this; + } + + public boolean isCipherConfigured() { + return isCipherConfigured; + } + + public SSLEngineConfigurator setCipherConfigured(boolean isCipherConfigured) { + this.isCipherConfigured = isCipherConfigured; + return this; + } + + public boolean isProtocolConfigured() { + return isProtocolConfigured; + } + + public SSLEngineConfigurator setProtocolConfigured(boolean isProtocolConfigured) { + this.isProtocolConfigured = isProtocolConfigured; + return this; + } + + public SSLContext getSslContext() { + if (sslContext == null) { + synchronized(sync) { + if (sslContext == null) { + sslContext = sslContextConfiguration.createSSLContext(true); + } + } + } + + return sslContext; + } + + /** + * Return the list of allowed protocol. + * @return String[] an array of supported protocols. + */ + private static String[] configureEnabledProtocols( + SSLEngine sslEngine, String[] requestedProtocols) { + + String[] supportedProtocols = sslEngine.getSupportedProtocols(); + String[] protocols = null; + ArrayList list = null; + for (String supportedProtocol : supportedProtocols) { + /* + * Check to see if the requested protocol is among the + * supported protocols, i.e., may be enabled + */ + for (String protocol : requestedProtocols) { + protocol = protocol.trim(); + if (supportedProtocol.equals(protocol)) { + if (list == null) { + list = new ArrayList(); + } + list.add(protocol); + break; + } + } + } + + if (list != null) { + protocols = list.toArray(new String[list.size()]); + } + + return protocols; + } + + /** + * Determines the SSL cipher suites to be enabled. + * + * @return Array of SSL cipher suites to be enabled, or null if none of the + * requested ciphers are supported + */ + private static String[] configureEnabledCiphers(SSLEngine sslEngine, + String[] requestedCiphers) { + + String[] supportedCiphers = sslEngine.getSupportedCipherSuites(); + String[] ciphers = null; + ArrayList list = null; + for (String supportedCipher : supportedCiphers) { + /* + * Check to see if the requested protocol is among the + * supported protocols, i.e., may be enabled + */ + for (String cipher : requestedCiphers) { + cipher = cipher.trim(); + if (supportedCipher.equals(cipher)) { + if (list == null) { + list = new ArrayList(); + } + list.add(cipher); + break; + } + } + } + + if (list != null) { + ciphers = list.toArray(new String[list.size()]); + } + + return ciphers; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("SSLEngineConfigurator"); + sb.append("{clientMode=").append(clientMode); + sb.append(", enabledCipherSuites=") + .append(enabledCipherSuites == null ? "null" : Arrays.asList(enabledCipherSuites).toString()); + sb.append(", enabledProtocols=") + .append(enabledProtocols == null ? "null" : Arrays.asList(enabledProtocols).toString()); + sb.append(", needClientAuth=").append(needClientAuth); + sb.append(", wantClientAuth=").append(wantClientAuth); + sb.append(", isProtocolConfigured=").append(isProtocolConfigured); + sb.append(", isCipherConfigured=").append(isCipherConfigured); + sb.append('}'); + return sb.toString(); + } + + public SSLEngineConfigurator copy() { + return new SSLEngineConfigurator(this); + } +}