You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@knox.apache.org by mo...@apache.org on 2019/05/18 12:57:20 UTC

[knox] branch master updated: KNOX-1858 - Add service name to X-Forwarded-Context header (#90)

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

more 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 bdfbe08  KNOX-1858 - Add service name to X-Forwarded-Context header (#90)
bdfbe08 is described below

commit bdfbe08ca452b5659e62ca269bd83e33c6c6fd0d
Author: Sandeep Moré <mo...@apache.org>
AuthorDate: Sat May 18 08:57:16 2019 -0400

    KNOX-1858 - Add service name to X-Forwarded-Context header (#90)
---
 gateway-release/home/conf/gateway-site.xml         |   5 +
 .../gateway/filter/XForwardedHeaderFilter.java     |  22 ++-
 .../filter/XForwardedHeaderRequestWrapper.java     |  45 +++++-
 .../gateway/filter/XForwardHeaderFilterTest.java   | 170 +++++++++++++++++++++
 .../gateway/config/impl/GatewayConfigImpl.java     |  21 +++
 .../impl/ApplicationDeploymentContributor.java     |  12 +-
 .../ServiceDefinitionDeploymentContributor.java    |  25 ++-
 .../apache/knox/gateway/config/GatewayConfig.java  |  11 ++
 .../org/apache/knox/gateway/GatewayTestConfig.java |  13 ++
 9 files changed, 318 insertions(+), 6 deletions(-)

diff --git a/gateway-release/home/conf/gateway-site.xml b/gateway-release/home/conf/gateway-site.xml
index 535b9d4..2b214da 100644
--- a/gateway-release/home/conf/gateway-site.xml
+++ b/gateway-release/home/conf/gateway-site.xml
@@ -139,5 +139,10 @@ limitations under the License.
         <description>The whitelist to be applied for dispatches associated with the service roles specified by gateway.dispatch.whitelist.services.
         If the value is DEFAULT, a domain-based whitelist will be derived from the Knox host.</description>
     </property>
+    <property>
+        <name>gateway.xforwarded.header.context.append.servicename</name>
+        <value>LIVYSERVER</value>
+        <description>Add service name to x-forward-context header for the list of services defined above.</description>
+    </property>
 
 </configuration>
diff --git a/gateway-server-xforwarded-filter/src/main/java/org/apache/knox/gateway/filter/XForwardedHeaderFilter.java b/gateway-server-xforwarded-filter/src/main/java/org/apache/knox/gateway/filter/XForwardedHeaderFilter.java
index cd6fda9..7b0bc1b 100644
--- a/gateway-server-xforwarded-filter/src/main/java/org/apache/knox/gateway/filter/XForwardedHeaderFilter.java
+++ b/gateway-server-xforwarded-filter/src/main/java/org/apache/knox/gateway/filter/XForwardedHeaderFilter.java
@@ -18,6 +18,7 @@
 package org.apache.knox.gateway.filter;
 
 import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -25,8 +26,27 @@ import java.io.IOException;
 
 public class XForwardedHeaderFilter extends AbstractGatewayFilter {
 
+  private static final String APPEND_SERVICE_NAME_PARAM = "isAppendServiceName";
+  /**
+   * There could be a case where a service might use double context paths
+   * e.g. "/livy/v1" instead of "livy".
+   * This parameter can be used to add such context (/livy/v1) to the
+   * X-Forward-Context header.
+   */
+  private static final String SERVICE_CONTEXT = "serviceContext";
+
+  private boolean isAppendServiceName;
+  private String serviceContext;
+
+  @Override
+  public void init( FilterConfig filterConfig ) throws ServletException {
+    super.init( filterConfig );
+    isAppendServiceName = Boolean.parseBoolean(filterConfig.getInitParameter(APPEND_SERVICE_NAME_PARAM));
+    serviceContext = filterConfig.getInitParameter(SERVICE_CONTEXT);
+  }
+
   @Override
   protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
-    chain.doFilter( new XForwardedHeaderRequestWrapper( request ), response );
+    chain.doFilter( new XForwardedHeaderRequestWrapper( request ,  isAppendServiceName, serviceContext), response );
   }
 }
diff --git a/gateway-server-xforwarded-filter/src/main/java/org/apache/knox/gateway/filter/XForwardedHeaderRequestWrapper.java b/gateway-server-xforwarded-filter/src/main/java/org/apache/knox/gateway/filter/XForwardedHeaderRequestWrapper.java
index ba8867d..2d32b5e 100644
--- a/gateway-server-xforwarded-filter/src/main/java/org/apache/knox/gateway/filter/XForwardedHeaderRequestWrapper.java
+++ b/gateway-server-xforwarded-filter/src/main/java/org/apache/knox/gateway/filter/XForwardedHeaderRequestWrapper.java
@@ -55,12 +55,21 @@ public class XForwardedHeaderRequestWrapper extends GatewayRequestWrapper {
 
   public XForwardedHeaderRequestWrapper(HttpServletRequest request) {
     super( request );
+    setHeaders(request, false, null);
+  }
+
+  public XForwardedHeaderRequestWrapper(HttpServletRequest request, final boolean isAppendServiceName, final String serviceContext) {
+    super( request );
+    setHeaders(request, isAppendServiceName, serviceContext);
+  }
+
+  private void setHeaders(final HttpServletRequest request, final boolean isAppendServiceName, final String serviceContext) {
     setHeader( X_FORWARDED_FOR_LOWER, getForwardedFor( request ) );
     setHeader( X_FORWARDED_PROTO_LOWER, getForwardedProto( request ) );
     setHeader( X_FORWARDED_PORT_LOWER, getForwardedPort( request ) );
     setHeader( X_FORWARDED_HOST_LOWER, getForwardedHost( request ) );
     setHeader( X_FORWARDED_SERVER_LOWER, getForwardedServer( request ) );
-    setHeader( X_FORWARDED_CONTEXT_LOWER, getForwardedContext( request ) );
+    setHeader( X_FORWARDED_CONTEXT_LOWER, getForwardedContext( request, isAppendServiceName, serviceContext) );
   }
 
   @Override
@@ -144,9 +153,39 @@ public class XForwardedHeaderRequestWrapper extends GatewayRequestWrapper {
     return request.getServerName();
   }
 
-  private static String getForwardedContext( HttpServletRequest request ) {
+  private static String getForwardedContext( HttpServletRequest request, final boolean isAppendServiceName, final String serviceContext) {
     String remote = request.getHeader( X_FORWARDED_CONTEXT );
-    String local = request.getContextPath();
+    String local;
+    /* prefer parameter defined in topology over the gateway-site.xml property */
+    if(serviceContext != null && !serviceContext.isEmpty()) {
+      local = request.getContextPath() + "/" + serviceContext;
+    }
+    else if(isAppendServiceName) {
+      local = request.getContextPath() + "/" + extractServiceName(request.getContextPath(), request.getRequestURI());
+    } else {
+      local = request.getContextPath();
+    }
     return ( remote == null ? "" : remote ) + ( local == null ? "" : local );
   }
+
+  /**
+   * Given a URI path and context, this function extracts service name
+   *
+   * @param originalContext {gatewayName}/{topologyname}
+   * @param path            request URI
+   * @return service name
+   * @since 1.3.0
+   */
+  private static String extractServiceName(final String originalContext,
+      final String path) {
+    final String[] sub = path.split(originalContext);
+    String serviceName = "";
+
+    if (sub.length > 1) {
+      final String[] paths = sub[1].split("/");
+      serviceName = paths[1];
+    }
+    return serviceName;
+  }
+
 }
diff --git a/gateway-server-xforwarded-filter/src/test/java/org/apache/knox/gateway/filter/XForwardHeaderFilterTest.java b/gateway-server-xforwarded-filter/src/test/java/org/apache/knox/gateway/filter/XForwardHeaderFilterTest.java
index 8050a3b..5665139 100644
--- a/gateway-server-xforwarded-filter/src/test/java/org/apache/knox/gateway/filter/XForwardHeaderFilterTest.java
+++ b/gateway-server-xforwarded-filter/src/test/java/org/apache/knox/gateway/filter/XForwardHeaderFilterTest.java
@@ -76,6 +76,63 @@ public class XForwardHeaderFilterTest {
     }
   }
 
+  /**
+   * Dummy XForwardedHeaderFilter that is used to call
+   * XForwardedHeaderRequestWrapper letting it know
+   * to append service name.
+   * @since 1.3.0
+   */
+  public static class DummyXForwardedHeaderFilter extends XForwardedHeaderFilter {
+
+    boolean isAppendServiceName;
+    String serviceContext;
+
+    DummyXForwardedHeaderFilter(final boolean isAppendServiceName, final String serviceContext) {
+      super();
+      this.isAppendServiceName = isAppendServiceName;
+      this.serviceContext = serviceContext;
+    }
+
+    @Override
+    protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
+      chain.doFilter( new XForwardedHeaderRequestWrapper( request ,  this.isAppendServiceName, this.serviceContext), response );
+    }
+  }
+
+  /**
+   * Dummy class that tests for proper X-Forwarded-Context assertion.
+   * @since 1.3.0
+   */
+  public static class AssertProxiedXForwardedContextHeaders extends TestFilterAdapter {
+
+    boolean appendServiceName;
+    String serviceContext;
+
+    AssertProxiedXForwardedContextHeaders(boolean appendServiceName, String serviceContext) {
+      super();
+      this.appendServiceName = appendServiceName;
+      this.serviceContext = serviceContext;
+    }
+
+    @Override
+    public void doFilter( HttpServletRequest request, HttpServletResponse response, FilterChain chain ) throws IOException, ServletException {
+      assertThat( request.getHeader( "X-Forwarded-For" ), is( "127.0.0.0,127.0.0.1" ) );
+      assertThat( request.getHeader( "X-Forwarded-Proto" ), is( "https" ) );
+      assertThat( request.getHeader( "X-Forwarded-Port" ), is( "9999" ) );
+      assertThat( request.getHeader( "X-Forwarded-Host" ), is( "remotehost:9999" ) );
+      assertThat( request.getHeader( "X-Forwarded-Server" ), is( "localhost" ) );
+      if(serviceContext!=null) {
+        assertThat( request.getHeader( "X-Forwarded-Context" ), is( "/gateway/sandbox/"+serviceContext ) );
+      }
+      else if(appendServiceName) {
+        assertThat( request.getHeader( "X-Forwarded-Context" ), is( "/gateway/sandbox/webhdfs" ) );
+      } else {
+        assertThat( request.getHeader( "X-Forwarded-Context" ), is( "/gateway/sandbox" ) );
+      }
+
+    }
+  }
+
   @Test
   public void testProxiedXForwardHeaders() throws ServletException, IOException {
     HttpServletRequest request = EasyMock.createNiceMock( HttpServletRequest.class );
@@ -105,4 +162,117 @@ public class XForwardHeaderFilterTest {
     chain.push( filter );
     chain.doFilter( request, response );
   }
+
+  /**
+   * Test the case where service name is appended to X-Forwarded-Context
+   * along with request context.
+   * @throws ServletException
+   * @throws IOException
+   * @since 1.3.0
+   */
+  @Test
+  public void testProxiedXForwardContextHeaders() throws ServletException, IOException {
+    HttpServletRequest request = EasyMock.createNiceMock( HttpServletRequest.class );
+
+    EasyMock.expect( request.getHeader( "X-Forwarded-For" ) ).andReturn( "127.0.0.0" ).anyTimes();
+    EasyMock.expect( request.getHeader( "X-Forwarded-Proto" ) ).andReturn( "https" ).anyTimes();
+    EasyMock.expect( request.getHeader( "X-Forwarded-Port" ) ).andReturn( "9999" ).anyTimes();
+    EasyMock.expect( request.getHeader( "X-Forwarded-Host" ) ).andReturn( "remotehost:9999" ).anyTimes();
+    EasyMock.expect( request.getHeader( "X-Forwarded-Server" ) ).andReturn( "remotehost" ).anyTimes();
+
+    EasyMock.expect( request.getRemoteAddr() ).andReturn( "127.0.0.1" ).anyTimes();
+    EasyMock.expect( request.isSecure() ).andReturn( false ).anyTimes();
+    EasyMock.expect( request.getLocalPort() ).andReturn( 8888 ).anyTimes();
+    EasyMock.expect( request.getHeader( "Host" ) ).andReturn( "localhost:8888" ).anyTimes();
+    EasyMock.expect( request.getServerName() ).andReturn( "localhost" ).anyTimes();
+    EasyMock.expect( request.getContextPath() ).andReturn( "/gateway/sandbox" ).anyTimes();
+    EasyMock.expect( request.getRequestURI() ).andReturn( "/gateway/sandbox/webhdfs/key?value" ).anyTimes();
+
+    HttpServletResponse response = EasyMock.createNiceMock( HttpServletResponse.class );
+    EasyMock.replay( request, response );
+
+    TestFilterChain chain = new TestFilterChain();
+
+    XForwardedHeaderFilter filter = new DummyXForwardedHeaderFilter(true, null);
+
+    chain.push( new AssertProxiedXForwardedContextHeaders(true, null) );
+    chain.push( filter );
+    chain.doFilter( request, response );
+  }
+
+  /**
+   * Test the case where service name is appended to X-Forwarded-Context
+   * along with request context.
+   * @throws ServletException
+   * @throws IOException
+   * @since 1.3.0
+   */
+  @Test
+  public void testProxiedXForwardContextHeadersServiceParam() throws ServletException, IOException {
+    HttpServletRequest request = EasyMock.createNiceMock( HttpServletRequest.class );
+
+    EasyMock.expect( request.getHeader( "X-Forwarded-For" ) ).andReturn( "127.0.0.0" ).anyTimes();
+    EasyMock.expect( request.getHeader( "X-Forwarded-Proto" ) ).andReturn( "https" ).anyTimes();
+    EasyMock.expect( request.getHeader( "X-Forwarded-Port" ) ).andReturn( "9999" ).anyTimes();
+    EasyMock.expect( request.getHeader( "X-Forwarded-Host" ) ).andReturn( "remotehost:9999" ).anyTimes();
+    EasyMock.expect( request.getHeader( "X-Forwarded-Server" ) ).andReturn( "remotehost" ).anyTimes();
+
+    EasyMock.expect( request.getRemoteAddr() ).andReturn( "127.0.0.1" ).anyTimes();
+    EasyMock.expect( request.isSecure() ).andReturn( false ).anyTimes();
+    EasyMock.expect( request.getLocalPort() ).andReturn( 8888 ).anyTimes();
+    EasyMock.expect( request.getHeader( "Host" ) ).andReturn( "localhost:8888" ).anyTimes();
+    EasyMock.expect( request.getServerName() ).andReturn( "localhost" ).anyTimes();
+    EasyMock.expect( request.getContextPath() ).andReturn( "/gateway/sandbox" ).anyTimes();
+    EasyMock.expect( request.getRequestURI() ).andReturn( "/gateway/sandbox/livy/v1/key?value" ).anyTimes();
+
+    HttpServletResponse response = EasyMock.createNiceMock( HttpServletResponse.class );
+    EasyMock.replay( request, response );
+
+    TestFilterChain chain = new TestFilterChain();
+
+    XForwardedHeaderFilter filter = new DummyXForwardedHeaderFilter(true, "livy/v1");
+
+    chain.push( new AssertProxiedXForwardedContextHeaders(true, "livy/v1") );
+    chain.push( filter );
+    chain.doFilter( request, response );
+  }
+
+  /**
+   * Test the case where appending service name to X-Forwarded-Context is
+   * disabled
+   *
+   * @throws ServletException
+   * @throws IOException
+   * @since 1.3.0
+   */
+  @Test
+  public void testProxiedXForwardContextHeadersNegativeTest() throws ServletException, IOException {
+    HttpServletRequest request = EasyMock.createNiceMock( HttpServletRequest.class );
+
+    EasyMock.expect( request.getHeader( "X-Forwarded-For" ) ).andReturn( "127.0.0.0" ).anyTimes();
+    EasyMock.expect( request.getHeader( "X-Forwarded-Proto" ) ).andReturn( "https" ).anyTimes();
+    EasyMock.expect( request.getHeader( "X-Forwarded-Port" ) ).andReturn( "9999" ).anyTimes();
+    EasyMock.expect( request.getHeader( "X-Forwarded-Host" ) ).andReturn( "remotehost:9999" ).anyTimes();
+    EasyMock.expect( request.getHeader( "X-Forwarded-Server" ) ).andReturn( "remotehost" ).anyTimes();
+
+    EasyMock.expect( request.getRemoteAddr() ).andReturn( "127.0.0.1" ).anyTimes();
+    EasyMock.expect( request.isSecure() ).andReturn( false ).anyTimes();
+    EasyMock.expect( request.getLocalPort() ).andReturn( 8888 ).anyTimes();
+    EasyMock.expect( request.getHeader( "Host" ) ).andReturn( "localhost:8888" ).anyTimes();
+    EasyMock.expect( request.getServerName() ).andReturn( "localhost" ).anyTimes();
+    EasyMock.expect( request.getContextPath() ).andReturn( "/gateway/sandbox" ).anyTimes();
+    EasyMock.expect( request.getRequestURI() ).andReturn( "/gateway/sandbox/webhdfs/key?value" ).anyTimes();
+
+    HttpServletResponse response = EasyMock.createNiceMock( HttpServletResponse.class );
+    EasyMock.replay( request, response );
+
+    TestFilterChain chain = new TestFilterChain();
+
+    XForwardedHeaderFilter filter = new DummyXForwardedHeaderFilter(false, null);
+
+    chain.push( new AssertProxiedXForwardedContextHeaders(false, null) );
+    chain.push( filter );
+    chain.doFilter( request, response );
+  }
+
 }
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
index df7503b..e7e9124 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
@@ -230,6 +230,9 @@ public class GatewayConfigImpl extends Configuration implements GatewayConfig {
       "NAMENODE", "JOBTRACKER", "WEBHDFS", "WEBHCAT",
       "OOZIE", "WEBHBASE", "HIVE", "RESOURCEMANAGER");
 
+  /* property that specifies list of services for which we need to append service name to the X-Forward-Context header */
+  public static final String X_FORWARD_CONTEXT_HEADER_APPEND_SERVICES = GATEWAY_CONFIG_FILE_PREFIX + ".xforwarded.header.context.append.servicename";
+
   public GatewayConfigImpl() {
     init();
   }
@@ -1051,4 +1054,22 @@ public class GatewayConfigImpl extends Configuration implements GatewayConfig {
     final String result = get(STRICT_TOPOLOGY_VALIDATION, Boolean.toString(DEFAULT_STRICT_TOPOLOGY_VALIDATION));
     return Boolean.parseBoolean(result);
   }
+
+  /**
+   * Returns a list of services that need service name appended to
+   * X-Forward-Context header as a result of which the new header would look
+   * /{gateway}/{sandbox}/{serviceName}
+   *
+   * @return
+   * @since 1.3.0
+   */
+  @Override
+  public List<String> getXForwardContextAppendServices() {
+    String value = get( X_FORWARD_CONTEXT_HEADER_APPEND_SERVICES );
+    if ( value != null && !value.isEmpty() && !"none".equalsIgnoreCase(value.trim()) ) {
+      return Arrays.asList( value.trim().split("\\s*,\\s*") );
+    } else {
+      return new ArrayList<>();
+    }
+  }
 }
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/deploy/impl/ApplicationDeploymentContributor.java b/gateway-server/src/main/java/org/apache/knox/gateway/deploy/impl/ApplicationDeploymentContributor.java
index f8e3114..8ea6787 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/deploy/impl/ApplicationDeploymentContributor.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/deploy/impl/ApplicationDeploymentContributor.java
@@ -64,6 +64,7 @@ public class ApplicationDeploymentContributor extends ServiceDeploymentContribut
   private static final String XFORWARDED_FILTER_ROLE = "xforwardedheaders";
   private static final String COOKIE_SCOPING_FILTER_NAME = "CookieScopeServletFilter";
   private static final String COOKIE_SCOPING_FILTER_ROLE = "cookiescopef";
