From e0976f42d33d151035d7bd8207342afb53d12745 Mon Sep 17 00:00:00 2001 From: exceptionfactory Date: Fri, 13 May 2022 13:27:05 -0500 Subject: [PATCH] NIFI-3869 Added HTTP/2 support to ListenHTTP and HandleHttpRequest Signed-off-by: Nathan Gough This closes #6048. --- nifi-commons/nifi-jetty-configuration/pom.xml | 39 ++++ .../connector/ApplicationLayerProtocol.java | 36 ++++ .../connector/ServerConnectorFactory.java | 31 +++ .../StandardServerConnectorFactory.java | 193 ++++++++++++++++++ .../alpn/ALPNServerConnectionFactory.java | 64 ++++++ .../connector/alpn/StandardALPNProcessor.java | 123 +++++++++++ .../StandardServerConnectorFactoryTest.java | 172 ++++++++++++++++ nifi-commons/pom.xml | 1 + nifi-nar-bundles/nifi-jetty-bundle/pom.xml | 10 + .../nifi-standard-processors/pom.xml | 13 ++ .../standard/HandleHttpRequest.java | 99 +++------ .../standard/HandleHttpResponse.java | 1 - .../nifi/processors/standard/ListenHTTP.java | 82 ++++---- .../standard/http/HttpProtocolStrategy.java | 68 ++++++ .../processors/standard/TestListenHTTP.java | 10 +- pom.xml | 13 ++ 16 files changed, 833 insertions(+), 122 deletions(-) create mode 100644 nifi-commons/nifi-jetty-configuration/pom.xml create mode 100644 nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/ApplicationLayerProtocol.java create mode 100644 nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/ServerConnectorFactory.java create mode 100644 nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/StandardServerConnectorFactory.java create mode 100644 nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/alpn/ALPNServerConnectionFactory.java create mode 100644 nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/alpn/StandardALPNProcessor.java create mode 100644 nifi-commons/nifi-jetty-configuration/src/test/java/org/apache/nifi/jetty/configuration/connector/StandardServerConnectorFactoryTest.java create mode 100644 nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/http/HttpProtocolStrategy.java diff --git a/nifi-commons/nifi-jetty-configuration/pom.xml b/nifi-commons/nifi-jetty-configuration/pom.xml new file mode 100644 index 0000000000..8d8e2e7390 --- /dev/null +++ b/nifi-commons/nifi-jetty-configuration/pom.xml @@ -0,0 +1,39 @@ + + + + 4.0.0 + + org.apache.nifi + nifi-commons + 1.17.0-SNAPSHOT + + nifi-jetty-configuration + jar + + + org.eclipse.jetty + jetty-server + + + org.eclipse.jetty.http2 + http2-server + + + org.eclipse.jetty + jetty-alpn-server + + + diff --git a/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/ApplicationLayerProtocol.java b/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/ApplicationLayerProtocol.java new file mode 100644 index 0000000000..9561d75471 --- /dev/null +++ b/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/ApplicationLayerProtocol.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.jetty.configuration.connector; + +/** + * Application Layer Protocols supported for Server Connectors + */ +public enum ApplicationLayerProtocol { + HTTP_1_1("http/1.1"), + + H2("h2"); + + private String protocol; + + ApplicationLayerProtocol(final String protocol) { + this.protocol = protocol; + } + + public String getProtocol() { + return protocol; + } +} diff --git a/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/ServerConnectorFactory.java b/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/ServerConnectorFactory.java new file mode 100644 index 0000000000..20440d2055 --- /dev/null +++ b/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/ServerConnectorFactory.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.jetty.configuration.connector; + +import org.eclipse.jetty.server.ServerConnector; + +/** + * Jetty Server Connector Factory + */ +public interface ServerConnectorFactory { + /** + * Get Server Connector + * + * @return Configured Server Connector + */ + ServerConnector getServerConnector(); +} diff --git a/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/StandardServerConnectorFactory.java b/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/StandardServerConnectorFactory.java new file mode 100644 index 0000000000..e670ac71ea --- /dev/null +++ b/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/StandardServerConnectorFactory.java @@ -0,0 +1,193 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.jetty.configuration.connector; + +import org.apache.nifi.jetty.configuration.connector.alpn.ALPNServerConnectionFactory; +import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.http2.HTTP2Cipher; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +import javax.net.ssl.SSLContext; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * Standard implementation of Server Connector Factory supporting HTTP/2 and HTTP/1.1 with TLS or simple HTTP/1.1 + */ +public class StandardServerConnectorFactory implements ServerConnectorFactory { + private static final boolean SEND_SERVER_VERSION = false; + + private static final String[] INCLUDE_ALL_SECURITY_PROTOCOLS = new String[0]; + + private static final Set DEFAULT_APPLICATION_LAYER_PROTOCOLS = Collections.singleton(ApplicationLayerProtocol.HTTP_1_1); + + private final Server server; + + private final int port; + + private Set applicationLayerProtocols = DEFAULT_APPLICATION_LAYER_PROTOCOLS; + + private SSLContext sslContext; + + private boolean needClientAuth; + + private boolean wantClientAuth; + + private String[] includeSecurityProtocols = INCLUDE_ALL_SECURITY_PROTOCOLS; + + /** + * Standard Server Connector Factory Constructor with required properties + * + * @param server Jetty Server + * @param port Secure Port Number + */ + public StandardServerConnectorFactory( + final Server server, + final int port + ) { + this.server = Objects.requireNonNull(server, "Server required"); + this.port = port; + } + + /** + * Get Server Connector configured with HTTP/2 and ALPN as well as fallback to HTTP/1.1 with TLS + * + * @return Secure Server Connector + */ + @Override + public ServerConnector getServerConnector() { + final HttpConfiguration httpConfiguration = getHttpConfiguration(); + final HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpConfiguration); + + final ServerConnector serverConnector; + if (sslContext == null) { + serverConnector = new ServerConnector(server, httpConnectionFactory); + } else { + final List connectionFactories = new ArrayList<>(); + if (applicationLayerProtocols.contains(ApplicationLayerProtocol.H2)) { + final ALPNServerConnectionFactory alpnServerConnectionFactory = new ALPNServerConnectionFactory(); + final HTTP2ServerConnectionFactory http2ServerConnectionFactory = new HTTP2ServerConnectionFactory(httpConfiguration); + + connectionFactories.add(alpnServerConnectionFactory); + connectionFactories.add(http2ServerConnectionFactory); + } + // Add HTTP/1.1 Connection Factory after HTTP/2 + if (applicationLayerProtocols.contains(ApplicationLayerProtocol.HTTP_1_1)) { + connectionFactories.add(httpConnectionFactory); + } + + // SslConnectionFactory must be first and must indicate the next protocol + final String nextProtocol = connectionFactories.get(0).getProtocol(); + final SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(getSslContextFactory(), nextProtocol); + connectionFactories.add(0, sslConnectionFactory); + + final ConnectionFactory[] factories = connectionFactories.toArray(new ConnectionFactory[0]); + serverConnector = new ServerConnector(server, factories); + } + + serverConnector.setPort(port); + return serverConnector; + } + + /** + * Set SSL Context enables TLS communication + * + * @param sslContext SSL Context + */ + public void setSslContext(final SSLContext sslContext) { + this.sslContext = sslContext; + } + + /** + * Set Need Client Authentication requires clients to provide certificates for mutual TLS + * + * @param needClientAuth Need Client Authentication status + */ + public void setNeedClientAuth(final boolean needClientAuth) { + this.needClientAuth = needClientAuth; + } + + /** + * Set Want Client Authentication requests clients to provide certificates for mutual TLS but does not require certificates + * + * @param wantClientAuth Want Client Authentication status + */ + public void setWantClientAuth(final boolean wantClientAuth) { + this.wantClientAuth = wantClientAuth; + } + + /** + * Set Include Security Protocols limits enabled TLS Protocols to the values provided + * + * @param includeSecurityProtocols Security Protocols with null or empty enabling all standard TLS protocol versions + */ + public void setIncludeSecurityProtocols(final String[] includeSecurityProtocols) { + this.includeSecurityProtocols = includeSecurityProtocols == null ? INCLUDE_ALL_SECURITY_PROTOCOLS : includeSecurityProtocols; + } + + /** + * Set Application Layer Protocols applicable when TLS is enabled + * + * @param applicationLayerProtocols Protocols requires at one Application Layer Protocol + */ + public void setApplicationLayerProtocols(final Set applicationLayerProtocols) { + if (Objects.requireNonNull(applicationLayerProtocols, "Application Layer Protocols required").isEmpty()) { + throw new IllegalArgumentException("Application Layer Protocols not specified"); + } + this.applicationLayerProtocols = applicationLayerProtocols; + } + + private HttpConfiguration getHttpConfiguration() { + final HttpConfiguration httpConfiguration = new HttpConfiguration(); + + if (sslContext != null) { + httpConfiguration.setSecurePort(port); + httpConfiguration.setSecureScheme(HttpScheme.HTTPS.asString()); + httpConfiguration.setSendServerVersion(SEND_SERVER_VERSION); + + final SecureRequestCustomizer secureRequestCustomizer = new SecureRequestCustomizer(); + httpConfiguration.addCustomizer(secureRequestCustomizer); + } + + return httpConfiguration; + } + + private SslContextFactory.Server getSslContextFactory() { + final SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setSslContext(sslContext); + sslContextFactory.setNeedClientAuth(needClientAuth); + sslContextFactory.setWantClientAuth(wantClientAuth); + sslContextFactory.setIncludeProtocols(includeSecurityProtocols); + + if (applicationLayerProtocols.contains(ApplicationLayerProtocol.H2)) { + sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR); + } + + return sslContextFactory; + } +} diff --git a/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/alpn/ALPNServerConnectionFactory.java b/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/alpn/ALPNServerConnectionFactory.java new file mode 100644 index 0000000000..c5da7a5091 --- /dev/null +++ b/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/alpn/ALPNServerConnectionFactory.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.jetty.configuration.connector.alpn; + +import org.eclipse.jetty.alpn.server.ALPNServerConnection; +import org.eclipse.jetty.io.AbstractConnection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.ssl.ALPNProcessor; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.NegotiatingServerConnectionFactory; + +import javax.net.ssl.SSLEngine; +import java.util.List; + +/** + * ALPN Server Connection Factory with standard ALPN Processor implementation + */ +public class ALPNServerConnectionFactory extends NegotiatingServerConnectionFactory { + private static final String ALPN_PROTOCOL = "alpn"; + + private final ALPNProcessor.Server processor; + + public ALPNServerConnectionFactory() { + super(ALPN_PROTOCOL); + processor = new StandardALPNProcessor(); + } + + /** + * Create new Server Connection and configure the connection using ALPN Processor + * + * @param connector Connector for the Connection + * @param endPoint End Point for the Connection + * @param sslEngine SSL Engine for the Connection + * @param protocols Application Protocols + * @param defaultProtocol Default Application Protocol + * @return ALPN Server Connection + */ + @Override + protected AbstractConnection newServerConnection( + final Connector connector, + final EndPoint endPoint, + final SSLEngine sslEngine, + final List protocols, + final String defaultProtocol + ) { + final ALPNServerConnection connection = new ALPNServerConnection(connector, endPoint, sslEngine, protocols, defaultProtocol); + processor.configure(sslEngine, connection); + return connection; + } +} diff --git a/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/alpn/StandardALPNProcessor.java b/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/alpn/StandardALPNProcessor.java new file mode 100644 index 0000000000..0c8825226d --- /dev/null +++ b/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/alpn/StandardALPNProcessor.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.jetty.configuration.connector.alpn; + +import org.eclipse.jetty.alpn.server.ALPNServerConnection; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.ssl.ALPNProcessor; +import org.eclipse.jetty.io.ssl.SslConnection; +import org.eclipse.jetty.io.ssl.SslHandshakeListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSession; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.function.BiFunction; + +/** + * Standard ALPN Processor supporting JDK 1.8.0-251 and higher based on Jetty JDK9ServerALPNProcessor + */ +public class StandardALPNProcessor implements ALPNProcessor.Server, SslHandshakeListener { + private static final Logger logger = LoggerFactory.getLogger(StandardALPNProcessor.class); + + /** + * Applies to SSL Engine instances regardless of implementation + * + * @param sslEngine SSL Engine to be evaluated + * @return Applicable Status + */ + @Override + public boolean appliesTo(final SSLEngine sslEngine) { + return true; + } + + /** + * Configure ALPN negotiation for Connection + * + * @param sslEngine SSL Engine to be configured + * @param connection Connection to be configured + */ + @Override + public void configure(final SSLEngine sslEngine, final Connection connection) { + logger.debug("Configuring Connection Remote Address [{}]", connection.getEndPoint().getRemoteAddress()); + final ALPNServerConnection serverConnection = (ALPNServerConnection) connection; + final ProtocolSelector protocolSelector = new ProtocolSelector(serverConnection); + sslEngine.setHandshakeApplicationProtocolSelector(protocolSelector); + + final SslConnection.DecryptedEndPoint endPoint = (SslConnection.DecryptedEndPoint) serverConnection.getEndPoint(); + endPoint.getSslConnection().addHandshakeListener(protocolSelector); + } + + private static final class ProtocolSelector implements BiFunction, String>, SslHandshakeListener { + private final ALPNServerConnection serverConnection; + + private ProtocolSelector(final ALPNServerConnection connection) { + serverConnection = connection; + } + + /** + * Select supported Application Layer Protocol based on requested protocols + * + * @param sslEngine SSL Engine + * @param protocols Protocols requested + * @return Protocol selected or null when no supported protocol found + */ + @Override + public String apply(final SSLEngine sslEngine, final List protocols) { + String protocol = null; + try { + serverConnection.select(protocols); + protocol = serverConnection.getProtocol(); + logger.debug("Connection Remote Address [{}] Application Layer Protocol [{}] selected", serverConnection.getEndPoint().getRemoteAddress(), protocol); + } catch (final Throwable e) { + logger.debug("Connection Remote Address [{}] Application Layer Protocols {} not supported", serverConnection.getEndPoint().getRemoteAddress(), protocols); + } + return protocol; + } + + /** + * Handler for successful handshake checks for selected Application Layer Protocol + * + * @param event Event is not used + */ + @Override + public void handshakeSucceeded(final Event event) { + final InetSocketAddress remoteAddress = serverConnection.getEndPoint().getRemoteAddress(); + final SSLSession session = event.getSSLEngine().getSession(); + logger.debug("Connection Remote Address [{}] Handshake Succeeded [{}] Cipher Suite [{}]", remoteAddress, session.getProtocol(), session.getCipherSuite()); + + final String protocol = serverConnection.getProtocol(); + if (protocol == null) { + logger.debug("Connection Remote Address [{}] Application Layer Protocol not supported", remoteAddress); + serverConnection.unsupported(); + } + } + + /** + * Handle for failed handshake logs status + * + * @param event Event is not used + * @param failure Failure cause to be logged + */ + @Override + public void handshakeFailed(final Event event, final Throwable failure) { + logger.warn("Connection Remote Address [{}] Handshake Failed", serverConnection.getEndPoint().getRemoteAddress(), failure); + } + } +} diff --git a/nifi-commons/nifi-jetty-configuration/src/test/java/org/apache/nifi/jetty/configuration/connector/StandardServerConnectorFactoryTest.java b/nifi-commons/nifi-jetty-configuration/src/test/java/org/apache/nifi/jetty/configuration/connector/StandardServerConnectorFactoryTest.java new file mode 100644 index 0000000000..d465504976 --- /dev/null +++ b/nifi-commons/nifi-jetty-configuration/src/test/java/org/apache/nifi/jetty/configuration/connector/StandardServerConnectorFactoryTest.java @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.jetty.configuration.connector; + +import org.apache.nifi.jetty.configuration.connector.alpn.ALPNServerConnectionFactory; +import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.SSLContext; + +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class StandardServerConnectorFactoryTest { + private static final int HTTP_PORT = 8080; + + private static final int HTTPS_PORT = 8443; + + private static final String[] INCLUDE_PROTOCOLS = new String[]{ "TLSv1.2" }; + + @Test + void testGetServerConnector() { + final Server server = new Server(); + final StandardServerConnectorFactory factory = new StandardServerConnectorFactory(server, HTTP_PORT); + + final ServerConnector serverConnector = factory.getServerConnector(); + + assertHttpConnectionFactoryFound(serverConnector); + } + + @Test + void testGetServerConnectorSecured() throws NoSuchAlgorithmException { + final StandardServerConnectorFactory factory = createSecuredStandardServerConnectorFactory(); + + final ServerConnector serverConnector = factory.getServerConnector(); + + assertHttpConnectionFactoryFound(serverConnector); + final SslConnectionFactory sslConnectionFactory = assertSslConnectionFactoryFound(serverConnector); + + final HttpConnectionFactory httpConnectionFactory = assertHttpConnectionFactoryFound(serverConnector); + assertHttpConnectionFactorySecured(httpConnectionFactory); + + final SslContextFactory.Server sslContextFactory = (SslContextFactory.Server) sslConnectionFactory.getSslContextFactory(); + assertFalse(sslContextFactory.getNeedClientAuth()); + assertFalse(sslContextFactory.getWantClientAuth()); + assertNotNull(sslContextFactory.getIncludeProtocols()); + + final HTTP2ServerConnectionFactory http2ConnectionFactory = serverConnector.getConnectionFactory(HTTP2ServerConnectionFactory.class); + assertNull(http2ConnectionFactory); + } + + @Test + void testGetServerConnectorSecuredNeedClientAuthentication() throws NoSuchAlgorithmException { + final StandardServerConnectorFactory factory = createSecuredStandardServerConnectorFactory(); + factory.setNeedClientAuth(true); + factory.setIncludeSecurityProtocols(INCLUDE_PROTOCOLS); + + final ServerConnector serverConnector = factory.getServerConnector(); + + assertHttpConnectionFactoryFound(serverConnector); + final SslConnectionFactory sslConnectionFactory = assertSslConnectionFactoryFound(serverConnector); + + final HttpConnectionFactory httpConnectionFactory = assertHttpConnectionFactoryFound(serverConnector); + assertHttpConnectionFactorySecured(httpConnectionFactory); + + final SslContextFactory.Server sslContextFactory = (SslContextFactory.Server) sslConnectionFactory.getSslContextFactory(); + assertTrue(sslContextFactory.getNeedClientAuth()); + assertArrayEquals(INCLUDE_PROTOCOLS, sslContextFactory.getIncludeProtocols()); + } + + @Test + void testGetServerConnectorSecuredHttp2AndHttp1() throws NoSuchAlgorithmException { + final StandardServerConnectorFactory factory = createSecuredStandardServerConnectorFactory(); + factory.setApplicationLayerProtocols(new LinkedHashSet<>(Arrays.asList(ApplicationLayerProtocol.H2, ApplicationLayerProtocol.HTTP_1_1))); + + final ServerConnector serverConnector = factory.getServerConnector(); + + final HttpConnectionFactory httpConnectionFactory = assertHttpConnectionFactoryFound(serverConnector); + assertHttpConnectionFactorySecured(httpConnectionFactory); + + final SslConnectionFactory sslConnectionFactory = assertSslConnectionFactoryFound(serverConnector); + final SslContextFactory.Server sslContextFactory = (SslContextFactory.Server) sslConnectionFactory.getSslContextFactory(); + assertFalse(sslContextFactory.getNeedClientAuth()); + + assertHttp2ConnectionFactoriesFound(serverConnector); + } + + @Test + void testGetServerConnectorSecuredHttp2() throws NoSuchAlgorithmException { + final StandardServerConnectorFactory factory = createSecuredStandardServerConnectorFactory(); + factory.setApplicationLayerProtocols(Collections.singleton(ApplicationLayerProtocol.H2)); + + final ServerConnector serverConnector = factory.getServerConnector(); + + final HttpConnectionFactory connectionFactory = serverConnector.getConnectionFactory(HttpConnectionFactory.class); + assertNull(connectionFactory); + + final SslConnectionFactory sslConnectionFactory = assertSslConnectionFactoryFound(serverConnector); + final SslContextFactory.Server sslContextFactory = (SslContextFactory.Server) sslConnectionFactory.getSslContextFactory(); + assertFalse(sslContextFactory.getNeedClientAuth()); + + assertHttp2ConnectionFactoriesFound(serverConnector); + } + + private StandardServerConnectorFactory createSecuredStandardServerConnectorFactory() throws NoSuchAlgorithmException { + final Server server = new Server(); + final StandardServerConnectorFactory factory = new StandardServerConnectorFactory(server, HTTPS_PORT); + final SSLContext sslContext = SSLContext.getDefault(); + factory.setSslContext(sslContext); + return factory; + } + + private HttpConnectionFactory assertHttpConnectionFactoryFound(final ServerConnector serverConnector) { + assertNotNull(serverConnector); + final HttpConnectionFactory connectionFactory = serverConnector.getConnectionFactory(HttpConnectionFactory.class); + assertNotNull(connectionFactory); + return connectionFactory; + } + + private void assertHttp2ConnectionFactoriesFound(final ServerConnector serverConnector) { + final HTTP2ServerConnectionFactory http2ConnectionFactory = serverConnector.getConnectionFactory(HTTP2ServerConnectionFactory.class); + assertNotNull(http2ConnectionFactory); + + final ALPNServerConnectionFactory alpnServerConnectionFactory = serverConnector.getConnectionFactory(ALPNServerConnectionFactory.class); + assertNotNull(alpnServerConnectionFactory); + } + + private SslConnectionFactory assertSslConnectionFactoryFound(final ServerConnector serverConnector) { + final SslConnectionFactory sslConnectionFactory = serverConnector.getConnectionFactory(SslConnectionFactory.class); + assertNotNull(sslConnectionFactory); + return sslConnectionFactory; + } + + private void assertHttpConnectionFactorySecured(final HttpConnectionFactory httpConnectionFactory) { + final HttpConfiguration configuration = httpConnectionFactory.getHttpConfiguration(); + assertEquals(HTTPS_PORT, configuration.getSecurePort()); + assertEquals(HttpScheme.HTTPS.asString(), configuration.getSecureScheme()); + final SecureRequestCustomizer secureRequestCustomizer = configuration.getCustomizer(SecureRequestCustomizer.class); + assertNotNull(secureRequestCustomizer); + } +} diff --git a/nifi-commons/pom.xml b/nifi-commons/pom.xml index 0c435cd2fc..ee3963ba72 100644 --- a/nifi-commons/pom.xml +++ b/nifi-commons/pom.xml @@ -32,6 +32,7 @@ nifi-flow-encryptor nifi-hl7-query-language nifi-json-utils + nifi-jetty-configuration nifi-logging-utils nifi-metrics nifi-parameter diff --git a/nifi-nar-bundles/nifi-jetty-bundle/pom.xml b/nifi-nar-bundles/nifi-jetty-bundle/pom.xml index f5e96121c2..0c3325b27d 100644 --- a/nifi-nar-bundles/nifi-jetty-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-jetty-bundle/pom.xml @@ -78,5 +78,15 @@ apache-jstl compile + + org.eclipse.jetty + jetty-alpn-server + compile + + + org.eclipse.jetty.http2 + http2-server + compile + diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/pom.xml b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/pom.xml index 44c1bef625..07acc867d0 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/pom.xml +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/pom.xml @@ -92,6 +92,11 @@ nifi-flowfile-packager 1.17.0-SNAPSHOT + + org.apache.nifi + nifi-jetty-configuration + 1.17.0-SNAPSHOT + org.apache.nifi nifi-distributed-cache-client-service-api @@ -180,6 +185,14 @@ org.eclipse.jetty jetty-servlet + + org.eclipse.jetty + jetty-alpn-server + + + org.eclipse.jetty.http2 + http2-server + org.apache.httpcomponents httpclient diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/HandleHttpRequest.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/HandleHttpRequest.java index 96af6c2c7b..7598e39845 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/HandleHttpRequest.java +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/HandleHttpRequest.java @@ -33,6 +33,7 @@ import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.expression.ExpressionLanguageScope; import org.apache.nifi.flowfile.FlowFile; import org.apache.nifi.http.HttpContextMap; +import org.apache.nifi.jetty.configuration.connector.StandardServerConnectorFactory; import org.apache.nifi.processor.AbstractProcessor; import org.apache.nifi.processor.DataUnit; import org.apache.nifi.processor.ProcessContext; @@ -40,21 +41,17 @@ import org.apache.nifi.processor.ProcessSession; import org.apache.nifi.processor.Relationship; import org.apache.nifi.processor.exception.ProcessException; import org.apache.nifi.processor.util.StandardValidators; +import org.apache.nifi.processors.standard.http.HttpProtocolStrategy; import org.apache.nifi.processors.standard.util.HTTPUtils; import org.apache.nifi.scheduling.ExecutionNode; import org.apache.nifi.ssl.RestrictedSSLContextService; import org.apache.nifi.ssl.SSLContextService; import org.apache.nifi.stream.io.StreamUtils; import org.eclipse.jetty.server.Connector; -import org.eclipse.jetty.server.HttpConfiguration; -import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.server.handler.AbstractHandler; -import org.eclipse.jetty.util.ssl.SslContextFactory; import javax.net.ssl.SSLContext; import javax.servlet.AsyncContext; @@ -187,6 +184,14 @@ public class HandleHttpRequest extends AbstractProcessor { .required(false) .identifiesControllerService(RestrictedSSLContextService.class) .build(); + public static final PropertyDescriptor HTTP_PROTOCOL_STRATEGY = new PropertyDescriptor.Builder() + .name("HTTP Protocols") + .description("HTTP Protocols supported for Application Layer Protocol Negotiation with TLS") + .required(true) + .allowableValues(HttpProtocolStrategy.class) + .defaultValue(HttpProtocolStrategy.HTTP_1_1.getValue()) + .dependsOn(SSL_CONTEXT) + .build(); public static final PropertyDescriptor URL_CHARACTER_SET = new PropertyDescriptor.Builder() .name("Default URL Character Set") .description("The character set to use for decoding URL parameters if the HTTP Request does not supply one") @@ -303,6 +308,7 @@ public class HandleHttpRequest extends AbstractProcessor { descriptors.add(PORT); descriptors.add(HOSTNAME); descriptors.add(SSL_CONTEXT); + descriptors.add(HTTP_PROTOCOL_STRATEGY); descriptors.add(HTTP_CONTEXT_MAP); descriptors.add(PATH_REGEX); descriptors.add(URL_CHARACTER_SET); @@ -356,61 +362,24 @@ public class HandleHttpRequest extends AbstractProcessor { final long requestTimeout = httpContextMap.getRequestTimeout(TimeUnit.MILLISECONDS); final String clientAuthValue = context.getProperty(CLIENT_AUTH).getValue(); - final boolean need; - final boolean want; - if (CLIENT_NEED.getValue().equals(clientAuthValue)) { - need = true; - want = false; - } else if (CLIENT_WANT.getValue().equals(clientAuthValue)) { - need = false; - want = true; - } else { - need = false; - want = false; - } - - final SslContextFactory sslFactory = (sslService == null) ? null : createSslFactory(sslService, need, want); - final Server server = new Server(port); - - // create the http configuration - final HttpConfiguration httpConfiguration = new HttpConfiguration(); - if (sslFactory == null) { - // create the connector - final ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfiguration)); - - // set host and port - if (StringUtils.isNotBlank(host)) { - http.setHost(host); - } - http.setPort(port); - - // If request timeout is longer than default Idle Timeout, then increase Idle Timeout as well. - http.setIdleTimeout(Math.max(http.getIdleTimeout(), requestTimeout)); - - // add this connector - server.setConnectors(new Connector[]{http}); - } else { - // add some secure config - final HttpConfiguration httpsConfiguration = new HttpConfiguration(httpConfiguration); - httpsConfiguration.setSecureScheme("https"); - httpsConfiguration.setSecurePort(port); - httpsConfiguration.addCustomizer(new SecureRequestCustomizer()); - - // build the connector - final ServerConnector https = new ServerConnector(server, new SslConnectionFactory(sslFactory, "http/1.1"), new HttpConnectionFactory(httpsConfiguration)); - - // set host and port - if (StringUtils.isNotBlank(host)) { - https.setHost(host); - } - https.setPort(port); - - // If request timeout is longer than default Idle Timeout, then increase Idle Timeout as well. - https.setIdleTimeout(Math.max(https.getIdleTimeout(), requestTimeout)); - - // add this connector - server.setConnectors(new Connector[]{https}); + final Server server = new Server(); + + final StandardServerConnectorFactory serverConnectorFactory = new StandardServerConnectorFactory(server, port); + final boolean needClientAuth = CLIENT_NEED.getValue().equals(clientAuthValue); + serverConnectorFactory.setNeedClientAuth(needClientAuth); + final boolean wantClientAuth = CLIENT_WANT.getValue().equals(clientAuthValue); + serverConnectorFactory.setNeedClientAuth(wantClientAuth); + final SSLContext sslContext = sslService == null ? null : sslService.createContext(); + serverConnectorFactory.setSslContext(sslContext); + final HttpProtocolStrategy httpProtocolStrategy = HttpProtocolStrategy.valueOf(context.getProperty(HTTP_PROTOCOL_STRATEGY).getValue()); + serverConnectorFactory.setApplicationLayerProtocols(httpProtocolStrategy.getApplicationLayerProtocols()); + + final ServerConnector serverConnector = serverConnectorFactory.getServerConnector(); + serverConnector.setIdleTimeout(Math.max(serverConnector.getIdleTimeout(), requestTimeout)); + if (StringUtils.isNotBlank(host)) { + serverConnector.setHost(host); } + server.addConnector(serverConnector); final Set allowedMethods = new HashSet<>(); if (context.getProperty(ALLOW_GET).asBoolean()) { @@ -522,18 +491,6 @@ public class HandleHttpRequest extends AbstractProcessor { return containerQueue.size(); } - private SslContextFactory createSslFactory(final SSLContextService sslContextService, final boolean needClientAuth, final boolean wantClientAuth) { - final SslContextFactory.Server sslFactory = new SslContextFactory.Server(); - - sslFactory.setNeedClientAuth(needClientAuth); - sslFactory.setWantClientAuth(wantClientAuth); - - final SSLContext sslContext = sslContextService.createContext(); - sslFactory.setSslContext(sslContext); - - return sslFactory; - } - @OnUnscheduled public void shutdown() throws Exception { ready = false; diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/HandleHttpResponse.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/HandleHttpResponse.java index 96aa93a803..eff8e4fbf0 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/HandleHttpResponse.java +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/HandleHttpResponse.java @@ -188,7 +188,6 @@ public class HandleHttpResponse extends AbstractProcessor { try { session.exportTo(flowFile, response.getOutputStream()); - response.flushBuffer(); } catch (final ProcessException e) { getLogger().error("Failed to respond to HTTP request for {} due to {}", new Object[]{flowFile, e}); try { diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ListenHTTP.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ListenHTTP.java index e5f98daeb0..a35b9edc73 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ListenHTTP.java +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ListenHTTP.java @@ -31,6 +31,7 @@ import org.apache.nifi.components.ValidationContext; import org.apache.nifi.components.ValidationResult; import org.apache.nifi.expression.ExpressionLanguageScope; import org.apache.nifi.flowfile.FlowFile; +import org.apache.nifi.jetty.configuration.connector.StandardServerConnectorFactory; import org.apache.nifi.processor.AbstractSessionFactoryProcessor; import org.apache.nifi.processor.DataUnit; import org.apache.nifi.processor.ProcessContext; @@ -39,26 +40,21 @@ import org.apache.nifi.processor.ProcessSessionFactory; import org.apache.nifi.processor.Relationship; import org.apache.nifi.processor.exception.ProcessException; import org.apache.nifi.processor.util.StandardValidators; +import org.apache.nifi.processors.standard.http.HttpProtocolStrategy; import org.apache.nifi.processors.standard.servlets.ContentAcknowledgmentServlet; import org.apache.nifi.processors.standard.servlets.HealthCheckServlet; import org.apache.nifi.processors.standard.servlets.ListenHTTPServlet; import org.apache.nifi.scheduling.ExecutionNode; import org.apache.nifi.security.util.ClientAuth; -import org.apache.nifi.security.util.TlsConfiguration; import org.apache.nifi.serialization.RecordReaderFactory; import org.apache.nifi.serialization.RecordSetWriterFactory; import org.apache.nifi.ssl.RestrictedSSLContextService; import org.apache.nifi.ssl.SSLContextService; import org.apache.nifi.stream.io.LeakyBucketStreamThrottler; import org.apache.nifi.stream.io.StreamThrottler; -import org.eclipse.jetty.server.HttpConfiguration; -import org.eclipse.jetty.server.HttpConnectionFactory; -import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import javax.net.ssl.SSLContext; @@ -191,10 +187,18 @@ public class ListenHTTP extends AbstractSessionFactoryProcessor { .build(); public static final PropertyDescriptor SSL_CONTEXT_SERVICE = new PropertyDescriptor.Builder() .name("SSL Context Service") - .description("The Controller Service to use in order to obtain an SSL Context") + .description("SSL Context Service enables support for HTTPS") .required(false) .identifiesControllerService(RestrictedSSLContextService.class) .build(); + public static final PropertyDescriptor HTTP_PROTOCOL_STRATEGY = new PropertyDescriptor.Builder() + .name("HTTP Protocols") + .description("HTTP Protocols supported for Application Layer Protocol Negotiation with TLS") + .required(true) + .allowableValues(HttpProtocolStrategy.class) + .defaultValue(HttpProtocolStrategy.HTTP_1_1.getValue()) + .dependsOn(SSL_CONTEXT_SERVICE) + .build(); public static final PropertyDescriptor HEADERS_AS_ATTRIBUTES_REGEX = new PropertyDescriptor.Builder() .name("HTTP Headers to receive as Attributes (Regex)") .description("Specifies the Regular Expression that determines the names of HTTP Headers that should be passed along as FlowFile attributes") @@ -276,6 +280,7 @@ public class ListenHTTP extends AbstractSessionFactoryProcessor { HEALTH_CHECK_PORT, MAX_DATA_RATE, SSL_CONTEXT_SERVICE, + HTTP_PROTOCOL_STRATEGY, CLIENT_AUTHENTICATION, AUTHORIZED_DN_PATTERN, AUTHORIZED_ISSUER_DN_PATTERN, @@ -396,7 +401,6 @@ public class ListenHTTP extends AbstractSessionFactoryProcessor { int maxThreadPoolSize = context.getProperty(MAX_THREAD_POOL_SIZE).asInteger(); throttlerRef.set(streamThrottler); - final boolean sslRequired = sslContextService != null; final PropertyValue clientAuthenticationProperty = context.getProperty(CLIENT_AUTHENTICATION); final ClientAuthentication clientAuthentication = getClientAuthentication(sslContextService, clientAuthenticationProperty); @@ -409,12 +413,13 @@ public class ListenHTTP extends AbstractSessionFactoryProcessor { // get the configured port final int port = context.getProperty(PORT).evaluateAttributeExpressions().asInteger(); - + final HttpProtocolStrategy httpProtocolStrategy = HttpProtocolStrategy.valueOf(context.getProperty(HTTP_PROTOCOL_STRATEGY).getValue()); final ServerConnector connector = createServerConnector(server, port, sslContextService, - sslRequired, - clientAuthentication); + clientAuthentication, + httpProtocolStrategy + ); server.addConnector(connector); // Add a separate connector for the health check port (if specified) @@ -423,12 +428,14 @@ public class ListenHTTP extends AbstractSessionFactoryProcessor { final ServerConnector healthCheckConnector = createServerConnector(server, healthCheckPort, sslContextService, - sslRequired, - ClientAuthentication.NONE); + ClientAuthentication.NONE, + httpProtocolStrategy + ); server.addConnector(healthCheckConnector); } - final ServletContextHandler contextHandler = new ServletContextHandler(server, "/", true, sslRequired); + final boolean securityEnabled = sslContextService != null; + final ServletContextHandler contextHandler = new ServletContextHandler(server, "/", true, securityEnabled); for (final Class cls : getServerClasses()) { final Path path = cls.getAnnotation(Path.class); // Note: servlets must have a path annotation - this will NPE otherwise @@ -488,41 +495,24 @@ public class ListenHTTP extends AbstractSessionFactoryProcessor { private ServerConnector createServerConnector(final Server server, final int port, final SSLContextService sslContextService, - final boolean sslRequired, - final ClientAuthentication clientAuthentication) { - final ServerConnector connector; - final HttpConfiguration httpConfiguration = new HttpConfiguration(); - if (sslRequired) { - httpConfiguration.setSecureScheme("https"); - httpConfiguration.setSecurePort(port); - httpConfiguration.addCustomizer(new SecureRequestCustomizer()); + final ClientAuthentication clientAuthentication, + final HttpProtocolStrategy httpProtocolStrategy + ) { + final StandardServerConnectorFactory serverConnectorFactory = new StandardServerConnectorFactory(server, port); + final SSLContext sslContext = sslContextService == null ? null : sslContextService.createContext(); + serverConnectorFactory.setSslContext(sslContext); - final SslContextFactory contextFactory = createSslContextFactory(sslContextService, clientAuthentication); + final String[] enabledProtocols = sslContextService == null ? new String[0] : sslContextService.createTlsConfiguration().getEnabledProtocols(); + serverConnectorFactory.setIncludeSecurityProtocols(enabledProtocols); - connector = new ServerConnector(server, new SslConnectionFactory(contextFactory, "http/1.1"), new HttpConnectionFactory(httpConfiguration)); - } else { - connector = new ServerConnector(server, new HttpConnectionFactory(httpConfiguration)); + if (ClientAuthentication.REQUIRED == clientAuthentication) { + serverConnectorFactory.setNeedClientAuth(true); + } else if (ClientAuthentication.WANT == clientAuthentication) { + serverConnectorFactory.setWantClientAuth(true); } - connector.setPort(port); - return connector; - } - - private SslContextFactory createSslContextFactory(final SSLContextService sslContextService, final ClientAuthentication clientAuthentication) { - final SslContextFactory.Server contextFactory = new SslContextFactory.Server(); - final SSLContext sslContext = sslContextService.createContext(); - contextFactory.setSslContext(sslContext); - - final TlsConfiguration tlsConfiguration = sslContextService.createTlsConfiguration(); - contextFactory.setIncludeProtocols(tlsConfiguration.getEnabledProtocols()); - - if (ClientAuthentication.REQUIRED.equals(clientAuthentication)) { - contextFactory.setNeedClientAuth(true); - } else if (ClientAuthentication.WANT.equals(clientAuthentication)) { - contextFactory.setWantClientAuth(true); - } - - return contextFactory; + serverConnectorFactory.setApplicationLayerProtocols(httpProtocolStrategy.getApplicationLayerProtocols()); + return serverConnectorFactory.getServerConnector(); } @OnScheduled @@ -572,7 +562,7 @@ public class ListenHTTP extends AbstractSessionFactoryProcessor { for (final String id : findOldFlowFileIds(context)) { final FlowFileEntryTimeWrapper wrapper = flowFileMap.remove(id); if (wrapper != null) { - getLogger().warn("failed to received acknowledgment for HOLD with ID {} sent by {}; rolling back session", new Object[] {id, wrapper.getClientIP()}); + getLogger().warn("failed to received acknowledgment for HOLD with ID {} sent by {}; rolling back session", id, wrapper.getClientIP()); wrapper.session.rollback(); } } diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/http/HttpProtocolStrategy.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/http/HttpProtocolStrategy.java new file mode 100644 index 0000000000..d99d4182ef --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/http/HttpProtocolStrategy.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.processors.standard.http; + +import org.apache.nifi.components.DescribedValue; +import org.apache.nifi.jetty.configuration.connector.ApplicationLayerProtocol; + +import java.util.LinkedHashSet; +import java.util.Set; + +import static java.util.Arrays.asList; +import static java.util.Collections.singleton; + +/** + * HTTP protocol configuration strategy + */ +public enum HttpProtocolStrategy implements DescribedValue { + HTTP_1_1("http/1.1", "HTTP/1.1", singleton(ApplicationLayerProtocol.HTTP_1_1)), + + H2_HTTP_1_1("h2 http/1.1", "HTTP/2 and HTTP/1.1 negotiated based on requested protocols", new LinkedHashSet<>(asList(ApplicationLayerProtocol.HTTP_1_1, ApplicationLayerProtocol.H2))), + + H2("h2", "HTTP/2", singleton(ApplicationLayerProtocol.H2)); + + private final String displayName; + + private final String description; + + private final Set applicationLayerProtocols; + + HttpProtocolStrategy(final String displayName, final String description, final Set applicationLayerProtocols) { + this.displayName = displayName; + this.description = description; + this.applicationLayerProtocols = applicationLayerProtocols; + } + + @Override + public String getValue() { + return name(); + } + + @Override + public String getDisplayName() { + return displayName; + } + + @Override + public String getDescription() { + return description; + } + + public Set getApplicationLayerProtocols() { + return applicationLayerProtocols; + } +} diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestListenHTTP.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestListenHTTP.java index cc006c333d..489a4d3382 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestListenHTTP.java +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestListenHTTP.java @@ -47,6 +47,7 @@ import okhttp3.RequestBody; import okhttp3.Response; import org.apache.nifi.processor.ProcessContext; import org.apache.nifi.processor.ProcessSessionFactory; +import org.apache.nifi.processors.standard.http.HttpProtocolStrategy; import org.apache.nifi.remote.io.socket.NetworkUtils; import org.apache.nifi.reporting.InitializationException; import org.apache.nifi.security.util.SslContextFactory; @@ -99,7 +100,6 @@ public class TestListenHTTP { private static final Duration CLIENT_CALL_TIMEOUT = Duration.ofSeconds(10); public static final String LOCALHOST_DN = "CN=localhost"; - private static TlsConfiguration tlsConfiguration; private static TlsConfiguration serverConfiguration; private static TlsConfiguration serverTls_1_3_Configuration; private static TlsConfiguration serverNoTruststoreConfiguration; @@ -117,7 +117,7 @@ public class TestListenHTTP { @BeforeClass public static void setUpSuite() throws GeneralSecurityException { // generate new keystore and truststore - tlsConfiguration = new TemporaryKeyStoreBuilder().build(); + final TlsConfiguration tlsConfiguration = new TemporaryKeyStoreBuilder().build(); serverConfiguration = new StandardTlsConfiguration( tlsConfiguration.getKeystorePath(), @@ -223,23 +223,25 @@ public class TestListenHTTP { } @Test - public void testSecurePOSTRequestsReceivedWithoutEL() throws Exception { + public void testSecurePOSTRequestsReceivedWithoutELHttp2AndHttp1() throws Exception { configureProcessorSslContextService(ListenHTTP.ClientAuthentication.AUTO, serverNoTruststoreConfiguration); runner.setProperty(ListenHTTP.PORT, Integer.toString(availablePort)); runner.setProperty(ListenHTTP.BASE_PATH, HTTP_BASE_PATH); + runner.setProperty(ListenHTTP.HTTP_PROTOCOL_STRATEGY, HttpProtocolStrategy.H2_HTTP_1_1.getValue()); runner.assertValid(); testPOSTRequestsReceived(HttpServletResponse.SC_OK, true, false); } @Test - public void testSecurePOSTRequestsReturnCodeReceivedWithoutEL() throws Exception { + public void testSecurePOSTRequestsReturnCodeReceivedWithoutELHttp2() throws Exception { configureProcessorSslContextService(ListenHTTP.ClientAuthentication.AUTO, serverNoTruststoreConfiguration); runner.setProperty(ListenHTTP.PORT, Integer.toString(availablePort)); runner.setProperty(ListenHTTP.BASE_PATH, HTTP_BASE_PATH); runner.setProperty(ListenHTTP.RETURN_CODE, Integer.toString(HttpServletResponse.SC_NO_CONTENT)); + runner.setProperty(ListenHTTP.HTTP_PROTOCOL_STRATEGY, HttpProtocolStrategy.H2.getValue()); runner.assertValid(); testPOSTRequestsReceived(HttpServletResponse.SC_NO_CONTENT, true, false); diff --git a/pom.xml b/pom.xml index ed6a0a35f8..6a12a4d1ac 100644 --- a/pom.xml +++ b/pom.xml @@ -420,6 +420,19 @@ ${jetty.version} provided + + org.eclipse.jetty + jetty-alpn-server + ${jetty.version} + provided + + + org.eclipse.jetty.http2 + http2-server + ${jetty.version} + provided + + org.eclipse.jetty jetty-alpn-client