diff --git a/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/StandardServerConnectorFactory.java b/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/StandardServerConnectorFactory.java index e670ac71ea12..22679ee81a68 100644 --- a/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/StandardServerConnectorFactory.java +++ b/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/StandardServerConnectorFactory.java @@ -162,7 +162,11 @@ public void setApplicationLayerProtocols(final Set app this.applicationLayerProtocols = applicationLayerProtocols; } - private HttpConfiguration getHttpConfiguration() { + protected Server getServer() { + return server; + } + + protected HttpConfiguration getHttpConfiguration() { final HttpConfiguration httpConfiguration = new HttpConfiguration(); if (sslContext != null) { @@ -177,7 +181,7 @@ private HttpConfiguration getHttpConfiguration() { return httpConfiguration; } - private SslContextFactory.Server getSslContextFactory() { + protected SslContextFactory.Server getSslContextFactory() { final SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setSslContext(sslContext); sslContextFactory.setNeedClientAuth(needClientAuth); diff --git a/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/alpn/StandardALPNProcessor.java b/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/alpn/StandardALPNProcessor.java index 0c8825226d57..2e813f98ffe2 100644 --- a/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/alpn/StandardALPNProcessor.java +++ b/nifi-commons/nifi-jetty-configuration/src/main/java/org/apache/nifi/jetty/configuration/connector/alpn/StandardALPNProcessor.java @@ -117,7 +117,7 @@ public void handshakeSucceeded(final Event event) { */ @Override public void handshakeFailed(final Event event, final Throwable failure) { - logger.warn("Connection Remote Address [{}] Handshake Failed", serverConnection.getEndPoint().getRemoteAddress(), failure); + logger.debug("Connection Remote Address [{}] Handshake Failed", serverConnection.getEndPoint().getRemoteAddress(), failure); } } } diff --git a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java index 78b9ae5af920..995c720271a7 100644 --- a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java +++ b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java @@ -225,6 +225,7 @@ public class NiFiProperties extends ApplicationProperties { public static final String WEB_HTTPS_PORT = "nifi.web.https.port"; public static final String WEB_HTTPS_PORT_FORWARDING = "nifi.web.https.port.forwarding"; public static final String WEB_HTTPS_HOST = "nifi.web.https.host"; + public static final String WEB_HTTPS_APPLICATION_PROTOCOLS = "nifi.web.https.application.protocols"; public static final String WEB_HTTPS_CIPHERSUITES_INCLUDE = "nifi.web.https.ciphersuites.include"; public static final String WEB_HTTPS_CIPHERSUITES_EXCLUDE = "nifi.web.https.ciphersuites.exclude"; public static final String WEB_HTTPS_NETWORK_INTERFACE_PREFIX = "nifi.web.https.network.interface."; @@ -334,6 +335,7 @@ public class NiFiProperties extends ApplicationProperties { public static final String DEFAULT_LOGIN_IDENTITY_PROVIDER_CONFIGURATION_FILE = "conf/login-identity-providers.xml"; public static final Integer DEFAULT_REMOTE_INPUT_PORT = null; public static final Path DEFAULT_TEMPLATE_DIRECTORY = Paths.get("conf", "templates"); + private static final String DEFAULT_WEB_HTTPS_APPLICATION_PROTOCOLS = "http/1.1"; public static final int DEFAULT_WEB_THREADS = 200; public static final String DEFAULT_WEB_MAX_HEADER_SIZE = "16 KB"; public static final String DEFAULT_WEB_WORKING_DIR = "./work/jetty"; @@ -705,6 +707,16 @@ public Integer getConfiguredHttpOrHttpsPort() throws RuntimeException { } } + /** + * Get Web HTTPS Application Protocols defaults to HTTP/1.1 + * + * @return Set of configured HTTPS Application Protocols + */ + public Set getWebHttpsApplicationProtocols() { + final String protocols = getProperty(WEB_HTTPS_APPLICATION_PROTOCOLS, DEFAULT_WEB_HTTPS_APPLICATION_PROTOCOLS); + return Arrays.stream(protocols.split("\\s+")).collect(Collectors.toSet()); + } + public String getWebMaxHeaderSize() { return getProperty(WEB_MAX_HEADER_SIZE, DEFAULT_WEB_MAX_HEADER_SIZE); } diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc index d61b2134efc3..11df4880138e 100644 --- a/nifi-docs/src/main/asciidoc/administration-guide.adoc +++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc @@ -3884,6 +3884,13 @@ For example, to provide two additional network interfaces, a user could also spe `nifi.web.https.network.interface.eth1=eth1` + + Providing three total network interfaces, including `nifi.web.https.network.interface.default`. +|`nifi.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.web.jetty.working.directory`|The location of the Jetty working directory. The default value is `./work/jetty`. |`nifi.web.jetty.threads`|The number of Jetty threads. The default value is `200`. |`nifi.web.max.header.size`|The maximum size allowed for request and response headers. The default value is `16 KB`. diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml index 0827a19fdb59..8bd65c08c4f8 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml @@ -126,6 +126,7 @@ 127.0.0.1 8443 + http/1.1 ./work/jetty 200 16 KB diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties index 1426b27280ca..c0a593d23a89 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties @@ -151,6 +151,7 @@ nifi.web.http.network.interface.default=${nifi.web.http.network.interface.defaul nifi.web.https.host=${nifi.web.https.host} nifi.web.https.port=${nifi.web.https.port} nifi.web.https.network.interface.default=${nifi.web.https.network.interface.default} +nifi.web.https.application.protocols=${nifi.web.https.application.protocols} nifi.web.jetty.working.directory=${nifi.jetty.work.dir} nifi.web.jetty.threads=${nifi.web.jetty.threads} nifi.web.max.header.size=${nifi.web.max.header.size} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/pom.xml index 07b052e923d0..5483945eddd2 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/pom.xml @@ -152,6 +152,19 @@ nifi-ui-extension compile + + org.apache.nifi + nifi-jetty-configuration + ${project.version} + + + org.eclipse.jetty.http2 + http2-server + + + org.eclipse.jetty + jetty-alpn-server + org.apache.nifi nifi-web-security diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java index f4d794ec8ab4..199b10e8906b 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java @@ -51,37 +51,29 @@ import org.apache.nifi.nar.NarThreadContextClassLoader; import org.apache.nifi.nar.StandardExtensionDiscoveringManager; import org.apache.nifi.nar.StandardNarLoader; -import org.apache.nifi.processor.DataUnit; -import org.apache.nifi.security.util.KeyStoreUtils; -import org.apache.nifi.security.util.TlsConfiguration; import org.apache.nifi.security.util.TlsException; import org.apache.nifi.services.FlowService; import org.apache.nifi.ui.extension.UiExtension; import org.apache.nifi.ui.extension.UiExtensionMapping; -import org.apache.nifi.util.FormatUtils; import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.web.ContentAccess; import org.apache.nifi.web.NiFiWebConfigurationContext; import org.apache.nifi.web.UiExtensionType; +import org.apache.nifi.web.server.connector.FrameworkServerConnectorFactory; import org.apache.nifi.web.server.filter.FilterParameter; import org.apache.nifi.web.server.filter.RequestFilterProvider; import org.apache.nifi.web.server.filter.RestApiRequestFilterProvider; import org.apache.nifi.web.server.filter.StandardRequestFilterProvider; import org.apache.nifi.web.server.log.RequestLogProvider; import org.apache.nifi.web.server.log.StandardRequestLogProvider; -import org.apache.nifi.web.server.util.TrustStoreScanner; import org.eclipse.jetty.annotations.AnnotationConfiguration; import org.eclipse.jetty.deploy.App; import org.eclipse.jetty.deploy.DeploymentManager; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.HttpConfiguration; -import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.RequestLog; -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.ContextHandlerCollection; import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.server.handler.HandlerList; @@ -89,8 +81,6 @@ import org.eclipse.jetty.servlet.DefaultServlet; import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.util.ssl.KeyStoreScanner; -import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.webapp.Configuration; import org.eclipse.jetty.webapp.JettyWebXmlConfiguration; @@ -111,8 +101,8 @@ import java.io.FileFilter; import java.io.IOException; import java.io.InputStreamReader; -import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.io.UncheckedIOException; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; @@ -130,7 +120,6 @@ import java.util.Objects; import java.util.Set; import java.util.UUID; -import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -164,17 +153,11 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader { private static final String DEFAULT_NAR_PROVIDER_POLL_INTERVAL = "5 min"; private static final String DEFAULT_NAR_PROVIDER_CONFLICT_RESOLUTION = "IGNORE"; - private static final int DOS_FILTER_REJECT_REQUEST = -1; - private static final FileFilter WAR_FILTER = pathname -> { final String nameToTest = pathname.getName().toLowerCase(); return nameToTest.endsWith(".war") && pathname.isFile(); }; - // property parsing util - private static final String REGEX_SPLIT_PROPERTY = ",\\s*"; - protected static final String JOIN_ARRAY = ", "; - private Server server; private NiFiProperties props; @@ -184,7 +167,6 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader { private NarAutoLoader narAutoLoader; private ExternalResourceProviderService narProviderService; private DiagnosticsFactory diagnosticsFactory; - private SslContextFactory.Server sslContextFactory; private DecommissionTask decommissionTask; private StatusHistoryDumpFactory statusHistoryDumpFactory; @@ -342,7 +324,6 @@ private Handler loadInitialWars(final Set bundles) { return gzip(webAppContextHandlers); } - @Override public void loadExtensionUis(final Set bundles) { // Find and load any WARs contained within the set of bundles... @@ -661,7 +642,7 @@ private WebAppContext loadWar(final File warFile, final String contextPath, fina private void addDocsServlets(WebAppContext docsContext) { try { // Load the nifi/docs directory - final File docsDir = getDocsDir("docs"); + final File docsDir = getDocsDir(); // load the component documentation working directory final File componentDocsDirPath = props.getComponentDocumentationWorkingDirectory(); @@ -705,10 +686,10 @@ private void addDocsServlets(WebAppContext docsContext) { * is that the documentation links under the 'General' portion of the help * page will not be accessible, but at least the process will be running. * - * @param docsDirectory Name of documentation directory in installation directory. * @return A File object to the documentation directory; else startUpFailure called. */ - private File getDocsDir(final String docsDirectory) { + private File getDocsDir() { + final String docsDirectory = "docs"; File docsDir; try { docsDir = Paths.get(docsDirectory).toRealPath().toFile(); @@ -748,268 +729,43 @@ private File getWebApiDocsDir() { return webApiDocsDir; } - private void configureConnectors(final Server server) throws ServerConfigurationException { - // create the http configuration - final HttpConfiguration httpConfiguration = new HttpConfiguration(); - final int headerSize = DataUnit.parseDataSize(props.getWebMaxHeaderSize(), DataUnit.B).intValue(); - httpConfiguration.setRequestHeaderSize(headerSize); - httpConfiguration.setResponseHeaderSize(headerSize); - httpConfiguration.setSendServerVersion(props.shouldSendServerVersion()); - - // Check if both HTTP and HTTPS connectors are configured and fail if both are configured - if (bothHttpAndHttpsConnectorsConfigured(props)) { - logger.error("NiFi only supports one mode of HTTP or HTTPS operation, not both simultaneously. " + - "Check the nifi.properties file and ensure that either the HTTP hostname and port or the HTTPS hostname and port are empty"); - startUpFailure(new IllegalStateException("Only one of the HTTP and HTTPS connectors can be configured at one time")); - } - - if (props.getSslPort() != null) { - configureHttpsConnector(server, httpConfiguration); - } else if (props.getPort() != null) { - configureHttpConnector(server, httpConfiguration); - } else { - logger.error("Neither the HTTP nor HTTPS connector was configured in nifi.properties"); - startUpFailure(new IllegalStateException("Must configure HTTP or HTTPS connector")); - } - } - - /** - * Configures an HTTPS connector and adds it to the server. - * - * @param server the Jetty server instance - * @param httpConfiguration the configuration object for the HTTPS protocol settings - */ - private void configureHttpsConnector(Server server, HttpConfiguration httpConfiguration) { - String hostname = props.getProperty(NiFiProperties.WEB_HTTPS_HOST); - final Integer port = props.getSslPort(); - String connectorLabel = "HTTPS"; - final Map httpsNetworkInterfaces = props.getHttpsNetworkInterfaces(); - ServerConnectorCreator scc = (s, c) -> createUnconfiguredSslServerConnector(s, c, port); - - configureGenericConnector(server, httpConfiguration, hostname, port, connectorLabel, httpsNetworkInterfaces, scc); - - if (props.isSecurityAutoReloadEnabled()) { - configureSslContextFactoryReloading(server); - } - } - - /** - * Configures a KeyStoreScanner and TrustStoreScanner at the configured reload intervals. This will - * reload the SSLContextFactory if any changes are detected to the keystore or truststore. - * - * @param server The Jetty server - */ - private void configureSslContextFactoryReloading(Server server) { - final int scanIntervalSeconds = Double.valueOf(FormatUtils.getPreciseTimeDuration( - props.getSecurityAutoReloadInterval(), TimeUnit.SECONDS)) - .intValue(); - - final KeyStoreScanner keyStoreScanner = new KeyStoreScanner(sslContextFactory); - keyStoreScanner.setScanInterval(scanIntervalSeconds); - server.addBean(keyStoreScanner); - - final TrustStoreScanner trustStoreScanner = new TrustStoreScanner(sslContextFactory); - trustStoreScanner.setScanInterval(scanIntervalSeconds); - server.addBean(trustStoreScanner); - } - - /** - * Configures an HTTP connector and adds it to the server. - * - * @param server the Jetty server instance - * @param httpConfiguration the configuration object for the HTTP protocol settings - */ - private void configureHttpConnector(Server server, HttpConfiguration httpConfiguration) { - String hostname = props.getProperty(NiFiProperties.WEB_HTTP_HOST); - final Integer port = props.getPort(); - String connectorLabel = "HTTP"; - final Map httpNetworkInterfaces = props.getHttpNetworkInterfaces(); - ServerConnectorCreator scc = (s, c) -> new ServerConnector(s, new HttpConnectionFactory(c)); - - configureGenericConnector(server, httpConfiguration, hostname, port, connectorLabel, httpNetworkInterfaces, scc); - } - - /** - * Configures an HTTP(S) connector for the server given the provided parameters. The functionality between HTTP and HTTPS connectors is largely similar. - * Here the common behavior has been extracted into a shared method and the respective calling methods obtain the right values and a lambda function for the differing behavior. - * - * @param server the Jetty server instance - * @param configuration the HTTP/HTTPS configuration instance - * @param hostname the hostname from the nifi.properties file - * @param port the port to expose - * @param connectorLabel used for log output (e.g. "HTTP" or "HTTPS") - * @param networkInterfaces the map of network interfaces from nifi.properties - * @param serverConnectorCreator a function which accepts a {@code Server} and {@code HttpConnection} instance and returns a {@code ServerConnector} - */ - private void configureGenericConnector(Server server, HttpConfiguration configuration, String hostname, Integer port, String connectorLabel, Map networkInterfaces, - ServerConnectorCreator serverConnectorCreator) { - if (port < 0 || (int) Math.pow(2, 16) <= port) { - throw new ServerConfigurationException("Invalid " + connectorLabel + " port: " + port); - } - - logger.info("Configuring Jetty for " + connectorLabel + " on port: " + port); - - final List serverConnectors = new ArrayList<>(); - - // Calculate Idle Timeout as twice the auto-refresh interval. This ensures that even with some variance in timing, - // we are able to avoid closing connections from users' browsers most of the time. This can make a significant difference - // in HTTPS connections, as each HTTPS connection that is established must perform the SSL handshake. - final String autoRefreshInterval = props.getAutoRefreshInterval(); - final long autoRefreshMillis = autoRefreshInterval == null ? 30000L : FormatUtils.getTimeDuration(autoRefreshInterval, TimeUnit.MILLISECONDS); - final long idleTimeout = autoRefreshMillis * 2; - - // If the interfaces collection is empty or each element is empty - if (networkInterfaces.isEmpty() || networkInterfaces.values().stream().filter(value -> StringUtils.isNotBlank(value)).collect(Collectors.toList()).isEmpty()) { - final ServerConnector serverConnector = serverConnectorCreator.create(server, configuration); - - // Set host and port - if (StringUtils.isNotBlank(hostname)) { - serverConnector.setHost(hostname); - } - serverConnector.setPort(port); - serverConnector.setIdleTimeout(idleTimeout); - serverConnectors.add(serverConnector); - } else { - // Add connectors for all IPs from network interfaces - serverConnectors.addAll(new ArrayList<>(networkInterfaces.values().stream().map(ifaceName -> { - NetworkInterface iface = null; - try { - iface = NetworkInterface.getByName(ifaceName); - } catch (SocketException e) { - logger.error("Unable to get network interface by name {}", ifaceName, e); - } - if (iface == null) { - logger.warn("Unable to find network interface named {}", ifaceName); + private void configureConnectors(final Server server) { + try { + final FrameworkServerConnectorFactory serverConnectorFactory = new FrameworkServerConnectorFactory(server, props); + final Map interfaces = props.isHTTPSConfigured() ? props.getHttpsNetworkInterfaces() : props.getHttpNetworkInterfaces(); + final Set interfaceNames = interfaces.values().stream().filter(StringUtils::isNotBlank).collect(Collectors.toSet()); + // Add Server Connectors based on configured Network Interface Names + if (interfaceNames.isEmpty()) { + final ServerConnector serverConnector = serverConnectorFactory.getServerConnector(); + final String host = props.isHTTPSConfigured() ? props.getProperty(NiFiProperties.WEB_HTTPS_HOST) : props.getProperty(NiFiProperties.WEB_HTTP_HOST); + if (StringUtils.isNotBlank(host)) { + serverConnector.setHost(host); } - return iface; - }).filter(Objects::nonNull).flatMap(iface -> Collections.list(iface.getInetAddresses()).stream()) - .map(inetAddress -> { - final ServerConnector serverConnector = serverConnectorCreator.create(server, configuration); - - // Set host and port - serverConnector.setHost(inetAddress.getHostAddress()); - serverConnector.setPort(port); - serverConnector.setIdleTimeout(idleTimeout); - - return serverConnector; - }).collect(Collectors.toList()))); - } - // Add all connectors - serverConnectors.forEach(server::addConnector); - } - - /** - * Returns true if there are configured properties for both HTTP and HTTPS connectors (specifically port because the hostname can be left blank in the HTTP connector). - * Prints a warning log message with the relevant properties. - * - * @param props the NiFiProperties - * @return true if both ports are present - */ - static boolean bothHttpAndHttpsConnectorsConfigured(NiFiProperties props) { - Integer httpPort = props.getPort(); - String httpHostname = props.getProperty(NiFiProperties.WEB_HTTP_HOST); - - Integer httpsPort = props.getSslPort(); - String httpsHostname = props.getProperty(NiFiProperties.WEB_HTTPS_HOST); - - if (httpPort != null && httpsPort != null) { - logger.warn("Both the HTTP and HTTPS connectors are configured in nifi.properties. Only one of these connectors should be configured. See the NiFi Admin Guide for more details"); - logger.warn("HTTP connector: http://" + httpHostname + ":" + httpPort); - logger.warn("HTTPS connector: https://" + httpsHostname + ":" + httpsPort); - return true; - } - - return false; - } - - private ServerConnector createUnconfiguredSslServerConnector(Server server, HttpConfiguration httpConfiguration, int port) { - // add some secure config - final HttpConfiguration httpsConfiguration = new HttpConfiguration(httpConfiguration); - httpsConfiguration.setSecureScheme("https"); - httpsConfiguration.setSecurePort(port); - httpsConfiguration.setSendServerVersion(props.shouldSendServerVersion()); - httpsConfiguration.addCustomizer(new SecureRequestCustomizer()); - - // build the connector - return new ServerConnector(server, - new SslConnectionFactory(createSslContextFactory(), "http/1.1"), - new HttpConnectionFactory(httpsConfiguration)); - } - - private SslContextFactory createSslContextFactory() { - final SslContextFactory.Server serverContextFactory = new SslContextFactory.Server(); - configureSslContextFactory(serverContextFactory, props); - this.sslContextFactory = serverContextFactory; - return serverContextFactory; - } - - protected static void configureSslContextFactory(SslContextFactory.Server contextFactory, NiFiProperties props) { - // Explicitly exclude legacy TLS protocol versions - contextFactory.setIncludeProtocols(TlsConfiguration.getCurrentSupportedTlsProtocolVersions()); - contextFactory.setExcludeProtocols("TLS", "TLSv1", "TLSv1.1", "SSL", "SSLv2", "SSLv2Hello", "SSLv3"); - - // on configuration, replace default application cipher suites with those configured - final String includeCipherSuitesProps = props.getProperty(NiFiProperties.WEB_HTTPS_CIPHERSUITES_INCLUDE); - if (StringUtils.isNotEmpty(includeCipherSuitesProps)) { - final String[] includeCipherSuites = includeCipherSuitesProps.split(REGEX_SPLIT_PROPERTY); - logger.info("Setting include cipher suites from configuration; parsed property = [{}].", - StringUtils.join(includeCipherSuites, JOIN_ARRAY)); - contextFactory.setIncludeCipherSuites(includeCipherSuites); - } - final String excludeCipherSuitesProps = props.getProperty(NiFiProperties.WEB_HTTPS_CIPHERSUITES_EXCLUDE); - if (StringUtils.isNotEmpty(excludeCipherSuitesProps)) { - final String[] excludeCipherSuites = excludeCipherSuitesProps.split(REGEX_SPLIT_PROPERTY); - logger.info("Setting exclude cipher suites from configuration; parsed property = [{}].", - StringUtils.join(excludeCipherSuites, JOIN_ARRAY)); - contextFactory.setExcludeCipherSuites(excludeCipherSuites); - } - - // require client auth when not supporting login, Kerberos service, or anonymous access - if (props.isClientAuthRequiredForRestApi()) { - contextFactory.setNeedClientAuth(true); - } else { - contextFactory.setWantClientAuth(true); - } - - /* below code sets JSSE system properties when values are provided */ - // keystore properties - if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.SECURITY_KEYSTORE))) { - contextFactory.setKeyStorePath(props.getProperty(NiFiProperties.SECURITY_KEYSTORE)); - } - String keyStoreType = props.getProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE); - if (StringUtils.isNotBlank(keyStoreType)) { - contextFactory.setKeyStoreType(keyStoreType); - String keyStoreProvider = KeyStoreUtils.getKeyStoreProvider(keyStoreType); - if (StringUtils.isNoneEmpty(keyStoreProvider)) { - contextFactory.setKeyStoreProvider(keyStoreProvider); - } - } - final String keystorePassword = props.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD); - final String keyPassword = props.getProperty(NiFiProperties.SECURITY_KEY_PASSWD); - if (StringUtils.isNotBlank(keystorePassword)) { - // if no key password was provided, then assume the keystore password is the same as the key password. - final String defaultKeyPassword = (StringUtils.isBlank(keyPassword)) ? keystorePassword : keyPassword; - contextFactory.setKeyStorePassword(keystorePassword); - contextFactory.setKeyManagerPassword(defaultKeyPassword); - } else if (StringUtils.isNotBlank(keyPassword)) { - // since no keystore password was provided, there will be no keystore integrity check - contextFactory.setKeyManagerPassword(keyPassword); - } - - // truststore properties - if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE))) { - contextFactory.setTrustStorePath(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE)); - } - String trustStoreType = props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE); - if (StringUtils.isNotBlank(trustStoreType)) { - contextFactory.setTrustStoreType(trustStoreType); - String trustStoreProvider = KeyStoreUtils.getKeyStoreProvider(trustStoreType); - if (StringUtils.isNoneEmpty(trustStoreProvider)) { - contextFactory.setTrustStoreProvider(trustStoreProvider); + server.addConnector(serverConnector); + } else { + interfaceNames.stream() + // Map interface name properties to Network Interfaces + .map(interfaceName -> { + try { + return NetworkInterface.getByName(interfaceName); + } catch (final SocketException e) { + throw new UncheckedIOException(String.format("Network Interface [%s] not found", interfaceName), e); + } + }) + // Map Network Interfaces to host addresses + .filter(Objects::nonNull) + .flatMap(networkInterface -> Collections.list(networkInterface.getInetAddresses()).stream()) + .map(InetAddress::getHostAddress) + // Map host addresses to Server Connectors + .map(host -> { + final ServerConnector serverConnector = serverConnectorFactory.getServerConnector(); + serverConnector.setHost(host); + return serverConnector; + }) + .forEach(server::addConnector); } - } - if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD))) { - contextFactory.setTrustStorePassword(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD)); + } catch (final Throwable e) { + startUpFailure(e); } } @@ -1123,7 +879,7 @@ public void start() { logger.info("Loading Flow..."); ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(webApiContext.getServletContext()); - flowService = ctx.getBean("flowService", FlowService.class); + flowService = Objects.requireNonNull(ctx).getBean("flowService", FlowService.class); // start and load the flow flowService.start(); @@ -1157,7 +913,7 @@ public Map buildExternalResourceProviders( final Set externalSourceNames = props.getDirectSubsequentTokens(providerPropertyPrefix); for(final String externalSourceName : externalSourceNames) { - logger.info("External resource provider \'{}\' found in configuration", externalSourceName); + logger.info("External resource provider '{}' found in configuration", externalSourceName); final String providerClass = props.getProperty(providerPropertyPrefix + externalSourceName + "." + NAR_PROVIDER_IMPLEMENTATION_PROPERTY); final String providerId = UUID.randomUUID().toString(); @@ -1174,6 +930,7 @@ public Map buildExternalResourceProviders( * In case the provider class is not an implementation of {@code ExternalResourceProvider} the method tries to instantiate it as a {@code NarProvider}. {@code NarProvider} instances * are wrapped into an adapter in order to envelope the support. */ + @SuppressWarnings("deprecation") private ExternalResourceProvider createProviderInstance( final ExtensionManager extensionManager, final String providerClass, @@ -1185,7 +942,7 @@ private ExternalResourceProvider createProviderInstance( try { provider = NarThreadContextClassLoader.createInstance(extensionManager, providerClass, ExternalResourceProvider.class, props, providerId); } catch (final ClassCastException e) { - logger.warn("Class {} does not implement \"ExternalResourceProvider\" falling back to \"NarProvider\""); + logger.warn("Class {} does not implement ExternalResourceProvider falling back to NarProvider", providerClass); provider = new NarProviderAdapter(NarThreadContextClassLoader.createInstance(extensionManager, providerClass, NarProvider.class, props, providerId)); } @@ -1259,11 +1016,9 @@ private void dumpUrls() throws SocketException { hosts.add(serverConnector.getHost()); } else { Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); - if (networkInterfaces != null) { - for (NetworkInterface networkInterface : Collections.list(networkInterfaces)) { - for (InetAddress inetAddress : Collections.list(networkInterface.getInetAddresses())) { - hosts.add(inetAddress.getHostAddress()); - } + for (NetworkInterface networkInterface : Collections.list(networkInterfaces)) { + for (InetAddress inetAddress : Collections.list(networkInterface.getInetAddresses())) { + hosts.add(inetAddress.getHostAddress()); } } } @@ -1384,24 +1139,17 @@ public Map> getComponentUiExtensionsByType() { private static class ThreadDumpDiagnosticsFactory implements DiagnosticsFactory { @Override public DiagnosticsDump create(final boolean verbose) { - return new DiagnosticsDump() { - @Override - public void writeTo(final OutputStream out) throws IOException { - final DiagnosticsDumpElement threadDumpElement = new ThreadDumpTask().captureDump(verbose); - final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out)); - for (final String detail : threadDumpElement.getDetails()) { - writer.write(detail); - writer.write("\n"); - } - - writer.flush(); + return out -> { + final DiagnosticsDumpElement threadDumpElement = new ThreadDumpTask().captureDump(verbose); + final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out)); + for (final String detail : threadDumpElement.getDetails()) { + writer.write(detail); + writer.write("\n"); } + + writer.flush(); }; } } } -@FunctionalInterface -interface ServerConnectorCreator { - ServerConnector create(Server server, HttpConfiguration httpConfiguration); -} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/connector/FrameworkServerConnectorFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/connector/FrameworkServerConnectorFactory.java new file mode 100644 index 000000000000..3fe915382446 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/connector/FrameworkServerConnectorFactory.java @@ -0,0 +1,199 @@ +/* + * 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.web.server.connector; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.nifi.jetty.configuration.connector.ApplicationLayerProtocol; +import org.apache.nifi.jetty.configuration.connector.StandardServerConnectorFactory; +import org.apache.nifi.processor.DataUnit; +import org.apache.nifi.security.util.StandardTlsConfiguration; +import org.apache.nifi.security.util.TlsConfiguration; +import org.apache.nifi.security.util.TlsException; +import org.apache.nifi.security.util.TlsPlatform; +import org.apache.nifi.util.FormatUtils; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.web.server.util.TrustStoreScanner; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.ssl.KeyStoreScanner; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +import javax.net.ssl.SSLContext; + +import java.util.Arrays; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.apache.nifi.security.util.SslContextFactory.createSslContext; + +/** + * Framework extension of Server Connector Factory configures additional settings based on application properties + */ +public class FrameworkServerConnectorFactory extends StandardServerConnectorFactory { + private static final String DEFAULT_AUTO_REFRESH_INTERVAL = "30 s"; + + private static final int IDLE_TIMEOUT_MULTIPLIER = 2; + + private static final String CIPHER_SUITE_SEPARATOR_PATTERN = ",\\s*"; + + private final int headerSize; + + private final int idleTimeout; + + private final Integer storeScanInterval; + + private final String includeCipherSuites; + + private final String excludeCipherSuites; + + private TlsConfiguration tlsConfiguration; + + private SslContextFactory.Server sslContextFactory; + + /** + * Framework Server Connector Factory Constructor with required properties + * + * @param server Jetty Server + * @param properties NiFi Properties + */ + public FrameworkServerConnectorFactory(final Server server, final NiFiProperties properties) { + super(server, getPort(properties)); + + includeCipherSuites = properties.getProperty(NiFiProperties.WEB_HTTPS_CIPHERSUITES_INCLUDE); + excludeCipherSuites = properties.getProperty(NiFiProperties.WEB_HTTPS_CIPHERSUITES_EXCLUDE); + headerSize = DataUnit.parseDataSize(properties.getWebMaxHeaderSize(), DataUnit.B).intValue(); + idleTimeout = getIdleTimeout(properties); + + if (properties.isHTTPSConfigured()) { + tlsConfiguration = StandardTlsConfiguration.fromNiFiProperties(properties); + try { + final SSLContext sslContext = createSslContext(tlsConfiguration); + setSslContext(sslContext); + } catch (final TlsException e) { + throw new IllegalStateException("Invalid nifi.web.https configuration in nifi.properties", e); + } + + if (properties.isClientAuthRequiredForRestApi()) { + setNeedClientAuth(true); + } else { + setWantClientAuth(true); + } + + if (properties.isSecurityAutoReloadEnabled()) { + final String securityAutoReloadInterval = properties.getSecurityAutoReloadInterval(); + final double reloadIntervalSeconds = FormatUtils.getPreciseTimeDuration(securityAutoReloadInterval, TimeUnit.SECONDS); + storeScanInterval = (int) reloadIntervalSeconds; + } else { + storeScanInterval = null; + } + + setApplicationLayerProtocols(properties); + + // Set Transport Layer Security Protocols based on platform configuration + setIncludeSecurityProtocols(TlsPlatform.getPreferredProtocols().toArray(new String[0])); + } else { + storeScanInterval = null; + } + } + + /** + * Get HTTP Configuration with additional settings based on application properties + * + * @return HTTP Configuration + */ + @Override + protected HttpConfiguration getHttpConfiguration() { + final HttpConfiguration httpConfiguration = super.getHttpConfiguration(); + + httpConfiguration.setRequestHeaderSize(headerSize); + httpConfiguration.setResponseHeaderSize(headerSize); + httpConfiguration.setIdleTimeout(idleTimeout); + + return httpConfiguration; + } + + /** + * 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); + } + + if (storeScanInterval != null) { + sslContextFactory.setKeyStorePath(tlsConfiguration.getKeystorePath()); + final KeyStoreScanner keyStoreScanner = new KeyStoreScanner(sslContextFactory); + keyStoreScanner.setScanInterval(storeScanInterval); + getServer().addBean(keyStoreScanner); + + sslContextFactory.setTrustStorePath(tlsConfiguration.getTruststorePath()); + final TrustStoreScanner trustStoreScanner = new TrustStoreScanner(sslContextFactory); + trustStoreScanner.setScanInterval(storeScanInterval); + getServer().addBean(trustStoreScanner); + } + } + + return sslContextFactory; + } + + private void setApplicationLayerProtocols(final NiFiProperties properties) { + final Set protocols = properties.getWebHttpsApplicationProtocols(); + + final Set applicationLayerProtocols = Arrays.stream(ApplicationLayerProtocol.values()) + .filter( + applicationLayerProtocol -> protocols.contains(applicationLayerProtocol.getProtocol()) + ) + .collect(Collectors.toSet()); + setApplicationLayerProtocols(applicationLayerProtocols); + } + + private int getIdleTimeout(final NiFiProperties properties) { + final String autoRefreshInterval = StringUtils.defaultIfBlank(properties.getAutoRefreshInterval(), DEFAULT_AUTO_REFRESH_INTERVAL); + final double autoRefreshMilliseconds = FormatUtils.getPreciseTimeDuration(autoRefreshInterval, TimeUnit.MILLISECONDS); + return Math.multiplyExact((int) autoRefreshMilliseconds, IDLE_TIMEOUT_MULTIPLIER); + } + + private String[] getCipherSuites(final String cipherSuitesProperty) { + return cipherSuitesProperty.split(CIPHER_SUITE_SEPARATOR_PATTERN); + } + + private static int getPort(final NiFiProperties properties) { + final Integer httpsPort = properties.getSslPort(); + final Integer httpPort = properties.getPort(); + + if (ObjectUtils.allNull(httpsPort, httpPort)) { + throw new IllegalStateException("Invalid port configuration in nifi.properties: Neither nifi.web.https.port nor nifi.web.http.port specified"); + } else if (ObjectUtils.allNotNull(httpsPort, httpPort)) { + throw new IllegalStateException("Invalid port configuration in nifi.properties: Both nifi.web.https.port and nifi.web.http.port specified"); + } + + return ObjectUtils.defaultIfNull(httpsPort, httpPort); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/JettyServerTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/JettyServerTest.java deleted file mode 100644 index 842826d908eb..000000000000 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/JettyServerTest.java +++ /dev/null @@ -1,181 +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.web.server; - -import static org.apache.nifi.security.util.KeyStoreUtils.SUN_PROVIDER_NAME; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import java.lang.reflect.InvocationTargetException; -import java.util.HashMap; -import java.util.Map; - -import org.apache.commons.lang3.StringUtils; -import org.apache.nifi.security.util.KeystoreType; -import org.apache.nifi.util.NiFiProperties; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.eclipse.jetty.util.ssl.SslContextFactory; -import org.junit.Test; - -public class JettyServerTest { - @Test - public void testConfigureSslContextFactoryWithKeystorePasswordAndKeyPassword() { - // Expect that if we set both passwords, KeyStore password is used for KeyStore, Key password is used for Key Manager - String testKeystorePassword = "testKeystorePassword"; - String testKeyPassword = "testKeyPassword"; - - final Map addProps = new HashMap<>(); - addProps.put(NiFiProperties.SECURITY_KEYSTORE_PASSWD, testKeystorePassword); - addProps.put(NiFiProperties.SECURITY_KEY_PASSWD, testKeyPassword); - NiFiProperties nifiProperties = NiFiProperties.createBasicNiFiProperties(null, addProps); - SslContextFactory.Server mockSCF = mock(SslContextFactory.Server.class); - - JettyServer.configureSslContextFactory(mockSCF, nifiProperties); - - verify(mockSCF).setKeyStorePassword(testKeystorePassword); - verify(mockSCF).setKeyManagerPassword(testKeyPassword); - } - - @Test - public void testConfigureSslContextFactoryWithKeyPassword() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { - // Expect that with no KeyStore password, we will only need to set Key Manager Password - String testKeyPassword = "testKeyPassword"; - - final Map addProps = new HashMap<>(); - addProps.put(NiFiProperties.SECURITY_KEY_PASSWD, testKeyPassword); - NiFiProperties nifiProperties = NiFiProperties.createBasicNiFiProperties(null, addProps); - SslContextFactory.Server mockSCF = mock(SslContextFactory.Server.class); - - JettyServer.configureSslContextFactory(mockSCF, nifiProperties); - - verify(mockSCF).setKeyManagerPassword(testKeyPassword); - verify(mockSCF, never()).setKeyStorePassword(anyString()); - } - - @Test - public void testConfigureSslContextFactoryWithKeystorePassword() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { - // Expect that with no KeyPassword, we use the same one from the KeyStore - String testKeystorePassword = "testKeystorePassword"; - - final Map addProps = new HashMap<>(); - addProps.put(NiFiProperties.SECURITY_KEYSTORE_PASSWD, testKeystorePassword); - NiFiProperties nifiProperties = NiFiProperties.createBasicNiFiProperties(null, addProps); - SslContextFactory.Server mockSCF = mock(SslContextFactory.Server.class); - - JettyServer.configureSslContextFactory(mockSCF, nifiProperties); - - verify(mockSCF).setKeyStorePassword(testKeystorePassword); - verify(mockSCF).setKeyManagerPassword(testKeystorePassword); - } - - @Test - public void testConfigureSslContextFactoryWithJksKeyStore() { - // Expect that we will not set provider for jks keystore - final Map addProps = new HashMap<>(); - String keyStoreType = KeystoreType.JKS.toString(); - addProps.put(NiFiProperties.SECURITY_KEYSTORE_TYPE, keyStoreType); - NiFiProperties nifiProperties = NiFiProperties.createBasicNiFiProperties(null, addProps); - SslContextFactory.Server mockSCF = mock(SslContextFactory.Server.class); - - JettyServer.configureSslContextFactory(mockSCF, nifiProperties); - - verify(mockSCF).setKeyStoreType(keyStoreType); - verify(mockSCF).setKeyStoreProvider(SUN_PROVIDER_NAME); - } - - @Test - public void testConfigureSslContextFactoryWithPkcsKeyStore() { - // Expect that we will set Bouncy Castle provider for pkcs12 keystore - final Map addProps = new HashMap<>(); - String keyStoreType = KeystoreType.PKCS12.toString(); - addProps.put(NiFiProperties.SECURITY_KEYSTORE_TYPE, keyStoreType); - NiFiProperties nifiProperties = NiFiProperties.createBasicNiFiProperties(null, addProps); - SslContextFactory.Server mockSCF = mock(SslContextFactory.Server.class); - - JettyServer.configureSslContextFactory(mockSCF, nifiProperties); - - verify(mockSCF).setKeyStoreType(keyStoreType); - verify(mockSCF).setKeyStoreProvider(BouncyCastleProvider.PROVIDER_NAME); - } - - @Test - public void testConfigureSslContextFactoryWithJksTrustStore() { - // Expect that we will not set provider for jks truststore - final Map addProps = new HashMap<>(); - String trustStoreType = KeystoreType.JKS.toString(); - addProps.put(NiFiProperties.SECURITY_TRUSTSTORE_TYPE, trustStoreType); - NiFiProperties nifiProperties = NiFiProperties.createBasicNiFiProperties(null, addProps); - SslContextFactory.Server mockSCF = mock(SslContextFactory.Server.class); - - JettyServer.configureSslContextFactory(mockSCF, nifiProperties); - - verify(mockSCF).setTrustStoreType(trustStoreType); - verify(mockSCF).setTrustStoreProvider(SUN_PROVIDER_NAME); - } - - @Test - public void testConfigureSslContextFactoryWithPkcsTrustStore() { - // Expect that we will set Bouncy Castle provider for pkcs12 truststore - final Map addProps = new HashMap<>(); - String trustStoreType = KeystoreType.PKCS12.toString(); - addProps.put(NiFiProperties.SECURITY_TRUSTSTORE_TYPE, trustStoreType); - NiFiProperties nifiProperties = NiFiProperties.createBasicNiFiProperties(null, addProps); - SslContextFactory.Server mockSCF = mock(SslContextFactory.Server.class); - - JettyServer.configureSslContextFactory(mockSCF, nifiProperties); - - verify(mockSCF).setTrustStoreType(trustStoreType); - verify(mockSCF).setTrustStoreProvider(BouncyCastleProvider.PROVIDER_NAME); - } - - /** - * Verify correct processing of cipher suites with multiple elements. Verify call to override runtime ciphers. - */ - @Test - public void testConfigureSslIncludeExcludeCiphers() { - final String[] includeCipherSuites = {"TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256"}; - final String includeCipherSuitesProp = StringUtils.join(includeCipherSuites, JettyServer.JOIN_ARRAY); - final String[] excludeCipherSuites = {".*DHE.*", ".*ECDH.*"}; - final String excludeCipherSuitesProp = StringUtils.join(excludeCipherSuites, JettyServer.JOIN_ARRAY); - final Map addProps = new HashMap<>(); - addProps.put(NiFiProperties.WEB_HTTPS_CIPHERSUITES_INCLUDE, includeCipherSuitesProp); - addProps.put(NiFiProperties.WEB_HTTPS_CIPHERSUITES_EXCLUDE, excludeCipherSuitesProp); - final NiFiProperties nifiProperties = NiFiProperties.createBasicNiFiProperties(null, addProps); - - final SslContextFactory.Server mockSCF = mock(SslContextFactory.Server.class); - JettyServer.configureSslContextFactory(mockSCF, nifiProperties); - verify(mockSCF, times(1)).setIncludeCipherSuites(includeCipherSuites); - verify(mockSCF, times(1)).setExcludeCipherSuites(excludeCipherSuites); - } - - /** - * Verify skip cipher configuration when NiFiProperties are not specified. - */ - @Test - public void testDoNotConfigureSslIncludeExcludeCiphers() { - final NiFiProperties nifiProperties = NiFiProperties.createBasicNiFiProperties(null); - final SslContextFactory.Server mockSCF = mock(SslContextFactory.Server.class); - JettyServer.configureSslContextFactory(mockSCF, nifiProperties); - verify(mockSCF, times(0)).setIncludeCipherSuites(any()); - verify(mockSCF, times(0)).setExcludeCipherSuites(any()); - } -} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/connector/FrameworkServerConnectorFactoryTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/connector/FrameworkServerConnectorFactoryTest.java new file mode 100644 index 000000000000..e997f25ac667 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/connector/FrameworkServerConnectorFactoryTest.java @@ -0,0 +1,195 @@ +/* + * 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.web.server.connector; + +import org.apache.nifi.jetty.configuration.connector.alpn.ALPNServerConnectionFactory; +import org.apache.nifi.security.util.TemporaryKeyStoreBuilder; +import org.apache.nifi.security.util.TlsConfiguration; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.web.server.util.TrustStoreScanner; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.util.ssl.KeyStoreScanner; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.Properties; + +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.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FrameworkServerConnectorFactoryTest { + private static final String PROPERTIES_FILE_PATH = null; + + private static final int HTTP_PORT = 8080; + + private static final int HTTPS_PORT = 8443; + + private static final String H2_HTTP_1_1_PROTOCOLS = "h2 http/1.1"; + + private static final String EXCLUDED_CIPHER_SUITE = "TLS_PSK_WITH_NULL_SHA"; + + private static final String INCLUDED_CIPHER_SUITE_PATTERN = ".*AES_256_GCM.*"; + + private static TlsConfiguration tlsConfiguration; + + @BeforeAll + static void setTlsConfiguration() { + final TemporaryKeyStoreBuilder builder = new TemporaryKeyStoreBuilder(); + tlsConfiguration = builder.build(); + } + + @Test + void testHttpPortAndHttpsPortNotConfiguredException() { + final Properties serverProperties = new Properties(); + final NiFiProperties properties = getProperties(serverProperties); + + final Server server = new Server(); + final IllegalStateException e = assertThrows(IllegalStateException.class, () -> new FrameworkServerConnectorFactory(server, properties)); + assertTrue(e.getMessage().contains(NiFiProperties.WEB_HTTP_PORT)); + } + + @Test + void testHttpPortAndHttpsPortException() { + final Properties serverProperties = new Properties(); + serverProperties.setProperty(NiFiProperties.WEB_HTTP_PORT, Integer.toString(HTTP_PORT)); + serverProperties.setProperty(NiFiProperties.WEB_HTTPS_PORT, Integer.toString(HTTPS_PORT)); + final NiFiProperties properties = getProperties(serverProperties); + + final Server server = new Server(); + final IllegalStateException e = assertThrows(IllegalStateException.class, () -> new FrameworkServerConnectorFactory(server, properties)); + assertTrue(e.getMessage().contains(NiFiProperties.WEB_HTTP_PORT)); + } + + @Test + void testGetServerConnector() { + final Properties serverProperties = new Properties(); + serverProperties.setProperty(NiFiProperties.WEB_HTTP_PORT, Integer.toString(HTTP_PORT)); + final NiFiProperties properties = getProperties(serverProperties); + + final Server server = new Server(); + final FrameworkServerConnectorFactory factory = new FrameworkServerConnectorFactory(server, properties); + + final ServerConnector serverConnector = factory.getServerConnector(); + + assertHttpConnectionFactoryFound(serverConnector); + } + + @Test + void testGetServerConnectorHttps() { + final Properties serverProperties = getHttpsProperties(); + serverProperties.setProperty(NiFiProperties.WEB_HTTPS_CIPHERSUITES_EXCLUDE, EXCLUDED_CIPHER_SUITE); + serverProperties.setProperty(NiFiProperties.WEB_HTTPS_CIPHERSUITES_INCLUDE, INCLUDED_CIPHER_SUITE_PATTERN); + serverProperties.setProperty(NiFiProperties.SECURITY_AUTO_RELOAD_ENABLED, Boolean.TRUE.toString()); + final FrameworkServerConnectorFactory factory = getHttpsConnectorFactory(serverProperties); + + final ServerConnector serverConnector = factory.getServerConnector(); + + assertHttpConnectionFactoryFound(serverConnector); + final SslConnectionFactory sslConnectionFactory = assertSslConnectionFactoryFound(serverConnector); + + final SslContextFactory.Server sslContextFactory = (SslContextFactory.Server) sslConnectionFactory.getSslContextFactory(); + assertTrue(sslContextFactory.getNeedClientAuth()); + assertFalse(sslContextFactory.getWantClientAuth()); + + assertCipherSuitesConfigured(sslContextFactory); + assertAutoReloadEnabled(serverConnector); + + final HTTP2ServerConnectionFactory http2ServerConnectionFactory = serverConnector.getConnectionFactory(HTTP2ServerConnectionFactory.class); + assertNull(http2ServerConnectionFactory); + } + + @Test + void testGetServerConnectorHttpsHttp2AndHttp11() { + final Properties serverProperties = getHttpsProperties(); + serverProperties.setProperty(NiFiProperties.WEB_HTTPS_APPLICATION_PROTOCOLS, H2_HTTP_1_1_PROTOCOLS); + final FrameworkServerConnectorFactory factory = getHttpsConnectorFactory(serverProperties); + + final ServerConnector serverConnector = factory.getServerConnector(); + + assertHttpConnectionFactoryFound(serverConnector); + assertSslConnectionFactoryFound(serverConnector); + + final HTTP2ServerConnectionFactory http2ServerConnectionFactory = serverConnector.getConnectionFactory(HTTP2ServerConnectionFactory.class); + assertNotNull(http2ServerConnectionFactory); + + final ALPNServerConnectionFactory alpnServerConnectionFactory = serverConnector.getConnectionFactory(ALPNServerConnectionFactory.class); + assertNotNull(alpnServerConnectionFactory); + } + + private Properties getHttpsProperties() { + final Properties serverProperties = new Properties(); + serverProperties.setProperty(NiFiProperties.WEB_HTTPS_PORT, Integer.toString(HTTPS_PORT)); + serverProperties.setProperty(NiFiProperties.SECURITY_KEYSTORE, tlsConfiguration.getKeystorePath()); + serverProperties.setProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE, tlsConfiguration.getKeystoreType().getType()); + serverProperties.setProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD, tlsConfiguration.getKeystorePassword()); + serverProperties.setProperty(NiFiProperties.SECURITY_KEY_PASSWD, tlsConfiguration.getKeyPassword()); + serverProperties.setProperty(NiFiProperties.SECURITY_TRUSTSTORE, tlsConfiguration.getTruststorePath()); + serverProperties.setProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE, tlsConfiguration.getTruststoreType().getType()); + serverProperties.setProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD, tlsConfiguration.getTruststorePassword()); + return serverProperties; + } + + private FrameworkServerConnectorFactory getHttpsConnectorFactory(final Properties serverProperties) { + final NiFiProperties properties = getProperties(serverProperties); + final Server server = new Server(); + return new FrameworkServerConnectorFactory(server, properties); + } + + private SslConnectionFactory assertSslConnectionFactoryFound(final ServerConnector serverConnector) { + final SslConnectionFactory sslConnectionFactory = serverConnector.getConnectionFactory(SslConnectionFactory.class); + assertNotNull(sslConnectionFactory); + return sslConnectionFactory; + } + + private void assertHttpConnectionFactoryFound(final ServerConnector serverConnector) { + assertNotNull(serverConnector); + final HttpConnectionFactory connectionFactory = serverConnector.getConnectionFactory(HttpConnectionFactory.class); + assertNotNull(connectionFactory); + } + + private void assertCipherSuitesConfigured(final SslContextFactory sslContextFactory) { + final String[] excludedCipherSuites = sslContextFactory.getExcludeCipherSuites(); + assertEquals(1, excludedCipherSuites.length); + assertEquals(EXCLUDED_CIPHER_SUITE, excludedCipherSuites[0]); + + final String[] includedCipherSuites = sslContextFactory.getIncludeCipherSuites(); + assertEquals(1, includedCipherSuites.length); + assertEquals(INCLUDED_CIPHER_SUITE_PATTERN, includedCipherSuites[0]); + } + + private void assertAutoReloadEnabled(final ServerConnector serverConnector) { + final Server server = serverConnector.getServer(); + final KeyStoreScanner keyStoreScanner = server.getBean(KeyStoreScanner.class); + assertNotNull(keyStoreScanner); + + final TrustStoreScanner trustStoreScanner = server.getBean(TrustStoreScanner.class); + assertNotNull(trustStoreScanner); + } + + private NiFiProperties getProperties(final Properties serverProperties) { + return NiFiProperties.createBasicNiFiProperties(PROPERTIES_FILE_PATH, serverProperties); + } +}