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 <[email protected]>
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 <[email protected]>
---
 .../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));
         }

Reply via email to