You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@nifi.apache.org by GitBox <gi...@apache.org> on 2018/12/17 22:32:49 UTC

[GitHub] kevdoran closed pull request #3129: NIFI-5748 Fixed proxy header support to use X-Forwarded-Host instead …

kevdoran closed pull request #3129: NIFI-5748 Fixed proxy header support to use X-Forwarded-Host instead …
URL: https://github.com/apache/nifi/pull/3129
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

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 90a83a96f6..fbf5c1948a 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.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 @@
 
     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 static String sanitizeContextPath(ServletRequest request, String whitelis
     }
 
     /**
-     * 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 static String determineContextPath(HttpServletRequest request) {
         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 b0a119140e..6b68457a93 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 dabf7eb6c2..4045420cb0 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 1cf5a7c5c8..447da40a26 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 1cf5a7c5c8..447da40a26 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 809453624d..0bb08a9b26 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -3188,8 +3188,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.
@@ -3905,7 +3905,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 d99fb968e5..f675a46f7b 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 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 @@ private URI buildResourceUri(final String... path) {
             // 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 @@ private URI buildResourceUri(final String... path) {
         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 7e773c19db..00e888c067 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 8ac0179e8c..b4638c94e5 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 @@
 
     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 @@ private ContentRequestContext getContentRequest(final HttpServletRequest request
         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));
         }


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services