This is an automated email from the ASF dual-hosted git repository.

smolnar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git


The following commit(s) were added to refs/heads/master by this push:
     new 7d17994ca KNOX-3193: Add fragmentation to NiFi redirect Location 
header (#1087)
7d17994ca is described below

commit 7d17994cadee1add2019fd0aff6ad1d1e8c7e55f
Author: hanicz <[email protected]>
AuthorDate: Tue Sep 23 15:10:20 2025 +0200

    KNOX-3193: Add fragmentation to NiFi redirect Location header (#1087)
---
 .../knox/gateway/dispatch/NiFiResponseUtil.java    |  11 +-
 .../gateway/dispatch/NiFiResponseUtilTest.java     | 235 +++++++++++++++++++++
 2 files changed, 243 insertions(+), 3 deletions(-)

diff --git 
a/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiResponseUtil.java
 
b/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiResponseUtil.java
index 29c3ec1a4..701c17944 100644
--- 
a/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiResponseUtil.java
+++ 
b/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiResponseUtil.java
@@ -52,7 +52,7 @@ class NiFiResponseUtil {
           throw new RuntimeException("Unable to parse the inbound request 
URI", e);
         }
         /*
-         * if the path specified in the Location header fron the inbound 
response contains the inbound request's URI's path,
+         * if the path specified in the Location header from the inbound 
response contains the inbound request's URI's path,
          * then it's going to the same web context, and the Location header 
should be updated based on the X_FORWARDED_* headers.
          */
         String inboundRequestUriPath = inboundRequestUriBuilder.getPath();
@@ -70,10 +70,15 @@ class NiFiResponseUtil {
 
           final String baseContextPath = 
inboundRequest.getHeader(NiFiHeaders.X_FORWARDED_CONTEXT);
           final String pathInfo = inboundRequest.getPathInfo();
+          final String fragment = originalLocationUriBuilder.getFragment();
 
           try {
-            final URI newLocation = new 
URIBuilder().setScheme(scheme).setHost(host).setPort((StringUtils.isNumeric(port)
 ? Integer.parseInt(port) : -1)).setPath(
-                baseContextPath + pathInfo + 
trailingSlash).setParameters(queryParams).build();
+              final URIBuilder builder = new 
URIBuilder().setScheme(scheme).setHost(host).setPort((StringUtils.isNumeric(port)
 ? Integer.parseInt(port) : -1)).setPath(
+                      baseContextPath + pathInfo + 
trailingSlash).setParameters(queryParams);
+              if(StringUtils.isNotBlank(fragment)) {
+                  builder.setFragment(fragment);
+              }
+              final URI newLocation = builder.build();
             outboundResponse.setHeader("Location", newLocation.toString());
           } catch (URISyntaxException e) {
             throw new RuntimeException("Unable to rewrite Location header in 
response", e);
diff --git 
a/gateway-service-nifi/src/test/java/org/apache/knox/gateway/dispatch/NiFiResponseUtilTest.java
 
b/gateway-service-nifi/src/test/java/org/apache/knox/gateway/dispatch/NiFiResponseUtilTest.java
new file mode 100644
index 000000000..9609e0b7f
--- /dev/null
+++ 
b/gateway-service-nifi/src/test/java/org/apache/knox/gateway/dispatch/NiFiResponseUtilTest.java
@@ -0,0 +1,235 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.knox.gateway.dispatch;
+
+import org.apache.http.Header;
+import org.apache.http.HttpResponse;
+import org.apache.http.StatusLine;
+import org.apache.http.message.BasicHeader;
+import org.easymock.Capture;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class NiFiResponseUtilTest {
+
+    @Test
+    public void testModifyOutboundResponse_302WithLocationHeader() throws 
IOException {
+        HttpServletRequest inboundRequest = 
createMock(HttpServletRequest.class);
+        HttpServletResponse outboundResponse = 
createMock(HttpServletResponse.class);
+        HttpResponse inboundResponse = createMock(HttpResponse.class);
+        StatusLine statusLine = createMock(StatusLine.class);
+        Capture<String> locationCapture = Capture.newInstance();
+
+        setupCommonMocks(inboundRequest, inboundResponse, statusLine,
+                "http://backend:8080/nifi/api/flow/status";,
+                "/nifi/api/flow/status");
+
+        outboundResponse.setHeader(eq("Location"), capture(locationCapture));
+        expectLastCall();
+
+        replay(inboundRequest, outboundResponse, inboundResponse, statusLine);
+        NiFiResponseUtil.modifyOutboundResponse(inboundRequest, 
outboundResponse, inboundResponse);
+        verify(inboundRequest, outboundResponse, inboundResponse, statusLine);
+        assertEquals("URL should be properly transformed",
+                "https://knox.proxy:8443/gateway/default/nifi/api/flow/status";,
+                locationCapture.getValue());
+    }
+
+    @Test
+    public void testModifyOutboundResponse_302WithTrailingSlash() throws 
IOException {
+        HttpServletRequest inboundRequest = 
createMock(HttpServletRequest.class);
+        HttpServletResponse outboundResponse = 
createMock(HttpServletResponse.class);
+        HttpResponse inboundResponse = createMock(HttpResponse.class);
+        StatusLine statusLine = createMock(StatusLine.class);
+        Capture<String> locationCapture = Capture.newInstance();
+
+        setupCommonMocks(inboundRequest, inboundResponse, statusLine,
+                "http://backend:8080/nifi/api/flow/status";,
+                "/nifi/api/flow/");
+
+        outboundResponse.setHeader(eq("Location"), capture(locationCapture));
+        expectLastCall();
+
+        replay(inboundRequest, outboundResponse, inboundResponse, statusLine);
+        NiFiResponseUtil.modifyOutboundResponse(inboundRequest, 
outboundResponse, inboundResponse);
+        verify(inboundRequest, outboundResponse, inboundResponse, statusLine);
+        assertEquals("URL should preserve trailing slash",
+                "https://knox.proxy:8443/gateway/default/nifi/api/flow/";,
+                locationCapture.getValue());
+    }
+
+    @Test
+    public void testModifyOutboundResponse_WithQueryParams() throws 
IOException {
+        HttpServletRequest inboundRequest = 
createMock(HttpServletRequest.class);
+        HttpServletResponse outboundResponse = 
createMock(HttpServletResponse.class);
+        HttpResponse inboundResponse = createMock(HttpResponse.class);
+        StatusLine statusLine = createMock(StatusLine.class);
+
+        Capture<String> locationCapture = Capture.newInstance();
+
+        setupCommonMocks(inboundRequest, inboundResponse, statusLine,
+                
"http://backend:8080/nifi/api/flow/status?param1=value1&param2=value2";,
+                "/nifi/api/flow/status");
+
+        outboundResponse.setHeader(eq("Location"), capture(locationCapture));
+        expectLastCall();
+        replay(inboundRequest, outboundResponse, inboundResponse, statusLine);
+        NiFiResponseUtil.modifyOutboundResponse(inboundRequest, 
outboundResponse, inboundResponse);
+        verify(inboundRequest, outboundResponse, inboundResponse, statusLine);
+        String transformedUrl = locationCapture.getValue();
+        assertTrue("URL should preserve query parameters", 
transformedUrl.contains("param1=value1"));
+        assertTrue("URL should preserve query parameters", 
transformedUrl.contains("param2=value2"));
+        assertTrue("URL should have correct base path",
+                
transformedUrl.startsWith("https://knox.proxy:8443/gateway/default/nifi/api/flow/status";));
+    }
+
+    @Test
+    public void testModifyOutboundResponse_WithFragment() throws IOException {
+        HttpServletRequest inboundRequest = 
createMock(HttpServletRequest.class);
+        HttpServletResponse outboundResponse = 
createMock(HttpServletResponse.class);
+        HttpResponse inboundResponse = createMock(HttpResponse.class);
+        StatusLine statusLine = createMock(StatusLine.class);
+
+        Capture<String> locationCapture = Capture.newInstance();
+
+        setupCommonMocks(inboundRequest, inboundResponse, statusLine,
+                "http://backend:8080/nifi/api/flow/status#section1";,
+                "/nifi/api/flow/status");
+
+        outboundResponse.setHeader(eq("Location"), capture(locationCapture));
+        expectLastCall();
+
+        replay(inboundRequest, outboundResponse, inboundResponse, statusLine);
+
+        NiFiResponseUtil.modifyOutboundResponse(inboundRequest, 
outboundResponse, inboundResponse);
+
+        verify(inboundRequest, outboundResponse, inboundResponse, statusLine);
+        assertEquals("URL should preserve fragment",
+                
"https://knox.proxy:8443/gateway/default/nifi/api/flow/status#section1";,
+                locationCapture.getValue());
+    }
+
+    @Test
+    public void testModifyOutboundResponse_WithQueryParamsAndFragment() throws 
IOException {
+        HttpServletRequest inboundRequest = 
createMock(HttpServletRequest.class);
+        HttpServletResponse outboundResponse = 
createMock(HttpServletResponse.class);
+        HttpResponse inboundResponse = createMock(HttpResponse.class);
+        StatusLine statusLine = createMock(StatusLine.class);
+        Capture<String> locationCapture = Capture.newInstance();
+
+        setupCommonMocks(inboundRequest, inboundResponse, statusLine,
+                
"http://backend:8080/nifi/api/flow/status?param1=value1&param2=value2#section1";,
+                "/nifi/api/flow/status");
+
+        outboundResponse.setHeader(eq("Location"), capture(locationCapture));
+        expectLastCall();
+
+        replay(inboundRequest, outboundResponse, inboundResponse, statusLine);
+        NiFiResponseUtil.modifyOutboundResponse(inboundRequest, 
outboundResponse, inboundResponse);
+        verify(inboundRequest, outboundResponse, inboundResponse, statusLine);
+
+        String transformedUrl = locationCapture.getValue();
+        assertTrue("URL should preserve query parameters", 
transformedUrl.contains("param1=value1"));
+        assertTrue("URL should preserve query parameters", 
transformedUrl.contains("param2=value2"));
+        assertTrue("URL should preserve fragment", 
transformedUrl.endsWith("#section1"));
+        assertTrue("URL should have correct base path",
+                
transformedUrl.startsWith("https://knox.proxy:8443/gateway/default/nifi/api/flow/status";));
+    }
+
+    @Test
+    public void testModifyOutboundResponse_Non302Status() throws IOException {
+        HttpServletRequest inboundRequest = 
createMock(HttpServletRequest.class);
+        HttpServletResponse outboundResponse = 
createMock(HttpServletResponse.class);
+        HttpResponse inboundResponse = createMock(HttpResponse.class);
+        StatusLine statusLine = createMock(StatusLine.class);
+
+        expect(inboundResponse.getStatusLine()).andReturn(statusLine);
+        
expect(statusLine.getStatusCode()).andReturn(HttpServletResponse.SC_OK);
+
+        replay(inboundRequest, outboundResponse, inboundResponse, statusLine);
+        NiFiResponseUtil.modifyOutboundResponse(inboundRequest, 
outboundResponse, inboundResponse);
+        verify(inboundRequest, outboundResponse, inboundResponse, statusLine);
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testModifyOutboundResponse_302WithoutLocationHeader() throws 
IOException {
+        HttpServletRequest inboundRequest = 
createMock(HttpServletRequest.class);
+        HttpServletResponse outboundResponse = 
createMock(HttpServletResponse.class);
+        HttpResponse inboundResponse = createMock(HttpResponse.class);
+        StatusLine statusLine = createMock(StatusLine.class);
+
+        expect(inboundResponse.getStatusLine()).andReturn(statusLine);
+        
expect(statusLine.getStatusCode()).andReturn(HttpServletResponse.SC_FOUND);
+        expect(inboundResponse.getFirstHeader("Location")).andReturn(null);
+
+        replay(inboundRequest, outboundResponse, inboundResponse, statusLine);
+
+        NiFiResponseUtil.modifyOutboundResponse(inboundRequest, 
outboundResponse, inboundResponse);
+    }
+
+    @Test
+    public void testModifyOutboundResponse_LocationNotContainingRequestPath() 
throws IOException {
+        HttpServletRequest inboundRequest = 
createMock(HttpServletRequest.class);
+        HttpServletResponse outboundResponse = 
createMock(HttpServletResponse.class);
+        HttpResponse inboundResponse = createMock(HttpResponse.class);
+        StatusLine statusLine = createMock(StatusLine.class);
+        Header locationHeader = new BasicHeader("Location", 
"http://different.server.com/other/path";);
+
+        expect(inboundResponse.getStatusLine()).andReturn(statusLine);
+        
expect(statusLine.getStatusCode()).andReturn(HttpServletResponse.SC_FOUND);
+        
expect(inboundResponse.getFirstHeader("Location")).andReturn(locationHeader);
+        
expect(inboundRequest.getRequestURI()).andReturn("/nifi/api/flow/status");
+
+        replay(inboundRequest, outboundResponse, inboundResponse, statusLine);
+        NiFiResponseUtil.modifyOutboundResponse(inboundRequest, 
outboundResponse, inboundResponse);
+        verify(inboundRequest, outboundResponse, inboundResponse, statusLine);
+    }
+
+    private void setupCommonMocks(
+            HttpServletRequest inboundRequest,
+            HttpResponse inboundResponse,
+            StatusLine statusLine,
+            String locationUrl,
+            String requestUri) {
+
+        expect(inboundResponse.getStatusLine()).andReturn(statusLine);
+        
expect(statusLine.getStatusCode()).andReturn(HttpServletResponse.SC_FOUND);
+        expect(inboundResponse.getFirstHeader("Location")).andReturn(
+                new BasicHeader("Location", locationUrl));
+        expect(inboundRequest.getRequestURI()).andReturn(requestUri);
+
+        
expect(inboundRequest.getHeader(NiFiHeaders.X_FORWARDED_PROTO)).andReturn("https");
+        
expect(inboundRequest.getHeader(NiFiHeaders.X_FORWARDED_HOST)).andReturn("knox.proxy");
+        
expect(inboundRequest.getHeader(NiFiHeaders.X_FORWARDED_PORT)).andReturn("8443");
+        
expect(inboundRequest.getHeader(NiFiHeaders.X_FORWARDED_CONTEXT)).andReturn("/gateway/default");
+        expect(inboundRequest.getPathInfo()).andReturn(requestUri);
+    }
+}

Reply via email to