You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by jo...@apache.org on 2022/05/20 16:32:14 UTC
[nifi] 04/05: NIFI-3869 Added HTTP/2 support to ListenHTTP and HandleHttpRequest
This is an automated email from the ASF dual-hosted git repository.
joewitt pushed a commit to branch support/nifi-1.16
in repository https://gitbox.apache.org/repos/asf/nifi.git
commit 0394270007fd8e0e66c36dd3ffb3cd31ad712670
Author: exceptionfactory <ex...@apache.org>
AuthorDate: Fri May 13 13:27:05 2022 -0500
NIFI-3869 Added HTTP/2 support to ListenHTTP and HandleHttpRequest
Signed-off-by: Nathan Gough <th...@gmail.com>
This closes #6048.
---
nifi-commons/nifi-jetty-configuration/pom.xml | 39 +++++
.../connector/ApplicationLayerProtocol.java | 36 ++++
.../connector/ServerConnectorFactory.java | 31 ++++
.../connector/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 ++
.../processors/standard/HandleHttpRequest.java | 99 +++--------
.../processors/standard/HandleHttpResponse.java | 1 -
.../nifi/processors/standard/ListenHTTP.java | 86 ++++-----
.../standard/http/HttpProtocolStrategy.java | 68 ++++++++
.../nifi/processors/standard/TestListenHTTP.java | 10 +-
pom.xml | 13 ++
16 files changed, 835 insertions(+), 124 deletions(-)
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 @@
+<?xml version="1.0"?>
+<!--
+ 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-commons</artifactId>
+ <version>1.17.0-SNAPSHOT</version>
+ </parent>
+ <artifactId>nifi-jetty-configuration</artifactId>
+ <packaging>jar</packaging>
+ <dependencies>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-server</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.http2</groupId>
+ <artifactId>http2-server</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-alpn-server</artifactId>
+ </dependency>
+ </dependencies>
+</project>
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<ApplicationLayerProtocol> DEFAULT_APPLICATION_LAYER_PROTOCOLS = Collections.singleton(ApplicationLayerProtocol.HTTP_1_1);
+
+ private final Server server;
+
+ private final int port;
+
+ private Set<ApplicationLayerProtocol> 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<ConnectionFactory> 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<ApplicationLayerProtocol> 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<String> 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<SSLEngine, List<String>, 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<String> 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 cc161ce9a1..02784b7671 100644
--- a/nifi-commons/pom.xml
+++ b/nifi-commons/pom.xml
@@ -32,6 +32,7 @@
<module>nifi-flow-encryptor</module>
<module>nifi-hl7-query-language</module>
<module>nifi-json-utils</module>
+ <module>nifi-jetty-configuration</module>
<module>nifi-logging-utils</module>
<module>nifi-metrics</module>
<module>nifi-parameter</module>
diff --git a/nifi-nar-bundles/nifi-jetty-bundle/pom.xml b/nifi-nar-bundles/nifi-jetty-bundle/pom.xml
index c3a2df3542..76523de05a 100644
--- a/nifi-nar-bundles/nifi-jetty-bundle/pom.xml
+++ b/nifi-nar-bundles/nifi-jetty-bundle/pom.xml
@@ -78,6 +78,16 @@
<artifactId>apache-jstl</artifactId>
<scope>compile</scope>
</dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-alpn-server</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.http2</groupId>
+ <artifactId>http2-server</artifactId>
+ <scope>compile</scope>
+ </dependency>
</dependencies>
<scm>
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 2793581458..b0b840c392 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 @@
<artifactId>nifi-flowfile-packager</artifactId>
<version>1.16.2-SNAPSHOT</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-jetty-configuration</artifactId>
+ <version>1.17.0-SNAPSHOT</version>
+ </dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-distributed-cache-client-service-api</artifactId>
@@ -180,6 +185,14 @@
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-alpn-server</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.http2</groupId>
+ <artifactId>http2-server</artifactId>
+ </dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
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<String> 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<? extends Servlet> 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 SslContextFactory contextFactory = createSslContextFactory(sslContextService, clientAuthentication);
-
- connector = new ServerConnector(server, new SslConnectionFactory(contextFactory, "http/1.1"), new HttpConnectionFactory(httpConfiguration));
- } else {
- connector = new ServerConnector(server, new HttpConnectionFactory(httpConfiguration));
- }
-
- 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);
+ 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 String[] enabledProtocols = sslContextService == null ? new String[0] : sslContextService.createTlsConfiguration().getEnabledProtocols();
+ serverConnectorFactory.setIncludeSecurityProtocols(enabledProtocols);
+
+ if (ClientAuthentication.REQUIRED == clientAuthentication) {
+ serverConnectorFactory.setNeedClientAuth(true);
+ } else if (ClientAuthentication.WANT == clientAuthentication) {
+ serverConnectorFactory.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<ApplicationLayerProtocol> applicationLayerProtocols;
+
+ HttpProtocolStrategy(final String displayName, final String description, final Set<ApplicationLayerProtocol> 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<ApplicationLayerProtocol> 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 26318b1566..682beac129 100644
--- a/pom.xml
+++ b/pom.xml
@@ -403,6 +403,19 @@
<version>${jetty.version}</version>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-alpn-server</artifactId>
+ <version>${jetty.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.http2</groupId>
+ <artifactId>http2-server</artifactId>
+ <version>${jetty.version}</version>
+ <scope>provided</scope>
+ </dependency>
+
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-client</artifactId>