+  private static final String APPEND_SERVICE_NAME_PARAM = "isAppendServiceName";
 
   private ServiceDefinition serviceDefinition;
 
@@ -185,7 +186,16 @@ public class ApplicationDeploymentContributor extends ServiceDeploymentContribut
     resource.pattern(binding.getPath());
     //add x-forwarded filter if enabled in config
     if (context.getGatewayConfig().isXForwardedEnabled()) {
-      resource.addFilter().name(XFORWARDED_FILTER_NAME).role(XFORWARDED_FILTER_ROLE).impl(XForwardedHeaderFilter.class);
+      final FilterDescriptor filter = resource.addFilter()
+          .name(XFORWARDED_FILTER_NAME).role(XFORWARDED_FILTER_ROLE)
+          .impl(XForwardedHeaderFilter.class);
+      /* check if we need to add service name to the context */
+      if (context.getGatewayConfig().getXForwardContextAppendServices() != null
+          && !context.getGatewayConfig().getXForwardContextAppendServices()
+          .isEmpty() && context.getGatewayConfig()
+          .getXForwardContextAppendServices().contains(service.getRole())) {
+        filter.param().name(APPEND_SERVICE_NAME_PARAM).value("true");
+      }
     }
     if (context.getGatewayConfig().isCookieScopingToPathEnabled()) {
       FilterDescriptor filter = resource.addFilter().name(COOKIE_SCOPING_FILTER_NAME).role(COOKIE_SCOPING_FILTER_ROLE).impl(CookieScopeServletFilter.class);
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/deploy/impl/ServiceDefinitionDeploymentContributor.java b/gateway-server/src/main/java/org/apache/knox/gateway/deploy/impl/ServiceDefinitionDeploymentContributor.java
index b192ee0..202fe1d 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/deploy/impl/ServiceDefinitionDeploymentContributor.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/deploy/impl/ServiceDefinitionDeploymentContributor.java
@@ -64,6 +64,16 @@ public class ServiceDefinitionDeploymentContributor extends ServiceDeploymentCon
 
   private static final String COOKIE_SCOPING_FILTER_ROLE = "cookiescopef";
 
+  private static final String APPEND_SERVICE_NAME_PARAM = "isAppendServiceName";
+
+  /**
+   * There could be a case where a service might use double context paths
+   * e.g. "/livy/v1" instead of "livy".
+   * This parameter can be used to add such context (/livy/v1) to the
+   * X-Forward-Context header.
+   */
+  private static final String SERVICE_CONTEXT = "serviceContext";
+
   private ServiceDefinition serviceDefinition;
 
   private UrlRewriteRulesDescriptor serviceRules;
@@ -128,7 +138,20 @@ public class ServiceDefinitionDeploymentContributor extends ServiceDeploymentCon
     resource.pattern(binding.getPath());
     //add x-forwarded filter if enabled in config
     if (context.getGatewayConfig().isXForwardedEnabled()) {
-      resource.addFilter().name(XFORWARDED_FILTER_NAME).role(XFORWARDED_FILTER_ROLE).impl(XForwardedHeaderFilter.class);
+      final FilterDescriptor filter = resource.addFilter()
+          .name(XFORWARDED_FILTER_NAME).role(XFORWARDED_FILTER_ROLE)
+          .impl(XForwardedHeaderFilter.class);
+      /* check if we need to add service name to the context */
+      if(service.getParams().containsKey(SERVICE_CONTEXT)) {
+        filter.param().name(APPEND_SERVICE_NAME_PARAM).value("true");
+        filter.param().name(SERVICE_CONTEXT).value(service.getParams().get(SERVICE_CONTEXT));
+      }
+      else if (context.getGatewayConfig().getXForwardContextAppendServices() != null
+          && !context.getGatewayConfig().getXForwardContextAppendServices()
+          .isEmpty() && context.getGatewayConfig()
+          .getXForwardContextAppendServices().contains(service.getRole())) {
+        filter.param().name(APPEND_SERVICE_NAME_PARAM).value("true");
+      }
     }
     if (context.getGatewayConfig().isCookieScopingToPathEnabled()) {
       FilterDescriptor filter = resource.addFilter().name(COOKIE_SCOPING_FILTER_NAME).role(COOKIE_SCOPING_FILTER_ROLE).impl(CookieScopeServletFilter.class);
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
index f9d32ce..5de18b6 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
@@ -606,5 +606,16 @@ public interface GatewayConfig {
    */
   boolean isTopologyValidationEnabled();
 
+  /**
+   * Returns a list of services that need service name appended to
+   * X-Forward-Context header as a result of which the new header would look
+   * /{gateway}/{sandbox}/{serviceName}
+   *
+   * @return List of service names for which service name needs to be appended
+   * to X-Forward-Context header, can be empty list.
+   * @since 1.3.0
+   */
+  List<String> getXForwardContextAppendServices();
+
 
 }
diff --git a/gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java b/gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
index 4572047..951d173 100644
--- a/gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
+++ b/gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
@@ -746,4 +746,17 @@ public class GatewayTestConfig extends Configuration implements GatewayConfig {
   public boolean isTopologyValidationEnabled() {
     return false;
   }
+
+  /**
+   * Returns a list of services that need service name appended to
+   * X-Forward-Context header as a result of which the new header would look
+   * /{gateway}/{sandbox}/{serviceName}
+   *
+   * @return
+   * @since 1.3.0
+   */
+  @Override
+  public List<String> getXForwardContextAppendServices() {
+    return null;
+  }
 }