You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@knox.apache.org by kr...@apache.org on 2019/01/24 15:52:34 UTC

[knox] branch master updated: KNOX-1559 - Create Dispatch implementation that is configurable via service.xml file

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

krisden 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 cd29c4b  KNOX-1559 - Create Dispatch implementation that is configurable via service.xml file
cd29c4b is described below

commit cd29c4b8171d2610cc46113efc68ed319b989a3a
Author: Kevin Risden <kr...@apache.org>
AuthorDate: Mon Nov 19 16:33:27 2018 -0500

    KNOX-1559 - Create Dispatch implementation that is configurable via service.xml file
    
    Signed-off-by: Kevin Risden <kr...@apache.org>
---
 .../gateway/dispatch/AbstractGatewayDispatch.java  |  12 +-
 .../gateway/dispatch/ConfigurableDispatch.java     | 104 +++++++++
 .../knox/gateway/dispatch/DefaultDispatch.java     |  44 ++--
 .../gateway/dispatch/PassAllHeadersDispatch.java   |  11 +-
 .../dispatch/PassAllHeadersNoEncodingDispatch.java |  42 ++--
 .../gateway/dispatch/ConfigurableDispatchTest.java | 258 +++++++++++++++++++++
 6 files changed, 406 insertions(+), 65 deletions(-)

diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/AbstractGatewayDispatch.java b/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/AbstractGatewayDispatch.java
index a973dcc..0214255 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/AbstractGatewayDispatch.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/AbstractGatewayDispatch.java
@@ -29,20 +29,14 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.Arrays;
 import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.Set;
 
 public abstract class AbstractGatewayDispatch implements Dispatch {
-
-  private static final Set<String> REQUEST_EXCLUDE_HEADERS = new HashSet<>();
-
-  static {
-      REQUEST_EXCLUDE_HEADERS.add("Host");
-      REQUEST_EXCLUDE_HEADERS.add("Authorization");
-      REQUEST_EXCLUDE_HEADERS.add("Content-Length");
-      REQUEST_EXCLUDE_HEADERS.add("Transfer-Encoding");
-  }
+  private static final Set<String> REQUEST_EXCLUDE_HEADERS = new HashSet<>(Arrays.asList(
+      "Host", "Authorization", "Content-Length", "Transfer-Encoding"));
 
   protected  HttpClient client;
 
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/ConfigurableDispatch.java b/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/ConfigurableDispatch.java
new file mode 100644
index 0000000..1611307
--- /dev/null
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/ConfigurableDispatch.java
@@ -0,0 +1,104 @@
+/*
+ * 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.knox.gateway.config.Configure;
+import org.apache.knox.gateway.config.Default;
+import javax.servlet.http.HttpServletRequest;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Extends DefaultDispatch to:
+ *   make request/response exclude headers configurable
+ *   make url encoding configurable
+ */
+public class ConfigurableDispatch extends DefaultDispatch {
+  private Set<String> requestExcludeHeaders = super.getOutboundRequestExcludeHeaders();
+  private Set<String> responseExcludeHeaders = super.getOutboundResponseExcludeHeaders();
+  private Boolean removeUrlEncoding = false;
+
+  private Set<String> handleCommaSeparatedHeaders(String headers) {
+    if(headers != null) {
+      return new HashSet<>(Arrays.asList(headers.split(",")));
+    }
+    return Collections.emptySet();
+  }
+
+  @Configure
+  protected void setRequestExcludeHeaders(@Default(" ") String headers) {
+    if(!" ".equals(headers)) {
+      this.requestExcludeHeaders = handleCommaSeparatedHeaders(headers);
+    }
+  }
+
+  @Configure
+  protected void setResponseExcludeHeaders(@Default(" ") String headers) {
+    if(!" ".equals(headers)) {
+      this.responseExcludeHeaders = handleCommaSeparatedHeaders(headers);
+    }
+  }
+
+  @Configure
+  protected void setRemoveUrlEncoding(@Default("false") String removeUrlEncoding) {
+    this.removeUrlEncoding = Boolean.parseBoolean(removeUrlEncoding);
+  }
+
+  @Override
+  public Set<String> getOutboundResponseExcludeHeaders() {
+    return responseExcludeHeaders;
+  }
+
+  @Override
+  public Set<String> getOutboundRequestExcludeHeaders() {
+    return requestExcludeHeaders;
+  }
+
+  public boolean getRemoveUrlEncoding() {
+    return removeUrlEncoding;
+  }
+
+  @Override
+  public URI getDispatchUrl(HttpServletRequest request) {
+    if (getRemoveUrlEncoding()) {
+      String base = request.getRequestURI();
+      StringBuffer str = new StringBuffer();
+      str.append(base);
+      String query = request.getQueryString();
+      if (query != null) {
+        try {
+          query = URLDecoder.decode(query, StandardCharsets.UTF_8.name());
+        } catch (UnsupportedEncodingException e) {
+          // log
+        }
+        str.append('?');
+        str.append(query);
+      }
+      encodeUnwiseCharacters(str);
+      return URI.create(str.toString());
+    }
+
+    return super.getDispatchUrl(request);
+  }
+}
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/DefaultDispatch.java b/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/DefaultDispatch.java
index 8b899b9..5f0b3e4 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/DefaultDispatch.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/DefaultDispatch.java
@@ -50,6 +50,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Locale;
 import java.util.Set;
@@ -64,20 +65,12 @@ public class DefaultDispatch extends AbstractGatewayDispatch {
   protected static final Auditor auditor = AuditServiceFactory.getAuditService().getAuditor(AuditConstants.DEFAULT_AUDITOR_NAME,
       AuditConstants.KNOX_SERVICE_NAME, AuditConstants.KNOX_COMPONENT_NAME);
 
-  private Set<String> outboundResponseExcludeHeaders;
+  private Set<String> outboundResponseExcludeHeaders = new HashSet<>(Arrays.asList(SET_COOKIE, WWW_AUTHENTICATE));
 
   //Buffer size in bytes
   private int replayBufferSize = -1;
 
   @Override
-  public void init() {
-    super.init();
-    outboundResponseExcludeHeaders = new HashSet<>();
-    outboundResponseExcludeHeaders.add(SET_COOKIE);
-    outboundResponseExcludeHeaders.add(WWW_AUTHENTICATE);
-  }
-
-  @Override
   public void destroy() {
 
   }
@@ -151,20 +144,7 @@ public class DefaultDispatch extends AbstractGatewayDispatch {
   protected void writeOutboundResponse(HttpUriRequest outboundRequest, HttpServletRequest inboundRequest, HttpServletResponse outboundResponse, HttpResponse inboundResponse) throws IOException {
     // Copy the client respond header to the server respond.
     outboundResponse.setStatus(inboundResponse.getStatusLine().getStatusCode());
-    Header[] headers = inboundResponse.getAllHeaders();
-    Set<String> excludeHeaders = getOutboundResponseExcludeHeaders();
-    boolean hasExcludeHeaders = false;
-    if ((excludeHeaders != null) && !(excludeHeaders.isEmpty())) {
-      hasExcludeHeaders = true;
-    }
-    for ( Header header : headers ) {
-      String name = header.getName();
-      if (hasExcludeHeaders && excludeHeaders.contains(name.toUpperCase(Locale.ROOT))) {
-        continue;
-      }
-      String value = header.getValue();
-      outboundResponse.addHeader(name, value);
-    }
+    copyResponseHeaderFields(outboundResponse, inboundResponse);
 
     HttpEntity entity = inboundResponse.getEntity();
     if( entity != null ) {
@@ -318,8 +298,24 @@ public class DefaultDispatch extends AbstractGatewayDispatch {
     executeRequest(method, request, response);
   }
 
+  public void copyResponseHeaderFields(HttpServletResponse outboundResponse, HttpResponse inboundResponse) {
+    Header[] headers = inboundResponse.getAllHeaders();
+    Set<String> excludeHeaders = getOutboundResponseExcludeHeaders();
+    boolean hasExcludeHeaders = false;
+    if ((excludeHeaders != null) && !(excludeHeaders.isEmpty())) {
+      hasExcludeHeaders = true;
+    }
+    for ( Header header : headers ) {
+      String name = header.getName();
+      if (hasExcludeHeaders && excludeHeaders.contains(name.toUpperCase(Locale.ROOT))) {
+        continue;
+      }
+      String value = header.getValue();
+      outboundResponse.addHeader(name, value);
+    }
+  }
+
   public Set<String> getOutboundResponseExcludeHeaders() {
     return outboundResponseExcludeHeaders;
   }
-
 }
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/PassAllHeadersDispatch.java b/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/PassAllHeadersDispatch.java
index 6267047..2af8dc9 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/PassAllHeadersDispatch.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/PassAllHeadersDispatch.java
@@ -21,7 +21,7 @@ import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
 
-public class PassAllHeadersDispatch extends DefaultDispatch {
+public class PassAllHeadersDispatch extends ConfigurableDispatch {
 
   private static final Set<String> REQUEST_EXCLUDE_HEADERS = new HashSet<>();
 
@@ -30,8 +30,13 @@ public class PassAllHeadersDispatch extends DefaultDispatch {
   }
 
   @Override
-  public void init() {
-    super.init();
+  protected void setResponseExcludeHeaders(String headers) {
+    super.setResponseExcludeHeaders(String.join(",", REQUEST_EXCLUDE_HEADERS));
+  }
+
+  @Override
+  protected void setRequestExcludeHeaders(String headers) {
+    super.setRequestExcludeHeaders("");
   }
 
   @Override
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/PassAllHeadersNoEncodingDispatch.java b/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/PassAllHeadersNoEncodingDispatch.java
index e947057..6177e0f 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/PassAllHeadersNoEncodingDispatch.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/PassAllHeadersNoEncodingDispatch.java
@@ -17,35 +17,19 @@
  */
 package org.apache.knox.gateway.dispatch;
 
-import java.io.UnsupportedEncodingException;
-import java.net.URI;
-import java.net.URLDecoder;
-import java.nio.charset.StandardCharsets;
-
-import javax.servlet.http.HttpServletRequest;
-
-  /**
-   * This is a specialized PassAllHeadersDispatch dispatch that decodes the URL before
-   * dispatch. Ambari Views do not work with the query string percent encoded. Other
-   * UIs may require this at some point as well.
-   */
+/**
+ * This is a specialized PassAllHeadersDispatch dispatch that decodes the URL before
+ * dispatch. Ambari Views do not work with the query string percent encoded. Other
+ * UIs may require this at some point as well.
+ */
 public class PassAllHeadersNoEncodingDispatch extends PassAllHeadersDispatch {
   @Override
-  public URI getDispatchUrl(HttpServletRequest request) {
-    String base = request.getRequestURI();
-    StringBuffer str = new StringBuffer();
-    str.append( base );
-    String query = request.getQueryString();
-    if (query != null) {
-      try {
-        query = URLDecoder.decode(query, StandardCharsets.UTF_8.name());
-      } catch (UnsupportedEncodingException e) {
-        // log
-      }
-      str.append( '?' );
-      str.append( query );
-    }
-    encodeUnwiseCharacters(str);
-    return URI.create( str.toString() );
+  protected void setRemoveUrlEncoding(String removeUrlEncoding) {
+    super.setRemoveUrlEncoding(Boolean.TRUE.toString());
+  }
+
+  @Override
+  public boolean getRemoveUrlEncoding() {
+    return Boolean.TRUE;
   }
-}
+}
\ No newline at end of file
diff --git a/gateway-spi/src/test/java/org/apache/knox/gateway/dispatch/ConfigurableDispatchTest.java b/gateway-spi/src/test/java/org/apache/knox/gateway/dispatch/ConfigurableDispatchTest.java
new file mode 100644
index 0000000..3e6d2af
--- /dev/null
+++ b/gateway-spi/src/test/java/org/apache/knox/gateway/dispatch/ConfigurableDispatchTest.java
@@ -0,0 +1,258 @@
+/*
+ * 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.HttpHeaders;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.message.BasicHeader;
+import org.apache.knox.test.TestUtils;
+import org.apache.knox.test.mock.MockHttpServletResponse;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.apache.knox.gateway.dispatch.DefaultDispatch.SET_COOKIE;
+import static org.apache.knox.gateway.dispatch.DefaultDispatch.WWW_AUTHENTICATE;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+public class ConfigurableDispatchTest {
+  @Test( timeout = TestUtils.SHORT_TIMEOUT )
+  public void testGetDispatchUrl() {
+    HttpServletRequest request;
+    String path;
+    String query;
+    URI uri;
+
+    ConfigurableDispatch dispatch = new ConfigurableDispatch();
+
+    path = "http://test-host:42/test-path";
+    request = EasyMock.createNiceMock( HttpServletRequest.class );
+    EasyMock.expect( request.getRequestURI() ).andReturn( path ).anyTimes();
+    EasyMock.expect( request.getRequestURL() ).andReturn( new StringBuffer( path ) ).anyTimes();
+    EasyMock.expect( request.getQueryString() ).andReturn( null ).anyTimes();
+    EasyMock.replay( request );
+    uri = dispatch.getDispatchUrl( request );
+    assertThat( uri.toASCIIString(), is( "http://test-host:42/test-path" ) );
+
+    path = "http://test-host:42/test,path";
+    request = EasyMock.createNiceMock( HttpServletRequest.class );
+    EasyMock.expect( request.getRequestURI() ).andReturn( path ).anyTimes();
+    EasyMock.expect( request.getRequestURL() ).andReturn( new StringBuffer( path ) ).anyTimes();
+    EasyMock.expect( request.getQueryString() ).andReturn( null ).anyTimes();
+    EasyMock.replay( request );
+    uri = dispatch.getDispatchUrl( request );
+    assertThat( uri.toASCIIString(), is( "http://test-host:42/test,path" ) );
+
+    path = "http://test-host:42/test%2Cpath";
+    request = EasyMock.createNiceMock( HttpServletRequest.class );
+    EasyMock.expect( request.getRequestURI() ).andReturn( path ).anyTimes();
+    EasyMock.expect( request.getRequestURL() ).andReturn( new StringBuffer( path ) ).anyTimes();
+    EasyMock.expect( request.getQueryString() ).andReturn( null ).anyTimes();
+    EasyMock.replay( request );
+    uri = dispatch.getDispatchUrl( request );
+    assertThat( uri.toASCIIString(), is( "http://test-host:42/test%2Cpath" ) );
+
+    path = "http://test-host:42/test%2Cpath";
+    query = "test%26name=test%3Dvalue";
+    request = EasyMock.createNiceMock( HttpServletRequest.class );
+    EasyMock.expect( request.getRequestURI() ).andReturn( path ).anyTimes();
+    EasyMock.expect( request.getRequestURL() ).andReturn( new StringBuffer( path ) ).anyTimes();
+    EasyMock.expect( request.getQueryString() ).andReturn( query ).anyTimes();
+    EasyMock.replay( request );
+    uri = dispatch.getDispatchUrl( request );
+    assertThat( uri.toASCIIString(), is( "http://test-host:42/test%2Cpath?test%26name=test%3Dvalue" ) );
+  }
+
+  @Test( timeout = TestUtils.SHORT_TIMEOUT )
+  public void testGetDispatchUrlNoUrlEncoding() {
+    HttpServletRequest request;
+    String path;
+    String query;
+    URI uri;
+
+    ConfigurableDispatch dispatch = new ConfigurableDispatch();
+    dispatch.setRemoveUrlEncoding(Boolean.TRUE.toString());
+
+    path = "http://test-host:42/test-path";
+    request = EasyMock.createNiceMock( HttpServletRequest.class );
+    EasyMock.expect( request.getRequestURI() ).andReturn( path ).anyTimes();
+    EasyMock.expect( request.getRequestURL() ).andReturn( new StringBuffer( path ) ).anyTimes();
+    EasyMock.expect( request.getQueryString() ).andReturn( null ).anyTimes();
+    EasyMock.replay( request );
+    uri = dispatch.getDispatchUrl( request );
+    assertThat( uri.toASCIIString(), is( "http://test-host:42/test-path" ) );
+
+    path = "http://test-host:42/test,path";
+    request = EasyMock.createNiceMock( HttpServletRequest.class );
+    EasyMock.expect( request.getRequestURI() ).andReturn( path ).anyTimes();
+    EasyMock.expect( request.getRequestURL() ).andReturn( new StringBuffer( path ) ).anyTimes();
+    EasyMock.expect( request.getQueryString() ).andReturn( null ).anyTimes();
+    EasyMock.replay( request );
+    uri = dispatch.getDispatchUrl( request );
+    assertThat( uri.toASCIIString(), is( "http://test-host:42/test,path" ) );
+
+    // encoding in the patch remains
+    path = "http://test-host:42/test%2Cpath";
+    request = EasyMock.createNiceMock( HttpServletRequest.class );
+    EasyMock.expect( request.getRequestURI() ).andReturn( path ).anyTimes();
+    EasyMock.expect( request.getRequestURL() ).andReturn( new StringBuffer( path ) ).anyTimes();
+    EasyMock.expect( request.getQueryString() ).andReturn( null ).anyTimes();
+    EasyMock.replay( request );
+    uri = dispatch.getDispatchUrl( request );
+    assertThat( uri.toASCIIString(), is( "http://test-host:42/test%2Cpath" ) );
+
+    // encoding in query string is removed
+    path = "http://test-host:42/test%2Cpath";
+    query = "test%26name=test%3Dvalue";
+    request = EasyMock.createNiceMock( HttpServletRequest.class );
+    EasyMock.expect( request.getRequestURI() ).andReturn( path ).anyTimes();
+    EasyMock.expect( request.getRequestURL() ).andReturn( new StringBuffer( path ) ).anyTimes();
+    EasyMock.expect( request.getQueryString() ).andReturn( query ).anyTimes();
+    EasyMock.replay( request );
+    uri = dispatch.getDispatchUrl( request );
+    assertThat( uri.toASCIIString(), is( "http://test-host:42/test%2Cpath?test&name=test=value" ) );
+
+    // double quotes removed
+    path = "https://test-host:42/api/v1/views/TEZ/versions/0.7.0.2.6.2.0-205/instances/TEZ_CLUSTER_INSTANCE/resources/atsproxy/ws/v1/timeline/TEZ_DAG_ID";
+    query = "limit=9007199254740991&primaryFilter=applicationId:%22application_1518808140659_0007%22&_=1519053586839";
+    request = EasyMock.createNiceMock( HttpServletRequest.class );
+    EasyMock.expect( request.getRequestURI() ).andReturn( path ).anyTimes();
+    EasyMock.expect( request.getRequestURL() ).andReturn( new StringBuffer( path ) ).anyTimes();
+    EasyMock.expect( request.getQueryString() ).andReturn( query ).anyTimes();
+    EasyMock.replay( request );
+    uri = dispatch.getDispatchUrl( request );
+    assertThat( uri.toASCIIString(), is( "https://test-host:42/api/v1/views/TEZ/versions/0.7.0.2.6.2.0-205/instances/TEZ_CLUSTER_INSTANCE/resources/atsproxy/ws/v1/timeline/TEZ_DAG_ID?limit=9007199254740991&primaryFilter=applicationId:%22application_1518808140659_0007%22&_=1519053586839" ) );
+
+    // encode < and > sign
+    path = "http://test-host:8080/api/v1/clusters/mmolnar-knox2/configurations/service_config_versions";
+    query = "group_id%3E0&fields=*&_=1541527314780";
+    request = EasyMock.createNiceMock( HttpServletRequest.class );
+    EasyMock.expect( request.getRequestURI() ).andReturn( path ).anyTimes();
+    EasyMock.expect( request.getRequestURL() ).andReturn( new StringBuffer( path ) ).anyTimes();
+    EasyMock.expect( request.getQueryString() ).andReturn( query ).anyTimes();
+    EasyMock.replay( request );
+    uri = dispatch.getDispatchUrl( request );
+    assertThat( uri.toASCIIString(), is( "http://test-host:8080/api/v1/clusters/mmolnar-knox2/configurations/service_config_versions?group_id%3E0&fields=*&_=1541527314780" ) );
+  }
+
+  @Test( timeout = TestUtils.SHORT_TIMEOUT )
+  public void testRequestExcludeHeadersDefault() {
+    ConfigurableDispatch dispatch = new ConfigurableDispatch();
+
+    Map<String, String> headers = new HashMap<>();
+    headers.put(HttpHeaders.AUTHORIZATION, "Basic ...");
+    headers.put(HttpHeaders.ACCEPT, "abc");
+    headers.put("TEST", "test");
+
+    HttpServletRequest inboundRequest = EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(inboundRequest.getHeaderNames()).andReturn(Collections.enumeration(headers.keySet())).anyTimes();
+    Capture<String> capturedArgument = Capture.newInstance();
+    EasyMock.expect(inboundRequest.getHeader(EasyMock.capture(capturedArgument)))
+        .andAnswer(() -> headers.get(capturedArgument.getValue())).anyTimes();
+    EasyMock.replay(inboundRequest);
+
+    HttpUriRequest outboundRequest = new HttpGet();
+    dispatch.copyRequestHeaderFields(outboundRequest, inboundRequest);
+
+    Header[] outboundRequestHeaders = outboundRequest.getAllHeaders();
+    assertThat(outboundRequestHeaders.length, is(2));
+    assertThat(outboundRequestHeaders[0].getName(), is(HttpHeaders.ACCEPT));
+    assertThat(outboundRequestHeaders[1].getName(), is("TEST"));
+  }
+
+  @Test( timeout = TestUtils.SHORT_TIMEOUT )
+  public void testRequestExcludeHeadersConfig() {
+    ConfigurableDispatch dispatch = new ConfigurableDispatch();
+    dispatch.setRequestExcludeHeaders(String.join(",", Arrays.asList(HttpHeaders.ACCEPT, "TEST")));
+
+    Map<String, String> headers = new HashMap<>();
+    headers.put(HttpHeaders.AUTHORIZATION, "Basic ...");
+    headers.put(HttpHeaders.ACCEPT, "abc");
+    headers.put("TEST", "test");
+
+    HttpServletRequest inboundRequest = EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(inboundRequest.getHeaderNames()).andReturn(Collections.enumeration(headers.keySet())).anyTimes();
+    Capture<String> capturedArgument = Capture.newInstance();
+    EasyMock.expect(inboundRequest.getHeader(EasyMock.capture(capturedArgument)))
+        .andAnswer(() -> headers.get(capturedArgument.getValue())).anyTimes();
+    EasyMock.replay(inboundRequest);
+
+    HttpUriRequest outboundRequest = new HttpGet();
+    dispatch.copyRequestHeaderFields(outboundRequest, inboundRequest);
+
+    Header[] outboundRequestHeaders = outboundRequest.getAllHeaders();
+    assertThat(outboundRequestHeaders.length, is(1));
+    assertThat(outboundRequestHeaders[0].getName(), is(HttpHeaders.AUTHORIZATION));
+  }
+
+  @Test( timeout = TestUtils.SHORT_TIMEOUT )
+  public void testResponseExcludeHeadersDefault() {
+    ConfigurableDispatch dispatch = new ConfigurableDispatch();
+
+    Header[] headers = new Header[]{
+        new BasicHeader(SET_COOKIE, "abc"),
+        new BasicHeader(WWW_AUTHENTICATE, "negotiate"),
+        new BasicHeader("TEST", "testValue")
+    };
+
+    HttpResponse inboundResponse = EasyMock.createNiceMock(HttpResponse.class);
+    EasyMock.expect(inboundResponse.getAllHeaders()).andReturn(headers).anyTimes();
+    EasyMock.replay(inboundResponse);
+
+    HttpServletResponse outboundResponse = new MockHttpServletResponse();
+    dispatch.copyResponseHeaderFields(outboundResponse, inboundResponse);
+
+    assertThat(outboundResponse.getHeaderNames().size(), is(1));
+    assertThat(outboundResponse.getHeader("TEST"), is("testValue"));
+  }
+
+  @Test( timeout = TestUtils.SHORT_TIMEOUT )
+  public void testResponseExcludeHeadersConfig() {
+    ConfigurableDispatch dispatch = new ConfigurableDispatch();
+    dispatch.setResponseExcludeHeaders(String.join(",", Collections.singletonList("TEST")));
+
+    Header[] headers = new Header[]{
+        new BasicHeader(SET_COOKIE, "abc"),
+        new BasicHeader(WWW_AUTHENTICATE, "negotiate"),
+        new BasicHeader("TEST", "testValue")
+    };
+
+    HttpResponse inboundResponse = EasyMock.createNiceMock(HttpResponse.class);
+    EasyMock.expect(inboundResponse.getAllHeaders()).andReturn(headers).anyTimes();
+    EasyMock.replay(inboundResponse);
+
+    HttpServletResponse outboundResponse = new MockHttpServletResponse();
+    dispatch.copyResponseHeaderFields(outboundResponse, inboundResponse);
+
+    assertThat(outboundResponse.getHeaderNames().size(), is(2));
+    assertThat(outboundResponse.getHeader(SET_COOKIE), is("abc"));
+    assertThat(outboundResponse.getHeader(WWW_AUTHENTICATE), is("negotiate"));
+  }
+}