NIFI-10625 Added support for HTTP/2 in Registry

- Added nifi-security-ssl for generalized SSLContext creation
- Removed static keystore and truststore test files from nifi-registry-jetty

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

This closes #6514.
This commit is contained in:
exceptionfactory 2022-10-11 14:29:42 -05:00 committed by Nathan Gough
parent dfbc7dfc21
commit b753c1c72d
22 changed files with 1008 additions and 312 deletions

View File

@ -0,0 +1,25 @@
<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">
<!--
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.
-->
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-commons</artifactId>
<version>1.19.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-security-ssl</artifactId>
<description>Shared TLS security components without additional dependencies</description>
</project>

View File

@ -0,0 +1,41 @@
/*
* 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.security.ssl;
/**
* Exception indicating runtime failure to create configured objects
*/
public class BuilderConfigurationException extends RuntimeException {
/**
* Builder Configuration Exception Constructor with standard properties
*
* @param message Exception Message
* @param cause Exception Cause
*/
public BuilderConfigurationException(final String message, final Throwable cause) {
super(message, cause);
}
/**
* Builder Configuration Exception Constructor without Throwable cause
*
* @param message Exception Message
*/
public BuilderConfigurationException(final String message) {
super(message);
}
}

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.security.ssl;
import java.security.KeyStore;
/**
* Builder interface for instances of java.security.KeyStore
*/
public interface KeyStoreBuilder {
/**
* Build Key Store based on configured properties
*
* @return Key Store
*/
KeyStore build();
}

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.security.ssl;
import javax.net.ssl.SSLContext;
/**
* Builder interface for instances of javax.net.ssl.SSLContext
*/
public interface SslContextBuilder {
/**
* Build SSLContext using configured properties
*
* @return SSLContext
*/
SSLContext build();
}

View File

@ -0,0 +1,117 @@
/*
* 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.security.ssl;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.CertificateException;
import java.util.Objects;
/**
* Standard implementation of Key Store Builder
*/
public class StandardKeyStoreBuilder implements KeyStoreBuilder {
private String provider;
private String type = KeyStore.getDefaultType();
private InputStream inputStream;
private char[] password;
/**
* Build Key Store using configured properties
*
* @return Key Store
*/
@Override
public KeyStore build() {
final KeyStore keyStore = getKeyStore();
if (inputStream == null) {
throw new BuilderConfigurationException("Key Store InputStream not configured");
}
try {
keyStore.load(inputStream, password);
} catch (final IOException|NoSuchAlgorithmException|CertificateException e) {
throw new BuilderConfigurationException("Key Store loading failed", e);
}
return keyStore;
}
/**
* Set Key Store Provider for Key Store implementation
*
* @param provider Key Store Provider
* @return Builder
*/
public StandardKeyStoreBuilder provider(final String provider) {
this.provider = Objects.requireNonNull(provider, "Key Store Provider required");
return this;
}
/**
* Set Key Store Type defaults to platform configuration derived from KeyStore.getDefaultType()
*
* @param type Key Store Type
* @return Builder
*/
public StandardKeyStoreBuilder type(final String type) {
this.type = Objects.requireNonNull(type, "Key Store Type required");
return this;
}
/**
* Set Key Store Password
*
* @param password Key Store Password
* @return Builder
*/
public StandardKeyStoreBuilder password(final char[] password) {
this.password = Objects.requireNonNull(password, "Key Store Password required");
return this;
}
/**
* Set Key Store InputStream to be loaded
*
* @param inputStream Key Store InputStream
* @return Builder
*/
public StandardKeyStoreBuilder inputStream(final InputStream inputStream) {
this.inputStream = Objects.requireNonNull(inputStream, "Key Store InputStream required");
return this;
}
private KeyStore getKeyStore() {
try {
return provider == null ? KeyStore.getInstance(type) : KeyStore.getInstance(type, provider);
} catch (final KeyStoreException e) {
final String message = String.format("Key Store Type [%s] creation failed", type);
throw new BuilderConfigurationException(message, e);
} catch (final NoSuchProviderException e) {
final String message = String.format("Key Store Type [%s] Provider [%s] creation failed", type, provider);
throw new BuilderConfigurationException(message, e);
}
}
}

View File

@ -0,0 +1,171 @@
/*
* 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.security.ssl;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.util.Objects;
/**
* Standard implementation of SSL Context Builder
*/
public class StandardSslContextBuilder implements SslContextBuilder {
private static final String DEFAULT_PROTOCOL = "TLS";
private String protocol = DEFAULT_PROTOCOL;
private KeyStore keyStore;
private char[] keyPassword;
private KeyStore trustStore;
/**
* Build and initialize an SSL Context using configured Key Manager and Trust Manager sources
*
* @return SSL Context
*/
@Override
public SSLContext build() {
final SSLContext sslContext = getSslContext();
final SecureRandom secureRandom = new SecureRandom();
final KeyManager[] keyManagers = getKeyManagers();
final TrustManager[] trustManagers = getTrustManagers();
try {
sslContext.init(keyManagers, trustManagers, secureRandom);
} catch (final KeyManagementException e) {
throw new BuilderConfigurationException("SSLContext initialization failed", e);
}
return sslContext;
}
/**
* Set TLS Protocol defaults to TLS without a specific version number
*
* @param protocol TLS Protocol
* @return Builder
*/
public StandardSslContextBuilder protocol(final String protocol) {
this.protocol = Objects.requireNonNull(protocol, "Protocol required");
return this;
}
/**
* Set Key Store with Private Key and Certificate Entry
*
* @param keyStore Key Store
* @return Builder
*/
public StandardSslContextBuilder keyStore(final KeyStore keyStore) {
this.keyStore = Objects.requireNonNull(keyStore, "Key Store required");
return this;
}
/**
* Set Key Password for reading Private Key entries from Key Store
*
* @param keyPassword Key Password
* @return Builder
*/
public StandardSslContextBuilder keyPassword(final char[] keyPassword) {
this.keyPassword = Objects.requireNonNull(keyPassword, "Key Password required");
return this;
}
/**
* Set Trust Store with Certificate Entries
*
* @param trustStore Trust Store
* @return Builder
*/
public StandardSslContextBuilder trustStore(final KeyStore trustStore) {
this.trustStore = Objects.requireNonNull(trustStore, "Trust Store required");
return this;
}
private KeyManager[] getKeyManagers() {
final KeyManager[] keyManagers;
if (keyStore == null) {
keyManagers = null;
} else {
final KeyManagerFactory keyManagerFactory = getKeyManagerFactory();
try {
keyManagerFactory.init(keyStore, keyPassword);
} catch (final KeyStoreException|NoSuchAlgorithmException|UnrecoverableKeyException e) {
throw new BuilderConfigurationException("Key Manager initialization failed", e);
}
keyManagers = keyManagerFactory.getKeyManagers();
}
return keyManagers;
}
private TrustManager[] getTrustManagers() {
final TrustManager[] trustManagers;
if (trustStore == null) {
trustManagers = null;
} else {
final TrustManagerFactory trustManagerFactory = getTrustManagerFactory();
try {
trustManagerFactory.init(trustStore);
} catch (final KeyStoreException e) {
throw new BuilderConfigurationException("Trust Manager initialization failed", e);
}
trustManagers = trustManagerFactory.getTrustManagers();
}
return trustManagers;
}
private KeyManagerFactory getKeyManagerFactory() {
final String algorithm = KeyManagerFactory.getDefaultAlgorithm();
try {
return KeyManagerFactory.getInstance(algorithm);
} catch (final NoSuchAlgorithmException e) {
final String message = String.format("KeyManagerFactory creation failed with algorithm [%s]", algorithm);
throw new BuilderConfigurationException(message, e);
}
}
private TrustManagerFactory getTrustManagerFactory() {
final String algorithm = TrustManagerFactory.getDefaultAlgorithm();
try {
return TrustManagerFactory.getInstance(algorithm);
} catch (final NoSuchAlgorithmException e) {
final String message = String.format("TrustManagerFactory creation failed with algorithm [%s]", algorithm);
throw new BuilderConfigurationException(message, e);
}
}
private SSLContext getSslContext() {
try {
return SSLContext.getInstance(protocol);
} catch (final NoSuchAlgorithmException e) {
final String message = String.format("SSLContext creation failed with protocol [%s]", protocol);
throw new BuilderConfigurationException(message, e);
}
}
}

