NIFI-3869 Added HTTP/2 support to ListenHTTP and HandleHttpRequest

Signed-off-by: Nathan Gough <thenatog@gmail.com>

This closes #6048.
This commit is contained in:
exceptionfactory 2022-05-13 13:27:05 -05:00 committed by Nathan Gough
parent 38b51b0dde
commit e0976f42d3
16 changed files with 833 additions and 122 deletions

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -78,5 +78,15 @@
<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>
</project>

View File

@ -92,6 +92,11 @@
<artifactId>nifi-flowfile-packager</artifactId>
<version>1.17.0-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>

View File

@ -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;

View File

@ -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 {

View File

@ -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 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();
}
}

View File

@ -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;
}
}

View File

@ -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);

13
pom.xml
View File

@ -420,6 +420,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>