You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by kd...@apache.org on 2018/12/17 22:32:32 UTC
[nifi] branch master updated: NIFI-5748 Improved Proxy Header
Support
This is an automated email from the ASF dual-hosted git repository.
kdoran pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/master by this push:
new cc47a8c NIFI-5748 Improved Proxy Header Support
cc47a8c is described below
commit cc47a8c0e1762b17f3889bf8e36b92cf51a8e7af
Author: Jeff Storck <jt...@gmail.com>
AuthorDate: Mon Oct 29 13:29:28 2018 -0400
NIFI-5748 Improved Proxy Header Support
- Fixed proxy header support to use X-Forwarded-Host instead of X-ForwardedServer
- Added support for the context path header used by Traefik when proxying a service (X-Forwarded-Prefix)
- Added tests to ApplicationResourceTest for X-Forwarded-Context and X-Forwarded-Prefix
- Updated administration doc to include X-Forwarded-Prefix
- Added NIFI_WEB_PROXY_CONTEXT_PATH env var to dockerhub and dockermaven start.sh scripts
- Added documentation for NIFI_WEB_PROXY_CONTEXT_PATH to dockerhub README.md
- Updated ApplicationResource to handle a port specified in X-ProxyPort and X-Forwarded-Port headers
This closes #3129.
Signed-off-by: Kevin Doran <kd...@apache.org>
---
.../java/org/apache/nifi/web/util/WebUtils.java | 19 ++--
.../org/apache/nifi/web/util/WebUtilsTest.groovy | 43 ++++++---
nifi-docker/dockerhub/README.md | 5 +-
nifi-docker/dockerhub/sh/start.sh | 1 +
nifi-docker/dockermaven/sh/start.sh | 1 +
.../src/main/asciidoc/administration-guide.adoc | 6 +-
.../apache/nifi/web/api/ApplicationResource.java | 50 +++++++++-
.../nifi/web/api/ApplicationResourceTest.groovy | 101 +++++++++++++++++++--
.../apache/nifi/web/ContentViewerController.java | 3 +-
9 files changed, 195 insertions(+), 34 deletions(-)
diff --git a/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/util/WebUtils.java b/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/util/WebUtils.java
index 90a83a9..fbf5c19 100644
--- a/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/util/WebUtils.java
+++ b/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/util/WebUtils.java
@@ -21,6 +21,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.stream.Stream;
import javax.net.ssl.SSLContext;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
@@ -45,6 +46,7 @@ public final class WebUtils {
private static final String PROXY_CONTEXT_PATH_HTTP_HEADER = "X-ProxyContextPath";
private static final String FORWARDED_CONTEXT_HTTP_HEADER = "X-Forwarded-Context";
+ private static final String FORWARDED_PREFIX_HTTP_HEADER = "X-Forwarded-Prefix";
private WebUtils() {
}
@@ -199,7 +201,8 @@ public final class WebUtils {
}
/**
- * Determines the context path if populated in {@code X-ProxyContextPath} or {@code X-ForwardContext} headers. If not populated, returns an empty string.
+ * Determines the context path if populated in {@code X-ProxyContextPath}, {@code X-ForwardContext},
+ * or {@code X-Forwarded-Prefix} headers. If not populated, returns an empty string.
*
* @param request the HTTP request
* @return the provided context path or an empty string
@@ -208,18 +211,20 @@ public final class WebUtils {
String contextPath = request.getContextPath();
String proxyContextPath = request.getHeader(PROXY_CONTEXT_PATH_HTTP_HEADER);
String forwardedContext = request.getHeader(FORWARDED_CONTEXT_HTTP_HEADER);
+ String prefix = request.getHeader(FORWARDED_PREFIX_HTTP_HEADER);
logger.debug("Context path: " + contextPath);
String determinedContextPath = "";
- // If either header is set, log both
- if (anyNotBlank(proxyContextPath, forwardedContext)) {
+ // If a context path header is set, log each
+ if (anyNotBlank(proxyContextPath, forwardedContext, prefix)) {
logger.debug(String.format("On the request, the following context paths were parsed" +
- " from headers:\n\t X-ProxyContextPath: %s\n\tX-Forwarded-Context: %s",
- proxyContextPath, forwardedContext));
+ " from headers:\n\t X-ProxyContextPath: %s\n\tX-Forwarded-Context: %s\n\tX-Forwarded-Prefix: %s",
+ proxyContextPath, forwardedContext, prefix));
- // Implementing preferred order here: PCP, FCP
- determinedContextPath = StringUtils.isNotBlank(proxyContextPath) ? proxyContextPath : forwardedContext;
+ // Implementing preferred order here: PCP, FC, FP
+ determinedContextPath = Stream.of(proxyContextPath, forwardedContext, prefix)
+ .filter(StringUtils::isNotBlank).findFirst().orElse("");
}
logger.debug("Determined context path: " + determinedContextPath);
diff --git a/nifi-commons/nifi-web-utils/src/test/groovy/org/apache/nifi/web/util/WebUtilsTest.groovy b/nifi-commons/nifi-web-utils/src/test/groovy/org/apache/nifi/web/util/WebUtilsTest.groovy
index b0a1191..6b68457 100644
--- a/nifi-commons/nifi-web-utils/src/test/groovy/org/apache/nifi/web/util/WebUtilsTest.groovy
+++ b/nifi-commons/nifi-web-utils/src/test/groovy/org/apache/nifi/web/util/WebUtilsTest.groovy
@@ -32,10 +32,10 @@ import sun.security.x509.X500Name
import javax.net.ssl.SSLPeerUnverifiedException
import javax.servlet.http.HttpServletRequest
import javax.ws.rs.core.UriBuilderException
-import javax.ws.rs.client.Client;
+import javax.ws.rs.client.Client
import javax.net.ssl.SSLContext
-import javax.net.ssl.HostnameVerifier;
-import java.security.cert.X509Certificate;
+import javax.net.ssl.HostnameVerifier
+import java.security.cert.X509Certificate
@RunWith(JUnit4.class)
@@ -44,9 +44,10 @@ class WebUtilsTest extends GroovyTestCase {
static final String PCP_HEADER = "X-ProxyContextPath"
static final String FC_HEADER = "X-Forwarded-Context"
+ static final String FP_HEADER = "X-Forwarded-Prefix"
static final String WHITELISTED_PATH = "/some/context/path"
- private static final String OCSP_REQUEST_CONTENT_TYPE = "application/ocsp-request";
+ private static final String OCSP_REQUEST_CONTENT_TYPE = "application/ocsp-request"
@BeforeClass
static void setUpOnce() throws Exception {
@@ -78,6 +79,9 @@ class WebUtilsTest extends GroovyTestCase {
case FC_HEADER:
return keys["forward"]
break
+ case FP_HEADER:
+ return keys["prefix"]
+ break
default:
return ""
}
@@ -94,8 +98,12 @@ class WebUtilsTest extends GroovyTestCase {
// Variety of requests with different ordering of context paths (the correct one is always "some/context/path"
HttpServletRequest proxyRequest = mockRequest([proxy: CORRECT_CONTEXT_PATH])
HttpServletRequest forwardedRequest = mockRequest([forward: CORRECT_CONTEXT_PATH])
+ HttpServletRequest prefixRequest = mockRequest([prefix: CORRECT_CONTEXT_PATH])
HttpServletRequest proxyBeforeForwardedRequest = mockRequest([proxy: CORRECT_CONTEXT_PATH, forward: WRONG_CONTEXT_PATH])
- List<HttpServletRequest> requests = [proxyRequest, forwardedRequest, proxyBeforeForwardedRequest]
+ HttpServletRequest proxyBeforePrefixRequest = mockRequest([proxy: CORRECT_CONTEXT_PATH, prefix: WRONG_CONTEXT_PATH])
+ HttpServletRequest forwardBeforePrefixRequest = mockRequest([forward: CORRECT_CONTEXT_PATH, prefix: WRONG_CONTEXT_PATH])
+ List<HttpServletRequest> requests = [proxyRequest, forwardedRequest, prefixRequest, proxyBeforeForwardedRequest,
+ proxyBeforePrefixRequest, forwardBeforePrefixRequest]
// Act
requests.each { HttpServletRequest request ->
@@ -117,8 +125,12 @@ class WebUtilsTest extends GroovyTestCase {
HttpServletRequest proxySpacesRequest = mockRequest([proxy: " "])
HttpServletRequest forwardedRequest = mockRequest([forward: ""])
HttpServletRequest forwardedSpacesRequest = mockRequest([forward: " "])
- HttpServletRequest proxyBeforeForwardedRequest = mockRequest([proxy: "", forward: ""])
- List<HttpServletRequest> requests = [proxyRequest, proxySpacesRequest, forwardedRequest, forwardedSpacesRequest, proxyBeforeForwardedRequest]
+ HttpServletRequest prefixRequest = mockRequest([prefix: ""])
+ HttpServletRequest prefixSpacesRequest = mockRequest([prefix: " "])
+ HttpServletRequest proxyBeforeForwardedOrPrefixRequest = mockRequest([proxy: "", forward: "", prefix: ""])
+ HttpServletRequest proxyBeforeForwardedOrPrefixSpacesRequest = mockRequest([proxy: " ", forward: " ", prefix: " "])
+ List<HttpServletRequest> requests = [proxyRequest, proxySpacesRequest, forwardedRequest, forwardedSpacesRequest, prefixRequest, prefixSpacesRequest,
+ proxyBeforeForwardedOrPrefixRequest, proxyBeforeForwardedOrPrefixSpacesRequest]
// Act
requests.each { HttpServletRequest request ->
@@ -156,7 +168,9 @@ class WebUtilsTest extends GroovyTestCase {
HttpServletRequest requestWithProxyHeader = mockRequest([proxy: "any/context/path"])
HttpServletRequest requestWithProxyAndForwardHeader = mockRequest([proxy: "any/context/path", forward: "any/other/context/path"])
- List<HttpServletRequest> requests = [requestWithProxyHeader, requestWithProxyAndForwardHeader]
+ HttpServletRequest requestWithProxyAndForwardAndPrefixHeader = mockRequest([proxy : "any/context/path", forward: "any/other/context/path",
+ prefix: "any/other/prefix/path"])
+ List<HttpServletRequest> requests = [requestWithProxyHeader, requestWithProxyAndForwardHeader, requestWithProxyAndForwardAndPrefixHeader]
// Act
requests.each { HttpServletRequest request ->
@@ -179,7 +193,10 @@ class WebUtilsTest extends GroovyTestCase {
HttpServletRequest requestWithProxyHeader = mockRequest([proxy: "some/context/path"])
HttpServletRequest requestWithForwardHeader = mockRequest([forward: "some/context/path"])
HttpServletRequest requestWithProxyAndForwardHeader = mockRequest([proxy: "some/context/path", forward: "any/other/context/path"])
- List<HttpServletRequest> requests = [requestWithProxyHeader, requestWithForwardHeader, requestWithProxyAndForwardHeader]
+ HttpServletRequest requestWithProxyAndForwardAndPrefixHeader = mockRequest([proxy: "some/context/path", forward: "any/other/context/path",
+ prefix: "any/other/prefix/path"])
+ List<HttpServletRequest> requests = [requestWithProxyHeader, requestWithForwardHeader, requestWithProxyAndForwardHeader,
+ requestWithProxyAndForwardAndPrefixHeader]
// Act
requests.each { HttpServletRequest request ->
@@ -194,15 +211,19 @@ class WebUtilsTest extends GroovyTestCase {
@Test
void testGetResourcePathShouldAllowContextPathHeaderIfElementInMultipleWhitelist() throws Exception {
// Arrange
- String multipleWhitelistedPaths = [WHITELISTED_PATH, "/another/path", "/a/third/path"].join(",")
+ String multipleWhitelistedPaths = [WHITELISTED_PATH, "/another/path", "/a/third/path", "/a/prefix/path"].join(",")
logger.info("Whitelisted path(s): ${multipleWhitelistedPaths}")
final List<String> VALID_RESOURCE_PATHS = multipleWhitelistedPaths.split(",").collect { "$it/actualResource" }
HttpServletRequest requestWithProxyHeader = mockRequest([proxy: "some/context/path"])
HttpServletRequest requestWithForwardHeader = mockRequest([forward: "another/path"])
+ HttpServletRequest requestWithPrefixHeader = mockRequest([prefix: "a/prefix/path"])
HttpServletRequest requestWithProxyAndForwardHeader = mockRequest([proxy: "a/third/path", forward: "any/other/context/path"])
- List<HttpServletRequest> requests = [requestWithProxyHeader, requestWithForwardHeader, requestWithProxyAndForwardHeader]
+ HttpServletRequest requestWithProxyAndForwardAndPrefixHeader = mockRequest([proxy : "a/third/path", forward: "any/other/context/path",
+ prefix: "any/other/prefix/path"])
+ List<HttpServletRequest> requests = [requestWithProxyHeader, requestWithForwardHeader, requestWithProxyAndForwardHeader,
+ requestWithPrefixHeader, requestWithProxyAndForwardAndPrefixHeader]
// Act
requests.each { HttpServletRequest request ->
diff --git a/nifi-docker/dockerhub/README.md b/nifi-docker/dockerhub/README.md
index e2123da..70aefee 100644
--- a/nifi-docker/dockerhub/README.md
+++ b/nifi-docker/dockerhub/README.md
@@ -191,6 +191,9 @@ can be published to the host.
The Variable Registry can be configured for the docker image using the `NIFI_VARIABLE_REGISTRY_PROPERTIES` environment variable.
-=======
+=======
+**NOTE**: If NiFi is proxied at context paths other than the root path of the proxy, the paths need to be set in the
+_nifi.web.proxy.context.path_ property, which can be assigned via the environment variable _NIFI\_WEB\_PROXY\_CONTEXT\_PATH_.
+
**NOTE**: If mapping the HTTPS port specifying trusted hosts should be provided for the property _nifi.web.proxy.host_. This property can be specified to running instances
via specifying an environment variable at container instantiation of _NIFI\_WEB\_PROXY\_HOST_.
diff --git a/nifi-docker/dockerhub/sh/start.sh b/nifi-docker/dockerhub/sh/start.sh
index 1cf5a7c..447da40 100755
--- a/nifi-docker/dockerhub/sh/start.sh
+++ b/nifi-docker/dockerhub/sh/start.sh
@@ -40,6 +40,7 @@ prop_replace 'nifi.zookeeper.connect.string' "${NIFI_ZK_CONNECT_S
prop_replace 'nifi.zookeeper.root.node' "${NIFI_ZK_ROOT_NODE:-/nifi}"
prop_replace 'nifi.cluster.flow.election.max.wait.time' "${NIFI_ELECTION_MAX_WAIT:-5 mins}"
prop_replace 'nifi.cluster.flow.election.max.candidates' "${NIFI_ELECTION_MAX_CANDIDATES:-}"
+prop_replace 'nifi.web.proxy.context.path' "${NIFI_WEB_PROXY_CONTEXT_PATH:-}"
. "${scripts_dir}/update_cluster_state_management.sh"
diff --git a/nifi-docker/dockermaven/sh/start.sh b/nifi-docker/dockermaven/sh/start.sh
index 1cf5a7c..447da40 100755
--- a/nifi-docker/dockermaven/sh/start.sh
+++ b/nifi-docker/dockermaven/sh/start.sh
@@ -40,6 +40,7 @@ prop_replace 'nifi.zookeeper.connect.string' "${NIFI_ZK_CONNECT_S
prop_replace 'nifi.zookeeper.root.node' "${NIFI_ZK_ROOT_NODE:-/nifi}"
prop_replace 'nifi.cluster.flow.election.max.wait.time' "${NIFI_ELECTION_MAX_WAIT:-5 mins}"
prop_replace 'nifi.cluster.flow.election.max.candidates' "${NIFI_ELECTION_MAX_CANDIDATES:-}"
+prop_replace 'nifi.web.proxy.context.path' "${NIFI_WEB_PROXY_CONTEXT_PATH:-}"
. "${scripts_dir}/update_cluster_state_management.sh"
diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index cded61e..61316f4 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -2292,8 +2292,8 @@ host[:port] the expected values need to be configured. This may be required when
separated list in _nifi.properties_ using the `nifi.web.proxy.host` property (e.g. `localhost:18443, proxyhost:443`). IPv6 addresses are accepted. Please refer to
RFC 5952 Sections link:https://tools.ietf.org/html/rfc5952#section-4[4] and link:https://tools.ietf.org/html/rfc5952#section-6[6] for additional details.
-** NiFi will only accept HTTP requests with a X-ProxyContextPath or X-Forwarded-Context header if the value is whitelisted in the `nifi.web.proxy.context.path` property in
-_nifi.properties_. This property accepts a comma separated list of expected values. In the event an incoming request has an X-ProxyContextPath or X-Forwarded-Context header value that is not
+** NiFi will only accept HTTP requests with a X-ProxyContextPath, X-Forwarded-Context, or X-Forwarded-Prefix header if the value is whitelisted in the `nifi.web.proxy.context.path` property in
+_nifi.properties_. This property accepts a comma separated list of expected values. In the event an incoming request has an X-ProxyContextPath, X-Forwarded-Context, or X-Forwarded-Prefix header value that is not
present in the whitelist, the "An unexpected error has occurred" page will be shown and an error will be written to the _nifi-app.log_.
* Additional configurations at both proxy server and NiFi cluster are required to make NiFi Site-to-Site work behind reverse proxies. See <<site_to_site_reverse_proxy_properties>> for details.
@@ -3009,7 +3009,7 @@ Providing three total network interfaces, including `nifi.web.https.network.int
|`nifi.web.proxy.host`|A comma separated list of allowed HTTP Host header values to consider when NiFi is running securely and will be receiving requests to a different host[:port] than it is bound to.
For example, when running in a Docker container or behind a proxy (e.g. localhost:18443, proxyhost:443). By default, this value is blank meaning NiFi should only allow requests sent to the
host[:port] that NiFi is bound to.
-|`nifi.web.proxy.context.path`|A comma separated list of allowed HTTP X-ProxyContextPath or X-Forwarded-Context header values to consider. By default, this value is
+|`nifi.web.proxy.context.path`|A comma separated list of allowed HTTP X-ProxyContextPath, X-Forwarded-Context, or X-Forwarded-Prefix header values to consider. By default, this value is
blank meaning all requests containing a proxy context path are rejected. Configuring this property would allow requests where the proxy path is contained in this listing.
|====
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
index d99fb96..f675a46 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
@@ -107,10 +107,13 @@ public abstract class ApplicationResource {
public static final String PROXY_CONTEXT_PATH_HTTP_HEADER = "X-ProxyContextPath";
public static final String FORWARDED_PROTO_HTTP_HEADER = "X-Forwarded-Proto";
- public static final String FORWARDED_HOST_HTTP_HEADER = "X-Forwarded-Server";
+ public static final String FORWARDED_HOST_HTTP_HEADER = "X-Forwarded-Host";
public static final String FORWARDED_PORT_HTTP_HEADER = "X-Forwarded-Port";
public static final String FORWARDED_CONTEXT_HTTP_HEADER = "X-Forwarded-Context";
+ // Traefik-specific headers
+ public static final String FORWARDED_PREFIX_HTTP_HEADER = "X-Forwarded-Prefix";
+
protected static final String NON_GUARANTEED_ENDPOINT = "Note: This endpoint is subject to change as NiFi and it's REST API evolve.";
private static final Logger logger = LoggerFactory.getLogger(ApplicationResource.class);
@@ -151,8 +154,11 @@ public abstract class ApplicationResource {
// check for proxy settings
final String scheme = getFirstHeaderValue(PROXY_SCHEME_HTTP_HEADER, FORWARDED_PROTO_HTTP_HEADER);
- final String host = getFirstHeaderValue(PROXY_HOST_HTTP_HEADER, FORWARDED_HOST_HTTP_HEADER);
- final String port = getFirstHeaderValue(PROXY_PORT_HTTP_HEADER, FORWARDED_PORT_HTTP_HEADER);
+ final String hostHeaderValue = getFirstHeaderValue(PROXY_HOST_HTTP_HEADER, FORWARDED_HOST_HTTP_HEADER);
+ final String portHeaderValue = getFirstHeaderValue(PROXY_PORT_HTTP_HEADER, FORWARDED_PORT_HTTP_HEADER);
+
+ final String host = determineProxiedHost(hostHeaderValue);
+ final String port = determineProxiedPort(hostHeaderValue, portHeaderValue);
// Catch header poisoning
String whitelistedContextPaths = properties.getWhitelistedContextPaths();
@@ -188,6 +194,44 @@ public abstract class ApplicationResource {
return uri;
}
+ private String determineProxiedHost(String hostHeaderValue) {
+ final String host;
+ // check for a port in the proxied host header
+ String[] hostSplits = hostHeaderValue == null ? new String[] {} : hostHeaderValue.split(":");
+ if (hostSplits.length >= 1 && hostSplits.length <= 2) {
+ // zero or one occurrence of ':', this is an IPv4 address
+ // strip off the port by reassigning host the 0th split
+ host = hostSplits[0];
+ } else if (hostSplits.length == 0) {
+ // hostHeaderValue passed in was null, no splits
+ host = null;
+ } else {
+ // hostHeaderValue has more than one occurrence of ":", IPv6 address
+ host = hostHeaderValue;
+ }
+ return host;
+ }
+
+ private String determineProxiedPort(String hostHeaderValue, String portHeaderValue) {
+ final String port;
+ // check for a port in the proxied host header
+ String[] hostSplits = hostHeaderValue == null ? new String[] {} : hostHeaderValue.split(":");
+ // determine the proxied port
+ final String portFromHostHeader;
+ if (hostSplits.length == 2) {
+ // if the port is specified in the proxied host header, it will be overridden by the
+ // port specified in X-ProxyPort or X-Forwarded-Port
+ portFromHostHeader = hostSplits[1];
+ } else {
+ portFromHostHeader = null;
+ }
+ if (StringUtils.isNotBlank(portFromHostHeader) && StringUtils.isNotBlank(portHeaderValue)) {
+ logger.warn(String.format("The proxied host header contained a port, but was overridden by the proxied port header"));
+ }
+ port = StringUtils.isNotBlank(portHeaderValue) ? portHeaderValue : (StringUtils.isNotBlank(portFromHostHeader) ? portFromHostHeader : null);
+ return port;
+ }
+
/**
* Edit the response headers to indicating no caching.
*
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/api/ApplicationResourceTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/api/ApplicationResourceTest.groovy
index 7e773c1..00e888c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/api/ApplicationResourceTest.groovy
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/api/ApplicationResourceTest.groovy
@@ -36,6 +36,9 @@ import javax.ws.rs.core.UriInfo
class ApplicationResourceTest extends GroovyTestCase {
private static final Logger logger = LoggerFactory.getLogger(ApplicationResourceTest.class)
+ public static final String PROXY_HOST_HTTP_HEADER = "X-ProxyHost"
+ public static final String FORWARDED_HOST_HTTP_HEADER = "X-Forwarded-Host"
+
static final String PROXY_SCHEME_HTTP_HEADER = "X-ProxyScheme"
static final String PROXY_PORT_HTTP_HEADER = "X-ProxyPort"
static final String PROXY_CONTEXT_PATH_HTTP_HEADER = "X-ProxyContextPath"
@@ -43,6 +46,7 @@ class ApplicationResourceTest extends GroovyTestCase {
static final String FORWARDED_PROTO_HTTP_HEADER = "X-Forwarded-Proto"
static final String FORWARDED_PORT_HTTP_HEADER = "X-Forwarded-Port"
static final String FORWARDED_CONTEXT_HTTP_HEADER = "X-Forwarded-Context"
+ static final String FORWARDED_PREFIX_HTTP_HEADER = "X-Forwarded-Prefix"
static final String PROXY_CONTEXT_PATH_PROP = NiFiProperties.WEB_PROXY_CONTEXT_PATH
static final String WHITELISTED_PATH = "/some/context/path"
@@ -73,21 +77,29 @@ class ApplicationResourceTest extends GroovyTestCase {
}
private ApplicationResource buildApplicationResource() {
+ buildApplicationResource([FORWARDED_PREFIX_HTTP_HEADER, FORWARDED_CONTEXT_HTTP_HEADER, PROXY_CONTEXT_PATH_HTTP_HEADER])
+ }
+
+ private ApplicationResource buildApplicationResource(List proxyHeaders) {
ApplicationResource resource = new MockApplicationResource()
+ String headerValue = ""
HttpServletRequest mockRequest = [getHeader: { String k ->
- logger.mock("Request.getHeader($k)")
- if ([FORWARDED_CONTEXT_HTTP_HEADER, PROXY_CONTEXT_PATH_HTTP_HEADER].contains(k)) {
- WHITELISTED_PATH
+ if (proxyHeaders.contains(k)) {
+ headerValue = WHITELISTED_PATH
} else if ([FORWARDED_PORT_HTTP_HEADER, PROXY_PORT_HTTP_HEADER].contains(k)) {
- "8081"
+ headerValue = "8081"
} else if ([FORWARDED_PROTO_HTTP_HEADER, PROXY_SCHEME_HTTP_HEADER].contains(k)) {
- "https"
+ headerValue = "https"
+ } else if ([PROXY_HOST_HTTP_HEADER, FORWARDED_HOST_HTTP_HEADER].contains(k)) {
+ headerValue = "nifi.apache.org:8081"
} else {
- "nifi.apache.org"
+ headerValue = ""
}
+ logger.mock("Request.getHeader($k) -> \"$headerValue\"")
+ headerValue
}, getContextPath: { ->
- logger.mock("Request.getContextPath()")
- ""
+ logger.mock("Request.getContextPath() -> \"$headerValue\"")
+ headerValue
}] as HttpServletRequest
UriInfo mockUriInfo = [getBaseUriBuilder: { ->
@@ -155,27 +167,100 @@ class ApplicationResourceTest extends GroovyTestCase {
@Test
void testGenerateUriShouldBlockForwardedContextHeaderIfNotInWhitelist() throws Exception {
// Arrange
+ ApplicationResource resource = buildApplicationResource([FORWARDED_CONTEXT_HTTP_HEADER])
+ logger.info("Whitelisted path(s): ")
+
+ // Act
+ def msg = shouldFail(UriBuilderException) {
+ String generatedUri = resource.generateResourceUri('actualResource')
+ logger.unexpected("Generated URI: ${generatedUri}")
+ }
+
+ // Assert
+ logger.expected(msg)
+ assert msg =~ "The provided context path \\[.*\\] was not whitelisted \\[\\]"
+ }
+
+ @Test
+ void testGenerateUriShouldBlockForwardedPrefixHeaderIfNotInWhitelist() throws Exception {
+ // Arrange
+ ApplicationResource resource = buildApplicationResource([FORWARDED_PREFIX_HTTP_HEADER])
+ logger.info("Whitelisted path(s): ")
// Act
+ def msg = shouldFail(UriBuilderException) {
+ String generatedUri = resource.generateResourceUri('actualResource')
+ logger.unexpected("Generated URI: ${generatedUri}")
+ }
// Assert
+ logger.expected(msg)
+ assert msg =~ "The provided context path \\[.*\\] was not whitelisted \\[\\]"
}
@Test
void testGenerateUriShouldAllowForwardedContextHeaderIfInWhitelist() throws Exception {
// Arrange
+ ApplicationResource resource = buildApplicationResource([FORWARDED_CONTEXT_HTTP_HEADER])
+ logger.info("Whitelisted path(s): ${WHITELISTED_PATH}")
+ NiFiProperties niFiProperties = new StandardNiFiProperties([(PROXY_CONTEXT_PATH_PROP): WHITELISTED_PATH] as Properties)
+ resource.properties = niFiProperties
// Act
+ String generatedUri = resource.generateResourceUri('actualResource')
+ logger.info("Generated URI: ${generatedUri}")
// Assert
+ assert generatedUri == "https://nifi.apache.org:8081${WHITELISTED_PATH}/actualResource"
+ }
+
+ @Test
+ void testGenerateUriShouldAllowForwardedPrefixHeaderIfInWhitelist() throws Exception {
+ // Arrange
+ ApplicationResource resource = buildApplicationResource([FORWARDED_PREFIX_HTTP_HEADER])
+ logger.info("Whitelisted path(s): ${WHITELISTED_PATH}")
+ NiFiProperties niFiProperties = new StandardNiFiProperties([(PROXY_CONTEXT_PATH_PROP): WHITELISTED_PATH] as Properties)
+ resource.properties = niFiProperties
+
+ // Act
+ String generatedUri = resource.generateResourceUri('actualResource')
+ logger.info("Generated URI: ${generatedUri}")
+
+ // Assert
+ assert generatedUri == "https://nifi.apache.org:8081${WHITELISTED_PATH}/actualResource"
}
@Test
void testGenerateUriShouldAllowForwardedContextHeaderIfElementInMultipleWhitelist() throws Exception {
// Arrange
+ ApplicationResource resource = buildApplicationResource([FORWARDED_CONTEXT_HTTP_HEADER])
+ String multipleWhitelistedPaths = [WHITELISTED_PATH, "another/path", "a/third/path"].join(",")
+ logger.info("Whitelisted path(s): ${multipleWhitelistedPaths}")
+ NiFiProperties niFiProperties = new StandardNiFiProperties([(PROXY_CONTEXT_PATH_PROP): multipleWhitelistedPaths] as Properties)
+ resource.properties = niFiProperties
// Act
+ String generatedUri = resource.generateResourceUri('actualResource')
+ logger.info("Generated URI: ${generatedUri}")
// Assert
+ assert generatedUri == "https://nifi.apache.org:8081${WHITELISTED_PATH}/actualResource"
+ }
+
+ @Test
+ void testGenerateUriShouldAllowForwardedPrefixHeaderIfElementInMultipleWhitelist() throws Exception {
+ // Arrange
+ ApplicationResource resource = buildApplicationResource([FORWARDED_PREFIX_HTTP_HEADER])
+ String multipleWhitelistedPaths = [WHITELISTED_PATH, "another/path", "a/third/path"].join(",")
+ logger.info("Whitelisted path(s): ${multipleWhitelistedPaths}")
+ NiFiProperties niFiProperties = new StandardNiFiProperties([(PROXY_CONTEXT_PATH_PROP): multipleWhitelistedPaths] as Properties)
+ resource.properties = niFiProperties
+
+ // Act
+ String generatedUri = resource.generateResourceUri('actualResource')
+ logger.info("Generated URI: ${generatedUri}")
+
+ // Assert
+ assert generatedUri == "https://nifi.apache.org:8081${WHITELISTED_PATH}/actualResource"
}
}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java
index 8ac0179..b4638c9 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java
@@ -57,6 +57,7 @@ public class ContentViewerController extends HttpServlet {
private static final String PROXY_CONTEXT_PATH_HTTP_HEADER = "X-ProxyContextPath";
private static final String FORWARDED_CONTEXT_HTTP_HEADER = "X-Forwarded-Context";
+ private static final String FORWARDED_PREFIX_HTTP_HEADER = "X-Forwarded-Prefix";
/**
* Gets the content and defers to registered viewers to generate the markup.
@@ -311,7 +312,7 @@ public class ContentViewerController extends HttpServlet {
refUriBuilder.scheme(request.getScheme());
// If there is path context from a proxy, remove it since this request will be used inside the cluster
- final String proxyContextPath = getFirstHeaderValue(request, PROXY_CONTEXT_PATH_HTTP_HEADER, FORWARDED_CONTEXT_HTTP_HEADER);
+ final String proxyContextPath = getFirstHeaderValue(request, PROXY_CONTEXT_PATH_HTTP_HEADER, FORWARDED_CONTEXT_HTTP_HEADER, FORWARDED_PREFIX_HTTP_HEADER);
if (StringUtils.isNotBlank(proxyContextPath)) {
refUriBuilder.replacePath(StringUtils.substringAfter(UriBuilder.fromUri(ref).build().getPath(), proxyContextPath));
}