View File

@ -0,0 +1,57 @@
/*
* 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.security.ssl;
import org.junit.jupiter.api.Test;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.util.UUID;
import static java.nio.file.Files.createTempFile;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class StandardKeyStoreBuilderTest {
private static final String TYPE = KeyStore.getDefaultType();
private static final char[] PASSWORD = UUID.randomUUID().toString().toCharArray();
@Test
void testBuild() throws Exception {
final Path path = createTempFile(StandardKeyStoreBuilderTest.class.getSimpleName(), TYPE);
path.toFile().deleteOnExit();
final KeyStore sourceKeyStore = KeyStore.getInstance(TYPE);
sourceKeyStore.load(null);
try (final OutputStream outputStream = Files.newOutputStream(path)) {
sourceKeyStore.store(outputStream, PASSWORD);
}
final StandardKeyStoreBuilder builder = new StandardKeyStoreBuilder();
builder.type(TYPE);
builder.password(PASSWORD);
try (final InputStream inputStream = Files.newInputStream(path)) {
builder.inputStream(inputStream);
final KeyStore keyStore = builder.build();
assertNotNull(keyStore);
}
}
}

View File

@ -0,0 +1,78 @@
/*
* 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.security.ssl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import javax.net.ssl.SSLContext;
import java.security.KeyStore;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ExtendWith(MockitoExtension.class)
class StandardSslContextBuilderTest {
private static final String TLS_PROTOCOL = "TLS";
@Mock
KeyStore trustStore;
@Test
void testBuild() {
final StandardSslContextBuilder builder = new StandardSslContextBuilder();
final SSLContext sslContext = builder.build();
assertNotNull(sslContext);
}
@Test
void testBuildProtocol() {
final StandardSslContextBuilder builder = new StandardSslContextBuilder();
builder.protocol(TLS_PROTOCOL);
final SSLContext sslContext = builder.build();
assertNotNull(sslContext);
}
@Test
void testBuildKeyStore() throws Exception {
final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
final StandardSslContextBuilder builder = new StandardSslContextBuilder();
builder.keyStore(keyStore);
final SSLContext sslContext = builder.build();
assertNotNull(sslContext);
}
@Test
void testBuildTrustStore() {
final StandardSslContextBuilder builder = new StandardSslContextBuilder();
builder.trustStore(trustStore);
final SSLContext sslContext = builder.build();
assertNotNull(sslContext);
}
}

View File

@ -57,6 +57,7 @@
<module>nifi-security-kerberos</module> <module>nifi-security-kerberos</module>
<module>nifi-security-kms</module> <module>nifi-security-kms</module>
<module>nifi-security-socket-ssl</module> <module>nifi-security-socket-ssl</module>
<module>nifi-security-ssl</module>
<module>nifi-security-utils-api</module> <module>nifi-security-utils-api</module>
<module>nifi-security-utils</module> <module>nifi-security-utils</module>
<module>nifi-single-user-utils</module> <module>nifi-single-user-utils</module>

View File

@ -165,6 +165,7 @@
<nifi.registry.web.http.port>18080</nifi.registry.web.http.port> <nifi.registry.web.http.port>18080</nifi.registry.web.http.port>
<nifi.registry.web.https.host /> <nifi.registry.web.https.host />
<nifi.registry.web.https.port /> <nifi.registry.web.https.port />
<nifi.registry.web.https.application.protocols>http/1.1</nifi.registry.web.https.application.protocols>
<nifi.registry.jetty.work.dir>./work/jetty</nifi.registry.jetty.work.dir> <nifi.registry.jetty.work.dir>./work/jetty</nifi.registry.jetty.work.dir>
<nifi.registry.web.jetty.threads>200</nifi.registry.web.jetty.threads> <nifi.registry.web.jetty.threads>200</nifi.registry.web.jetty.threads>
<nifi.registry.web.should.send.server.version>true</nifi.registry.web.should.send.server.version> <nifi.registry.web.should.send.server.version>true</nifi.registry.web.should.send.server.version>

View File

@ -1013,6 +1013,13 @@ These properties pertain to the web-based User Interface.
|`nifi.registry.web.http.port`|The HTTP port. The default value is `18080`. |`nifi.registry.web.http.port`|The HTTP port. The default value is `18080`.
|`nifi.registry.web.https.host`|The HTTPS host. It is blank by default. |`nifi.registry.web.https.host`|The HTTPS host. It is blank by default.
|`nifi.registry.web.https.port`|The HTTPS port. It is blank by default. When configuring NiFi Registry to run securely, this port should be configured. |`nifi.registry.web.https.port`|The HTTPS port. It is blank by default. When configuring NiFi Registry to run securely, this port should be configured.
|`nifi.registry.web.https.application.protocols`|The space-separated list of application protocols supported when running with HTTPS enabled.
The default value is `http/1.1`.
The value can be set to `h2 http/1.1` to support Application Layer Protocol Negotiation (ALPN) for HTTP/2 or HTTP/1.1 based on client capabilities.
The value can be set to `h2` to require HTTP/2 and disable HTTP/1.1.
|`nifi.registry.web.jetty.working.directory`|The location of the Jetty working directory. The default value is `./work/jetty`. |`nifi.registry.web.jetty.working.directory`|The location of the Jetty working directory. The default value is `./work/jetty`.
|`nifi.registry.web.jetty.threads`|The number of Jetty threads. The default value is `200`. |`nifi.registry.web.jetty.threads`|The number of Jetty threads. The default value is `200`.
|==== |====

View File

@ -28,6 +28,29 @@
<artifactId>nifi-registry-properties</artifactId> <artifactId>nifi-registry-properties</artifactId>
<version>1.19.0-SNAPSHOT</version> <version>1.19.0-SNAPSHOT</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-jetty-configuration</artifactId>
<version>1.19.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-ssl</artifactId>
<version>1.19.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-utils-api</artifactId>
<version>1.19.0-SNAPSHOT</version>
</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>
<dependency> <dependency>
<groupId>org.eclipse.jetty</groupId> <groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId> <artifactId>jetty-server</artifactId>
@ -62,5 +85,11 @@
<artifactId>apache-jstl</artifactId> <artifactId>apache-jstl</artifactId>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-utils</artifactId>
<version>1.19.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -17,6 +17,8 @@
package org.apache.nifi.registry.jetty; package org.apache.nifi.registry.jetty;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.jetty.configuration.connector.ServerConnectorFactory;
import org.apache.nifi.registry.jetty.connector.ApplicationServerConnectorFactory;
import org.apache.nifi.registry.jetty.headers.ContentSecurityPolicyFilter; import org.apache.nifi.registry.jetty.headers.ContentSecurityPolicyFilter;
import org.apache.nifi.registry.jetty.headers.StrictTransportSecurityFilter; import org.apache.nifi.registry.jetty.headers.StrictTransportSecurityFilter;
import org.apache.nifi.registry.jetty.headers.XFrameOptionsFilter; import org.apache.nifi.registry.jetty.headers.XFrameOptionsFilter;
@ -26,17 +28,12 @@ import org.apache.nifi.registry.security.crypto.CryptoKeyProvider;
import org.eclipse.jetty.annotations.AnnotationConfiguration; import org.eclipse.jetty.annotations.AnnotationConfiguration;
import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Handler;
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.Server;
import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.servlet.DefaultServlet; import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.webapp.Configuration; import org.eclipse.jetty.webapp.Configuration;
import org.eclipse.jetty.webapp.JettyWebXmlConfiguration; import org.eclipse.jetty.webapp.JettyWebXmlConfiguration;
@ -72,16 +69,12 @@ public class JettyServer {
private static final Logger logger = LoggerFactory.getLogger(JettyServer.class); private static final Logger logger = LoggerFactory.getLogger(JettyServer.class);
private static final String WEB_DEFAULTS_XML = "org/apache/nifi-registry/web/webdefault.xml"; private static final String WEB_DEFAULTS_XML = "org/apache/nifi-registry/web/webdefault.xml";
private static final int HEADER_BUFFER_SIZE = 16 * 1024; // 16kb
private static final String CIPHER_SUITE_SEPARATOR_PATTERN = ",\\s*";
private static final String ALL_PATHS = "/*";
private static final FileFilter WAR_FILTER = new FileFilter() { private static final FileFilter WAR_FILTER = pathname -> {
@Override final String nameToTest = pathname.getName().toLowerCase();
public boolean accept(File pathname) { return nameToTest.endsWith(".war") && pathname.isFile();
final String nameToTest = pathname.getName().toLowerCase();
return nameToTest.endsWith(".war") && pathname.isFile();
}
}; };
private final NiFiRegistryProperties properties; private final NiFiRegistryProperties properties;
@ -89,9 +82,7 @@ public class JettyServer {
private final String docsLocation; private final String docsLocation;
private final Server server; private final Server server;
private WebAppContext webUiContext;
private WebAppContext webApiContext; private WebAppContext webApiContext;
private WebAppContext webDocsContext;
public JettyServer(final NiFiRegistryProperties properties, final CryptoKeyProvider cryptoKeyProvider, final String docsLocation) { public JettyServer(final NiFiRegistryProperties properties, final CryptoKeyProvider cryptoKeyProvider, final String docsLocation) {
final QueuedThreadPool threadPool = new QueuedThreadPool(properties.getWebThreads()); final QueuedThreadPool threadPool = new QueuedThreadPool(properties.getWebThreads());
@ -156,128 +147,9 @@ public class JettyServer {
} }
private void configureConnectors() { private void configureConnectors() {
// create the http configuration final ServerConnectorFactory serverConnectorFactory = new ApplicationServerConnectorFactory(server, properties);
final HttpConfiguration httpConfiguration = new HttpConfiguration(); final ServerConnector serverConnector = serverConnectorFactory.getServerConnector();
httpConfiguration.setRequestHeaderSize(HEADER_BUFFER_SIZE); server.addConnector(serverConnector);
httpConfiguration.setResponseHeaderSize(HEADER_BUFFER_SIZE);
httpConfiguration.setSendServerVersion(properties.shouldSendServerVersion());
if (properties.getPort() != null) {
final Integer port = properties.getPort();
if (port < 0 || (int) Math.pow(2, 16) <= port) {
throw new IllegalStateException("Invalid HTTP port: " + port);
}
logger.info("Configuring Jetty for HTTP on port: " + port);
// create the connector
final ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfiguration));
// set host and port
if (StringUtils.isNotBlank(properties.getHttpHost())) {
http.setHost(properties.getHttpHost());
}
http.setPort(port);
// add this connector
server.addConnector(http);
} else if (properties.getSslPort() != null) {
final Integer port = properties.getSslPort();
if (port < 0 || (int) Math.pow(2, 16) <= port) {
throw new IllegalStateException("Invalid HTTPs port: " + port);
}
if (StringUtils.isBlank(properties.getKeyStorePath())) {
throw new IllegalStateException(NiFiRegistryProperties.SECURITY_KEYSTORE
+ " must be provided to configure Jetty for HTTPs");
}
logger.info("Configuring Jetty for HTTPs on port: " + port);
// add some secure config
final HttpConfiguration httpsConfiguration = new HttpConfiguration(httpConfiguration);
httpsConfiguration.setSecureScheme("https");
httpsConfiguration.setSecurePort(properties.getSslPort());
httpsConfiguration.addCustomizer(new SecureRequestCustomizer());
// build the connector
final ServerConnector https = new ServerConnector(server,
new SslConnectionFactory(createSslContextFactory(), "http/1.1"),
new HttpConnectionFactory(httpsConfiguration));
// set host and port
if (StringUtils.isNotBlank(properties.getHttpsHost())) {
https.setHost(properties.getHttpsHost());
}
https.setPort(port);
// add this connector
server.addConnector(https);
}
}
private static String[] getCipherSuites(final String cipherSuitesProperty) {
return cipherSuitesProperty.split(CIPHER_SUITE_SEPARATOR_PATTERN);
}
private SslContextFactory createSslContextFactory() {
final SslContextFactory.Server contextFactory = new SslContextFactory.Server();
// if needClientAuth is false then set want to true so we can optionally use certs
if (properties.getNeedClientAuth()) {
logger.info("Setting Jetty's SSLContextFactory needClientAuth to true");
contextFactory.setNeedClientAuth(true);
} else {
logger.info("Setting Jetty's SSLContextFactory wantClientAuth to true");
contextFactory.setWantClientAuth(true);
}
/* below code sets JSSE system properties when values are provided */
// keystore properties
if (StringUtils.isNotBlank(properties.getKeyStorePath())) {
contextFactory.setKeyStorePath(properties.getKeyStorePath());
}
if (StringUtils.isNotBlank(properties.getKeyStoreType())) {
contextFactory.setKeyStoreType(properties.getKeyStoreType());
}
final String keystorePassword = properties.getKeyStorePassword();
final String keyPassword = properties.getKeyPassword();
if (StringUtils.isEmpty(keystorePassword)) {
throw new IllegalArgumentException("The keystore password cannot be null or empty");
} else {
// if no key password was provided, then assume the key password is the same as the keystore password.
final String defaultKeyPassword = (StringUtils.isBlank(keyPassword)) ? keystorePassword : keyPassword;
contextFactory.setKeyStorePassword(keystorePassword);
contextFactory.setKeyManagerPassword(defaultKeyPassword);
}
// truststore properties
if (StringUtils.isNotBlank(properties.getTrustStorePath())) {
contextFactory.setTrustStorePath(properties.getTrustStorePath());
}
if (StringUtils.isNotBlank(properties.getTrustStoreType())) {
contextFactory.setTrustStoreType(properties.getTrustStoreType());
}
if (StringUtils.isNotBlank(properties.getTrustStorePassword())) {
contextFactory.setTrustStorePassword(properties.getTrustStorePassword());
}
final String includeCipherSuites = properties.getHttpsCipherSuitesInclude();
if (StringUtils.isNotBlank(includeCipherSuites)) {
final String[] cipherSuites = getCipherSuites(includeCipherSuites);
contextFactory.setIncludeCipherSuites(cipherSuites);
}
final String excludeCipherSuites = properties.getHttpsCipherSuitesExclude();
if (StringUtils.isNotBlank(excludeCipherSuites)) {
final String[] cipherSuites = getCipherSuites(excludeCipherSuites);
contextFactory.setExcludeCipherSuites(cipherSuites);
}
return contextFactory;
} }
private void loadWars() throws IOException { private void loadWars() throws IOException {
@ -309,7 +181,7 @@ public class JettyServer {
throw new IllegalStateException("Unable to locate NiFi Registry Web Docs"); throw new IllegalStateException("Unable to locate NiFi Registry Web Docs");
} }
webUiContext = loadWar(webUiWar, "/nifi-registry"); WebAppContext webUiContext = loadWar(webUiWar, "/nifi-registry");
webUiContext.getInitParams().put("oidc-supported", String.valueOf(properties.isOidcEnabled())); webUiContext.getInitParams().put("oidc-supported", String.valueOf(properties.isOidcEnabled()));
webApiContext = loadWar(webApiWar, "/nifi-registry-api", getWebApiAdditionalClasspath()); webApiContext = loadWar(webApiWar, "/nifi-registry-api", getWebApiAdditionalClasspath());
@ -322,7 +194,7 @@ public class JettyServer {
webApiContext.setAttribute("org.eclipse.jetty.server.webapp.WebInfIncludeJarPattern", ".*/spring-[^/]*\\.jar$"); webApiContext.setAttribute("org.eclipse.jetty.server.webapp.WebInfIncludeJarPattern", ".*/spring-[^/]*\\.jar$");
final String docsContextPath = "/nifi-registry-docs"; final String docsContextPath = "/nifi-registry-docs";
webDocsContext = loadWar(webDocsWar, docsContextPath); WebAppContext webDocsContext = loadWar(webDocsWar, docsContextPath);
addDocsServlets(webDocsContext); addDocsServlets(webDocsContext);
final HandlerCollection handlers = new HandlerCollection(); final HandlerCollection handlers = new HandlerCollection();
@ -371,20 +243,18 @@ public class JettyServer {
webappContext.setMaxFormContentSize(600000); webappContext.setMaxFormContentSize(600000);
// add HTTP security headers to all responses // add HTTP security headers to all responses
final String ALL_PATHS = "/*";
ArrayList<Class<? extends Filter>> filters = new ArrayList<>(Arrays.asList(XFrameOptionsFilter.class, ContentSecurityPolicyFilter.class, XSSProtectionFilter.class)); ArrayList<Class<? extends Filter>> filters = new ArrayList<>(Arrays.asList(XFrameOptionsFilter.class, ContentSecurityPolicyFilter.class, XSSProtectionFilter.class));
if(properties.isHTTPSConfigured()) { if (properties.isHTTPSConfigured()) {
filters.add(StrictTransportSecurityFilter.class); filters.add(StrictTransportSecurityFilter.class);
} }
filters.forEach( (filter) -> addFilters(filter, ALL_PATHS, webappContext)); filters.forEach( (filter) -> addFilters(filter, webappContext));
// start out assuming the system ClassLoader will be the parent, but if additional resources were specified then // start out assuming the system ClassLoader will be the parent, but if additional resources were specified then
// inject a new ClassLoader in between the system and webapp ClassLoaders that contains the additional resources // inject a new ClassLoader in between the system and webapp ClassLoaders that contains the additional resources
ClassLoader parentClassLoader = ClassLoader.getSystemClassLoader(); ClassLoader parentClassLoader = ClassLoader.getSystemClassLoader();
if (additionalResources != null && additionalResources.length > 0) { if (additionalResources != null && additionalResources.length > 0) {
URLClassLoader additionalClassLoader = new URLClassLoader(additionalResources, ClassLoader.getSystemClassLoader()); parentClassLoader = new URLClassLoader(additionalResources, ClassLoader.getSystemClassLoader());
parentClassLoader = additionalClassLoader;
} }
webappContext.setClassLoader(new WebAppClassLoader(parentClassLoader, webappContext)); webappContext.setClassLoader(new WebAppClassLoader(parentClassLoader, webappContext));
@ -393,10 +263,10 @@ public class JettyServer {
return webappContext; return webappContext;
} }
private void addFilters(Class<? extends Filter> clazz, String path, WebAppContext webappContext) { private void addFilters(Class<? extends Filter> clazz, final WebAppContext webappContext) {
FilterHolder holder = new FilterHolder(clazz); FilterHolder holder = new FilterHolder(clazz);
holder.setName(clazz.getSimpleName()); holder.setName(clazz.getSimpleName());
webappContext.addFilter(holder, path, EnumSet.allOf(DispatcherType.class)); webappContext.addFilter(holder, ALL_PATHS, EnumSet.allOf(DispatcherType.class));
} }
private URL[] getWebApiAdditionalClasspath() { private URL[] getWebApiAdditionalClasspath() {
@ -451,7 +321,7 @@ public class JettyServer {
logger.info("]"); logger.info("]");
} }
return resources.toArray(new URL[resources.size()]); return resources.toArray(new URL[0]);
} }
private void addDocsServlets(WebAppContext docsContext) { private void addDocsServlets(WebAppContext docsContext) {

View File

@ -0,0 +1,231 @@
/*
* 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.registry.jetty.connector;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.jetty.configuration.connector.ApplicationLayerProtocol;
import org.apache.nifi.jetty.configuration.connector.StandardServerConnectorFactory;
import org.apache.nifi.registry.properties.NiFiRegistryProperties;
import org.apache.nifi.security.ssl.StandardKeyStoreBuilder;
import org.apache.nifi.security.ssl.StandardSslContextBuilder;
import org.apache.nifi.security.util.TlsPlatform;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Registry Application extension of Standard Jetty Server Connector Factory
*/
public class ApplicationServerConnectorFactory extends StandardServerConnectorFactory {
private static final int HEADER_SIZE = 16384;
private static final String CIPHER_SUITE_SEPARATOR_PATTERN = ",\\s*";
private final String includeCipherSuites;
private final String excludeCipherSuites;
private final String host;
private SslContextFactory.Server sslContextFactory;
public ApplicationServerConnectorFactory(final Server server, final NiFiRegistryProperties properties) {
super(server, getPort(properties));
host = getHost(properties);
includeCipherSuites = properties.getHttpsCipherSuitesInclude();
excludeCipherSuites = properties.getHttpsCipherSuitesExclude();
if (properties.isHTTPSConfigured()) {
if (properties.getNeedClientAuth()) {
setNeedClientAuth(true);
} else {
setWantClientAuth(true);
}
final SSLContext sslContext = buildSslContext(properties);
setSslContext(sslContext);
setApplicationLayerProtocols(properties);
// Set Transport Layer Security Protocols based on platform configuration
setIncludeSecurityProtocols(TlsPlatform.getPreferredProtocols().toArray(new String[0]));
}
}
/**
* Get Server Connector using configured properties
*
* @return Server Connector
*/
@Override
public ServerConnector getServerConnector() {
final ServerConnector serverConnector = super.getServerConnector();
serverConnector.setHost(host);
return serverConnector;
}
/**
* Get Jetty Server SSL Context Factory and reuse the same instance for multiple invocations
*
* @return Jetty Server SSL Context Factory
*/
@Override
protected SslContextFactory.Server getSslContextFactory() {
if (sslContextFactory == null) {
sslContextFactory = super.getSslContextFactory();
if (StringUtils.isNotBlank(includeCipherSuites)) {
final String[] cipherSuites = getCipherSuites(includeCipherSuites);
sslContextFactory.setIncludeCipherSuites(cipherSuites);
}
if (StringUtils.isNotBlank(excludeCipherSuites)) {
final String[] cipherSuites = getCipherSuites(excludeCipherSuites);
sslContextFactory.setExcludeCipherSuites(cipherSuites);
}
}
return sslContextFactory;
}
/**
* Get HTTP Configuration with additional settings
*
* @return HTTP Configuration
*/
@Override
protected HttpConfiguration getHttpConfiguration() {
final HttpConfiguration httpConfiguration = super.getHttpConfiguration();
httpConfiguration.setRequestHeaderSize(HEADER_SIZE);
httpConfiguration.setResponseHeaderSize(HEADER_SIZE);
return httpConfiguration;
}
private String[] getCipherSuites(final String cipherSuitesProperty) {
return cipherSuitesProperty.split(CIPHER_SUITE_SEPARATOR_PATTERN);
}
private SSLContext buildSslContext(final NiFiRegistryProperties properties) {
final KeyStore keyStore = buildKeyStore(properties);
final char[] keyPassword = getKeyPassword(properties);
final KeyStore trustStore = buildTrustStore(properties);
return new StandardSslContextBuilder()
.keyStore(keyStore)
.keyPassword(keyPassword)
.trustStore(trustStore)
.build();
}
private char[] getKeyPassword(final NiFiRegistryProperties properties) {
final String keyStorePassword = getRequiredProperty(properties, NiFiRegistryProperties.SECURITY_KEYSTORE_PASSWD);
final String keyPassword = properties.getProperty(NiFiRegistryProperties.SECURITY_KEY_PASSWD, keyStorePassword);
return keyPassword.toCharArray();
}
private KeyStore buildKeyStore(final NiFiRegistryProperties properties) {
final String keyStore = getRequiredProperty(properties, NiFiRegistryProperties.SECURITY_KEYSTORE);
final String keyStoreType = getRequiredProperty(properties, NiFiRegistryProperties.SECURITY_KEYSTORE_TYPE);
final String keyStorePassword = getRequiredProperty(properties, NiFiRegistryProperties.SECURITY_KEYSTORE_PASSWD);
return buildStore(keyStore, keyStoreType, keyStorePassword);
}
private KeyStore buildTrustStore(final NiFiRegistryProperties properties) {
final String trustStore = getRequiredProperty(properties, NiFiRegistryProperties.SECURITY_TRUSTSTORE);
final String trustStoreType = getRequiredProperty(properties, NiFiRegistryProperties.SECURITY_KEYSTORE_TYPE);
final String trustStorePassword = getRequiredProperty(properties, NiFiRegistryProperties.SECURITY_TRUSTSTORE_PASSWD);
return buildStore(trustStore, trustStoreType, trustStorePassword);
}
private KeyStore buildStore(
final String store,
final String storeType,
final String storePassword
) {
final Path trustStorePath = Paths.get(store);
try (final InputStream inputStream = Files.newInputStream(trustStorePath)) {
return new StandardKeyStoreBuilder()
.type(storeType)
.password(storePassword.toCharArray())
.inputStream(inputStream)
.build();
} catch (final IOException e) {
final String message = String.format("Store Path [%s] read failed", store);
throw new IllegalStateException(message, e);
}
}
private String getRequiredProperty(final NiFiRegistryProperties properties, final String property) {
final String requiredProperty = properties.getProperty(property);
if (requiredProperty == null || requiredProperty.isEmpty()) {
throw new IllegalStateException(String.format("Required Property [%s] not configured", property));
}
return requiredProperty;
}
private void setApplicationLayerProtocols(final NiFiRegistryProperties properties) {
final Set<String> protocols = properties.getWebHttpsApplicationProtocols();
final Set<ApplicationLayerProtocol> applicationLayerProtocols = Arrays.stream(ApplicationLayerProtocol.values())
.filter(
applicationLayerProtocol -> protocols.contains(applicationLayerProtocol.getProtocol())
)
.collect(Collectors.toSet());
setApplicationLayerProtocols(applicationLayerProtocols);
}
private static String getHost(final NiFiRegistryProperties properties) {
final String host;
if (properties.isHTTPSConfigured()) {
host = properties.getHttpsHost();
} else {
host = properties.getHttpHost();
}
return host;
}
private static int getPort(final NiFiRegistryProperties properties) {
final Integer httpsPort = properties.getSslPort();
final Integer httpPort = properties.getPort();
if (ObjectUtils.allNull(httpsPort, httpPort)) {
throw new IllegalStateException("Invalid port configuration: Neither nifi.registry.web.https.port nor nifi.registry.web.http.port specified");
} else if (ObjectUtils.allNotNull(httpsPort, httpPort)) {
throw new IllegalStateException("Invalid port configuration: Both nifi.registry.web.https.port and nifi.registry.web.http.port specified");
}
return ObjectUtils.defaultIfNull(httpsPort, httpPort);
}
}

View File

@ -1,165 +0,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.
*/
package org.apache.nifi.registry.jetty
import org.apache.nifi.registry.properties.NiFiRegistryProperties
import org.eclipse.jetty.util.ssl.SslContextFactory
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import org.junit.runner.RunWith
import org.mockito.junit.MockitoJUnitRunner
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.eclipse.jetty.server.Server
@RunWith(MockitoJUnitRunner.class)
class JettyServerGroovyTest extends GroovyTestCase {
private static final Logger logger = LoggerFactory.getLogger(JettyServerGroovyTest.class)
private static final keyPassword = "keyPassword"
private static final keystorePassword = "keystorePassword"
private static final truststorePassword = "truststorePassword"
private static final matchingPassword = "thePassword"
@Test
void testCreateSslContextFactoryWithKeystoreAndKeypassword() throws Exception {
// Arrange
NiFiRegistryProperties properties = new NiFiRegistryProperties()
properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE, "src/test/resources/truststore.jks")
properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE_PASSWD, truststorePassword)
properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE_TYPE, "JKS")
properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE, "src/test/resources/keystoreDifferentPasswords.jks")
properties.setProperty(NiFiRegistryProperties.SECURITY_KEY_PASSWD, keyPassword)
properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE_PASSWD, keystorePassword)
properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE_TYPE, "JKS")
Server internalServer = new Server()
JettyServer testServer = new JettyServer(internalServer, properties)
// Act
SslContextFactory sslContextFactory = testServer.createSslContextFactory()
sslContextFactory.start()
// Assert
assertNotNull(sslContextFactory)
assertNotNull(sslContextFactory.getSslContext())
}
@Test
void testCreateSslContextFactoryWithOnlyKeystorePassword() throws Exception {
// Arrange
NiFiRegistryProperties properties = new NiFiRegistryProperties()
properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE, "src/test/resources/truststore.jks")
properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE_PASSWD, truststorePassword)
properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE_TYPE, "JKS")
properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE, "src/test/resources/keystoreSamePassword.jks")
properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE_PASSWD, matchingPassword)
properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE_TYPE, "JKS")
Server internalServer = new Server()
JettyServer testServer = new JettyServer(internalServer, properties)
// Act
SslContextFactory sslContextFactory = testServer.createSslContextFactory()
sslContextFactory.start()
// Assert
assertNotNull(sslContextFactory)
assertNotNull(sslContextFactory.getSslContext())
}
@Test
void testCreateSslContextFactoryWithMatchingPasswordsDefined() throws Exception {
// Arrange
NiFiRegistryProperties properties = new NiFiRegistryProperties()
properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE, "src/test/resources/truststore.jks")
properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE_PASSWD, truststorePassword)
properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE_TYPE, "JKS")
properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE, "src/test/resources/keystoreSamePassword.jks")
properties.setProperty(NiFiRegistryProperties.SECURITY_KEY_PASSWD, matchingPassword)
properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE_PASSWD, matchingPassword)
properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE_TYPE, "JKS")
Server internalServer = new Server()
JettyServer testServer = new JettyServer(internalServer, properties)
// Act
SslContextFactory sslContextFactory = testServer.createSslContextFactory()
sslContextFactory.start()
// Assert
assertNotNull(sslContextFactory)
assertNotNull(sslContextFactory.getSslContext())
}
@Rule public ExpectedException exception = ExpectedException.none()
@Test
void testCreateSslContextFactoryWithNoKeystorePasswordFails() throws Exception {
// Arrange
exception.expect(IllegalArgumentException.class)
exception.expectMessage("The keystore password cannot be null or empty")
NiFiRegistryProperties properties = new NiFiRegistryProperties()
properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE, "src/test/resources/truststore.jks")
properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE_PASSWD, truststorePassword)
properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE_TYPE, "JKS")
properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE, "src/test/resources/keystoreSamePassword.jks")
properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE_TYPE, "JKS")
Server internalServer = new Server()
JettyServer testServer = new JettyServer(internalServer, properties)
// Act but expect exception
SslContextFactory sslContextFactory = testServer.createSslContextFactory()
}
@Test
void testCreateSslContextFactoryWithCipherSuites() throws Exception {
// Arrange
NiFiRegistryProperties properties = new NiFiRegistryProperties()
properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE, "src/test/resources/truststore.jks")
properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE_PASSWD, truststorePassword)
properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE_TYPE, "JKS")
properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE, "src/test/resources/keystoreSamePassword.jks")
properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE_PASSWD, matchingPassword)
properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE_TYPE, "JKS")
properties.setProperty(NiFiRegistryProperties.WEB_HTTPS_CIPHERSUITES_INCLUDE, "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,TLS_DHE_RSA_WITH_AES_256_GCM_SHA384")
properties.setProperty(NiFiRegistryProperties.WEB_HTTPS_CIPHERSUITES_EXCLUDE, "BAD_CIPHER")
Server internalServer = new Server()
JettyServer testServer = new JettyServer(internalServer, properties)
// Act
SslContextFactory sslContextFactory = testServer.createSslContextFactory()
sslContextFactory.start()
// Assert this
assertNotNull(sslContextFactory)
assertNotNull(sslContextFactory.getSslContext())
assertEquals("INCLUDE_CIPHER_SUITES", sslContextFactory.getIncludeCipherSuites(), ["TLS_DHE_RSA_WITH_AES_128_GCM_SHA256","TLS_DHE_RSA_WITH_AES_256_GCM_SHA384"])
assertEquals("EXCLUDE_CIPHER_SUITES", sslContextFactory.getExcludeCipherSuites(), ["BAD_CIPHER"])
}
}

