From 8b90343715be8e28e465991fcdde00f64a8c6c6f Mon Sep 17 00:00:00 2001 From: Jeff Storck Date: Tue, 17 Jan 2017 16:24:38 -0500 Subject: [PATCH] NIFI-3355 Allows NiFi to bind to specific network interfaces, with separate interface lists for HTTP and HTTPS. This closes #1508. Signed-off-by: Bryan Rosander --- .../org/apache/nifi/util/NiFiProperties.java | 46 ++++++++ .../main/asciidoc/administration-guide.adoc | 25 +++- .../nifi-framework/nifi-resources/pom.xml | 2 + .../src/main/resources/conf/nifi.properties | 2 + .../apache/nifi/web/server/JettyServer.java | 111 ++++++++++++++---- 5 files changed, 159 insertions(+), 27 deletions(-) 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 933d62d668..6a39852ea8 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 @@ -152,9 +152,11 @@ public abstract class NiFiProperties { public static final String WEB_HTTP_PORT = "nifi.web.http.port"; public static final String WEB_HTTP_PORT_FORWARDING = "nifi.web.http.port.forwarding"; public static final String WEB_HTTP_HOST = "nifi.web.http.host"; + public static final String WEB_HTTP_NETWORK_INTERFACE_PREFIX = "nifi.web.http.network.interface."; 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_NETWORK_INTERFACE_PREFIX = "nifi.web.https.network.interface."; public static final String WEB_WORKING_DIR = "nifi.web.jetty.working.directory"; public static final String WEB_THREADS = "nifi.web.jetty.threads"; @@ -1016,6 +1018,50 @@ public abstract class NiFiProperties { } } + /** + * Returns the network interface list to use for HTTP. This method returns a mapping of + * network interface property names to network interface names. + * + * @return the property name and network interface name of all HTTP network interfaces + */ + public Map getHttpNetworkInterfaces() { + final Map networkInterfaces = new HashMap<>(); + + // go through each property + for (String propertyName : getPropertyKeys()) { + // determine if the property is a network interface name + if (StringUtils.startsWith(propertyName, WEB_HTTP_NETWORK_INTERFACE_PREFIX)) { + // get the network interface property key + final String key = StringUtils.substringAfter(propertyName, + WEB_HTTP_NETWORK_INTERFACE_PREFIX); + networkInterfaces.put(key, getProperty(propertyName)); + } + } + return networkInterfaces; + } + + /** + * Returns the network interface list to use for HTTPS. This method returns a mapping of + * network interface property names to network interface names. + * + * @return the property name and network interface name of all HTTPS network interfaces + */ + public Map getHttpsNetworkInterfaces() { + final Map networkInterfaces = new HashMap<>(); + + // go through each property + for (String propertyName : getPropertyKeys()) { + // determine if the property is a network interface name + if (StringUtils.startsWith(propertyName, WEB_HTTPS_NETWORK_INTERFACE_PREFIX)) { + // get the network interface property key + final String key = StringUtils.substringAfter(propertyName, + WEB_HTTPS_NETWORK_INTERFACE_PREFIX); + networkInterfaces.put(key, getProperty(propertyName)); + } + } + return networkInterfaces; + } + public int size() { return getPropertyKeys().size(); } diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc index 25b2f11906..0daa7754fa 100644 --- a/nifi-docs/src/main/asciidoc/administration-guide.adoc +++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc @@ -152,8 +152,9 @@ NiFi provides several different configuration options for security purposes. The Once the above properties have been configured, we can enable the User Interface to be accessed over HTTPS instead of HTTP. This is accomplished by setting the `nifi.web.https.host` and `nifi.web.https.port` properties. The `nifi.web.https.host` property indicates which hostname the server -should run on. This allows admins to configure the application to run only on specific network interfaces. If it is desired that the HTTPS interface -be accessible from all network interfaces, a value of `0.0.0.0` should be used. +should run on. If it is desired that the HTTPS interface be accessible from all network interfaces, a value of `0.0.0.0` should be used. To allow +admins to configure the application to run only on specific network interfaces, `nifi.web.http.network.interface*` or `nifi.web.http.network.interface*` +properties can be specified. NOTE: It is important when enabling HTTPS that the `nifi.web.http.port` property be unset. @@ -2162,9 +2163,29 @@ These properties pertain to the web-based User Interface. |nifi.web.http.host|The HTTP host. It is blank by default. |nifi.web.http.port|The HTTP port. The default value is 8080. |nifi.web.http.port.forwarding|The port which forwards incoming HTTP requests to nifi.web.http.host. This property is designed to be used with 'port forwarding', when NiFi has to be started by a non-root user for better security, yet it needs to be accessed via low port to go through a firewall. For example, to expose NiFi via HTTP protocol on port 80, but actually listening on port 8080, you need to configure OS level port forwarding such as `iptables` (Linux/Unix) or `pfctl` (OS X) that redirects requests from 80 to 8080. Then set `nifi.web.http.port` as 8080, and `nifi.web.http.port.forwarding` as 80. It is blank by default. +|nifi.web.http.network.interface*|The name of the network interface to which NiFi should bind for HTTP requests. It is blank by default. + + + +*NOTE*: Multiple network interfaces can be specified by using the *_nifi.web.http.network.interface._* prefix with unique suffixes and separate network interface names as values. + + + +For example, to provide two additional network interfaces, a user could also specify additional properties with keys of: + + + +nifi.web.http.network.interface.eth0=eth0 + +nifi.web.http.network.interface.eth1=eth1 + + + +Providing three total network interfaces, including _nifi.web.http.network.interface.default_. |nifi.web.https.host|The HTTPS host. It is blank by default. |nifi.web.https.port|The HTTPS port. It is blank by default. When configuring NiFi to run securely, this port should be configured. |nifi.web.https.port.forwarding|Same as `nifi.web.http.port.forwarding`, but with HTTPS for secure communication. It is blank by default. +|nifi.web.https.network.interface*|The name of the network interface to which NiFi should bind for HTTPS requests. It is blank by default. + + + +*NOTE*: Multiple network interfaces can be specified by using the *_nifi.web.https.network.interface._* prefix with unique suffixes and separate network interface names as values. + + + +For example, to provide two additional network interfaces, a user could also specify additional properties with keys of: + + + +nifi.web.https.network.interface.eth0=eth0 + +nifi.web.https.network.interface.eth1=eth1 + + + +Providing three total network interfaces, including _nifi.web.https.network.interface.default_. |nif.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. |==== 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 9baa3eac2b..662f87ef5d 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 @@ -119,8 +119,10 @@ ./lib 8080 + + ./work/jetty 200 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 a768af47b9..11b2d6aa6c 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 @@ -124,8 +124,10 @@ nifi.remote.input.http.transaction.ttl=30 sec nifi.web.war.directory=${nifi.web.war.directory} nifi.web.http.host=${nifi.web.http.host} nifi.web.http.port=${nifi.web.http.port} +nifi.web.http.network.interface.default=${nifi.web.http.network.interface.default} 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.jetty.working.directory=${nifi.jetty.work.dir} nifi.web.jetty.threads=${nifi.web.jetty.threads} 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 61b0e86338..d2def9f1a0 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 @@ -16,6 +16,8 @@ */ package org.apache.nifi.web.server; +import com.google.common.base.Strings; +import com.google.common.collect.Lists; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; @@ -85,9 +87,11 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; +import java.util.stream.Collectors; /** * Encapsulates the Jetty instance. @@ -569,17 +573,43 @@ public class JettyServer implements NiFiServer { logger.info("Configuring Jetty for HTTP on port: " + port); - // create the connector - final ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfiguration)); + final List serverConnectors = Lists.newArrayList(); - // set host and port - if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.WEB_HTTP_HOST))) { - http.setHost(props.getProperty(NiFiProperties.WEB_HTTP_HOST)); + final Map httpNetworkInterfaces = props.getHttpNetworkInterfaces(); + if (httpNetworkInterfaces.isEmpty() || httpNetworkInterfaces.values().stream().filter(value -> !Strings.isNullOrEmpty(value)).collect(Collectors.toList()).isEmpty()) { + // create the connector + final ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfiguration)); + // set host and port + if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.WEB_HTTP_HOST))) { + http.setHost(props.getProperty(NiFiProperties.WEB_HTTP_HOST)); + } + http.setPort(port); + serverConnectors.add(http); + } else { + // add connectors for all IPs from http network interfaces + serverConnectors.addAll(Lists.newArrayList(httpNetworkInterfaces.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); + } + return iface; + }).filter(Objects::nonNull).flatMap(iface -> Collections.list(iface.getInetAddresses()).stream()) + .map(inetAddress -> { + // create the connector + final ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfiguration)); + // set host and port + http.setHost(inetAddress.getHostAddress()); + http.setPort(port); + return http; + }).collect(Collectors.toList()))); } - http.setPort(port); - - // add this connector - server.addConnector(http); + // add all connectors + serverConnectors.forEach(server::addConnector); } if (props.getSslPort() != null) { @@ -590,28 +620,59 @@ public class JettyServer implements NiFiServer { logger.info("Configuring Jetty for HTTPs on port: " + port); - // add some secure config - final HttpConfiguration httpsConfiguration = new HttpConfiguration(httpConfiguration); - httpsConfiguration.setSecureScheme("https"); - httpsConfiguration.setSecurePort(props.getSslPort()); - httpsConfiguration.addCustomizer(new SecureRequestCustomizer()); + final List serverConnectors = Lists.newArrayList(); - // build the connector - final ServerConnector https = new ServerConnector(server, - new SslConnectionFactory(createSslContextFactory(), "http/1.1"), - new HttpConnectionFactory(httpsConfiguration)); + final Map httpsNetworkInterfaces = props.getHttpsNetworkInterfaces(); + if (httpsNetworkInterfaces.isEmpty() || httpsNetworkInterfaces.values().stream().filter(value -> !Strings.isNullOrEmpty(value)).collect(Collectors.toList()).isEmpty()) { + final ServerConnector https = createUnconfiguredSslServerConnector(server, httpConfiguration); - // set host and port - if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.WEB_HTTPS_HOST))) { - https.setHost(props.getProperty(NiFiProperties.WEB_HTTPS_HOST)); + // set host and port + if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.WEB_HTTPS_HOST))) { + https.setHost(props.getProperty(NiFiProperties.WEB_HTTPS_HOST)); + } + https.setPort(port); + serverConnectors.add(https); + } else { + // add connectors for all IPs from https network interfaces + serverConnectors.addAll(Lists.newArrayList(httpsNetworkInterfaces.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); + } + return iface; + }).filter(Objects::nonNull).flatMap(iface -> Collections.list(iface.getInetAddresses()).stream()) + .map(inetAddress -> { + final ServerConnector https = createUnconfiguredSslServerConnector(server, httpConfiguration); + + // set host and port + https.setHost(inetAddress.getHostAddress()); + https.setPort(port); + return https; + }).collect(Collectors.toList()))); } - https.setPort(port); - - // add this connector - server.addConnector(https); + // add all connectors + serverConnectors.forEach(server::addConnector); } } + private ServerConnector createUnconfiguredSslServerConnector(Server server, HttpConfiguration httpConfiguration) { + // add some secure config + final HttpConfiguration httpsConfiguration = new HttpConfiguration(httpConfiguration); + httpsConfiguration.setSecureScheme("https"); + httpsConfiguration.setSecurePort(props.getSslPort()); + 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 contextFactory = new SslContextFactory(); configureSslContextFactory(contextFactory, props);