View File

@ -0,0 +1,146 @@
/*
* 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.registry.jetty.connector;
import org.apache.nifi.registry.properties.NiFiRegistryProperties;
import org.apache.nifi.remote.io.socket.NetworkUtils;
import org.apache.nifi.security.util.TemporaryKeyStoreBuilder;
import org.apache.nifi.security.util.TlsConfiguration;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Properties;
import static org.junit.jupiter.api.Assertions.assertEquals;
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 ApplicationServerConnectorFactoryTest {
private static final String SSL_PROTOCOL = "ssl";
private static final String H2_PROTOCOL = "h2";
private static final String INCLUDE_CIPHERS = ".*GCM.*";
private static final String EXCLUDE_CIPHERS = "*.CBC.*";
private static final String LOCALHOST = "127.0.0.1";
static TlsConfiguration tlsConfiguration;
Server server;
@BeforeAll
static void setTlsConfiguration() {
tlsConfiguration = new TemporaryKeyStoreBuilder().build();
}
@BeforeEach
void setServer() {
server = new Server();
}
@Test
void testGetServerConnectorRequiredProperties() {
final int port = NetworkUtils.getAvailableTcpPort();
final Properties configuredProperties = new Properties();
configuredProperties.put(NiFiRegistryProperties.WEB_HTTP_PORT, Integer.toString(port));
final NiFiRegistryProperties properties = getProperties(configuredProperties);
final ApplicationServerConnectorFactory factory = new ApplicationServerConnectorFactory(server, properties);
final ServerConnector serverConnector = factory.getServerConnector();
assertNotNull(serverConnector);
assertNull(serverConnector.getHost());
}
@Test
void testGetServerConnectorHostProperty() {
final int port = NetworkUtils.getAvailableTcpPort();
final Properties configuredProperties = new Properties();
configuredProperties.put(NiFiRegistryProperties.WEB_HTTP_PORT, Integer.toString(port));
configuredProperties.put(NiFiRegistryProperties.WEB_HTTP_HOST, LOCALHOST);
final NiFiRegistryProperties properties = getProperties(configuredProperties);
final ApplicationServerConnectorFactory factory = new ApplicationServerConnectorFactory(server, properties);
final ServerConnector serverConnector = factory.getServerConnector();
assertNotNull(serverConnector);
assertEquals(LOCALHOST, serverConnector.getHost());
}
@Test
void testGetServerConnectorSslProperties() {
final int port = NetworkUtils.getAvailableTcpPort();
final Properties configuredProperties = getSecurityProperties();
configuredProperties.put(NiFiRegistryProperties.WEB_HTTPS_PORT, Integer.toString(port));
final NiFiRegistryProperties properties = getProperties(configuredProperties);
final ApplicationServerConnectorFactory factory = new ApplicationServerConnectorFactory(server, properties);
final ServerConnector serverConnector = factory.getServerConnector();
assertNotNull(serverConnector);
assertNull(serverConnector.getHost());
assertTrue(serverConnector.getProtocols().contains(SSL_PROTOCOL));
}
@Test
void testGetServerConnectorHttp2Properties() {
final int port = NetworkUtils.getAvailableTcpPort();
final Properties configuredProperties = getSecurityProperties();
configuredProperties.put(NiFiRegistryProperties.WEB_HTTPS_PORT, Integer.toString(port));
configuredProperties.put(NiFiRegistryProperties.WEB_HTTPS_APPLICATION_PROTOCOLS, H2_PROTOCOL);
configuredProperties.put(NiFiRegistryProperties.WEB_HTTPS_CIPHERSUITES_INCLUDE, INCLUDE_CIPHERS);
configuredProperties.put(NiFiRegistryProperties.WEB_HTTPS_CIPHERSUITES_EXCLUDE, EXCLUDE_CIPHERS);
final NiFiRegistryProperties properties = getProperties(configuredProperties);
final ApplicationServerConnectorFactory factory = new ApplicationServerConnectorFactory(server, properties);
final ServerConnector serverConnector = factory.getServerConnector();
assertNotNull(serverConnector);
assertNull(serverConnector.getHost());
final List<String> protocols = serverConnector.getProtocols();
assertTrue(protocols.contains(SSL_PROTOCOL));
assertTrue(protocols.contains(H2_PROTOCOL));
}
private Properties getSecurityProperties() {
final Properties securityProperties = new Properties();
securityProperties.put(NiFiRegistryProperties.SECURITY_KEYSTORE, tlsConfiguration.getKeystorePath());
securityProperties.put(NiFiRegistryProperties.SECURITY_KEYSTORE_TYPE, tlsConfiguration.getKeystoreType().getType());
securityProperties.put(NiFiRegistryProperties.SECURITY_KEYSTORE_PASSWD, tlsConfiguration.getKeystorePassword());
securityProperties.put(NiFiRegistryProperties.SECURITY_TRUSTSTORE, tlsConfiguration.getTruststorePath());
securityProperties.put(NiFiRegistryProperties.SECURITY_TRUSTSTORE_TYPE, tlsConfiguration.getTruststoreType().getType());
securityProperties.put(NiFiRegistryProperties.SECURITY_TRUSTSTORE_PASSWD, tlsConfiguration.getTruststorePassword());
return securityProperties;
}
private NiFiRegistryProperties getProperties(final Properties configuredProperties) {
return new NiFiRegistryProperties(configuredProperties);
}
}

View File

@ -53,6 +53,7 @@ public class NiFiRegistryProperties extends ApplicationProperties {
public static final String WEB_HTTPS_HOST = "nifi.registry.web.https.host"; public static final String WEB_HTTPS_HOST = "nifi.registry.web.https.host";
public static final String WEB_HTTPS_CIPHERSUITES_INCLUDE = "nifi.registry.web.https.ciphersuites.include"; public static final String WEB_HTTPS_CIPHERSUITES_INCLUDE = "nifi.registry.web.https.ciphersuites.include";
public static final String WEB_HTTPS_CIPHERSUITES_EXCLUDE = "nifi.registry.web.https.ciphersuites.exclude"; public static final String WEB_HTTPS_CIPHERSUITES_EXCLUDE = "nifi.registry.web.https.ciphersuites.exclude";
public static final String WEB_HTTPS_APPLICATION_PROTOCOLS = "nifi.registry.web.https.application.protocols";
public static final String WEB_WORKING_DIR = "nifi.registry.web.jetty.working.directory"; public static final String WEB_WORKING_DIR = "nifi.registry.web.jetty.working.directory";
public static final String WEB_THREADS = "nifi.registry.web.jetty.threads"; public static final String WEB_THREADS = "nifi.registry.web.jetty.threads";
public static final String WEB_SHOULD_SEND_SERVER_VERSION = "nifi.registry.web.should.send.server.version"; public static final String WEB_SHOULD_SEND_SERVER_VERSION = "nifi.registry.web.should.send.server.version";
@ -119,6 +120,7 @@ public class NiFiRegistryProperties extends ApplicationProperties {
// Defaults // Defaults
public static final String DEFAULT_WEB_WORKING_DIR = "./work/jetty"; public static final String DEFAULT_WEB_WORKING_DIR = "./work/jetty";
public static final String DEFAULT_WEB_HTTPS_APPLICATION_PROTOCOLS = "http/1.1";
public static final String DEFAULT_WAR_DIR = "./lib"; public static final String DEFAULT_WAR_DIR = "./lib";
public static final String DEFAULT_PROVIDERS_CONFIGURATION_FILE = "./conf/providers.xml"; public static final String DEFAULT_PROVIDERS_CONFIGURATION_FILE = "./conf/providers.xml";
public static final String DEFAULT_REGISTRY_ALIAS_CONFIGURATION_FILE = "./conf/registry-aliases.xml"; public static final String DEFAULT_REGISTRY_ALIAS_CONFIGURATION_FILE = "./conf/registry-aliases.xml";
@ -229,6 +231,16 @@ public class NiFiRegistryProperties extends ApplicationProperties {
return new File(getProperty(WEB_WORKING_DIR, DEFAULT_WEB_WORKING_DIR)); return new File(getProperty(WEB_WORKING_DIR, DEFAULT_WEB_WORKING_DIR));
} }
/**
* Get Web HTTPS Application Protocols defaults to HTTP/1.1
*
* @return Set of configured HTTPS Application Protocols
*/
public Set<String> getWebHttpsApplicationProtocols() {
final String protocols = getProperty(WEB_HTTPS_APPLICATION_PROTOCOLS, DEFAULT_WEB_HTTPS_APPLICATION_PROTOCOLS);
return Arrays.stream(protocols.split("\\s+")).collect(Collectors.toSet());
}
public File getExtensionsWorkingDirectory() { public File getExtensionsWorkingDirectory() {
return new File(getProperty(EXTENSIONS_WORKING_DIR, DEFAULT_EXTENSIONS_WORKING_DIR)); return new File(getProperty(EXTENSIONS_WORKING_DIR, DEFAULT_EXTENSIONS_WORKING_DIR));
} }

View File

@ -19,6 +19,7 @@ nifi.registry.web.http.host=${nifi.registry.web.http.host}
nifi.registry.web.http.port=${nifi.registry.web.http.port} nifi.registry.web.http.port=${nifi.registry.web.http.port}
nifi.registry.web.https.host=${nifi.registry.web.https.host} nifi.registry.web.https.host=${nifi.registry.web.https.host}
nifi.registry.web.https.port=${nifi.registry.web.https.port} nifi.registry.web.https.port=${nifi.registry.web.https.port}
nifi.registry.web.https.application.protocols=${nifi.registry.web.https.application.protocols}
nifi.registry.web.jetty.working.directory=${nifi.registry.jetty.work.dir} nifi.registry.web.jetty.working.directory=${nifi.registry.jetty.work.dir}
nifi.registry.web.jetty.threads=${nifi.registry.web.jetty.threads} nifi.registry.web.jetty.threads=${nifi.registry.web.jetty.threads}
nifi.registry.web.should.send.server.version=${nifi.registry.web.should.send.server.version} nifi.registry.web.should.send.server.version=${nifi.registry.web.should.send.server.version}

View File

@ -93,6 +93,18 @@
<artifactId>apache-jstl</artifactId> <artifactId>apache-jstl</artifactId>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-server</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-server</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<!-- lib/java11 --> <!-- lib/java11 -->
<dependency> <dependency>
<groupId>jakarta.xml.bind</groupId> <groupId>jakarta.xml.bind</groupId>