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/10/03 15:15:15 UTC

[knox] branch master updated: KNOX-2015 - Allow end-users to exclude only certain directives of the SET-COOKIE HTTP header (#154)

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 b98e77e  KNOX-2015 - Allow end-users to exclude only certain directives of the SET-COOKIE HTTP header (#154)
b98e77e is described below

commit b98e77ee906dc9c6326b7d8703870faf523694ae
Author: Sandor Molnar <sm...@apache.org>
AuthorDate: Thu Oct 3 17:15:10 2019 +0200

    KNOX-2015 - Allow end-users to exclude only certain directives of the SET-COOKIE HTTP header (#154)
---
 .../apache/knox/gateway/storm/StormDispatch.java   |  3 +-
 .../gateway/dispatch/ConfigurableDispatch.java     | 47 +++++++++++++++-----
 .../knox/gateway/dispatch/DefaultDispatch.java     | 51 ++++++++++++++++------
 .../gateway/dispatch/ConfigurableDispatchTest.java | 29 ++++++++++--
 4 files changed, 102 insertions(+), 28 deletions(-)

diff --git a/gateway-service-storm/src/main/java/org/apache/knox/gateway/storm/StormDispatch.java b/gateway-service-storm/src/main/java/org/apache/knox/gateway/storm/StormDispatch.java
index e09f6e2..1eb2fbb 100644
--- a/gateway-service-storm/src/main/java/org/apache/knox/gateway/storm/StormDispatch.java
+++ b/gateway-service-storm/src/main/java/org/apache/knox/gateway/storm/StormDispatch.java
@@ -19,6 +19,7 @@ package org.apache.knox.gateway.storm;
 
 import org.apache.knox.gateway.dispatch.DefaultDispatch;
 
+import java.util.Collections;
 import java.util.Set;
 
 /**
@@ -29,7 +30,7 @@ public class StormDispatch extends DefaultDispatch {
 
   @Override
   public Set<String> getOutboundResponseExcludeHeaders() {
-    return null;
+    return Collections.emptySet();
   }
 }
 
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
index 1611307..09c02da 100644
--- 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
@@ -27,7 +27,9 @@ import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.Optional;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * Extends DefaultDispatch to:
@@ -37,29 +39,49 @@ import java.util.Set;
 public class ConfigurableDispatch extends DefaultDispatch {
   private Set<String> requestExcludeHeaders = super.getOutboundRequestExcludeHeaders();
   private Set<String> responseExcludeHeaders = super.getOutboundResponseExcludeHeaders();
+  private Set<String> responseExcludeSetCookieHeaderDirectives = super.getOutboundResponseExcludedSetCookieHeaderDirectives();
   private Boolean removeUrlEncoding = false;
 
-  private Set<String> handleCommaSeparatedHeaders(String headers) {
-    if(headers != null) {
-      return new HashSet<>(Arrays.asList(headers.split(",")));
-    }
-    return Collections.emptySet();
+  private Set<String> convertCommaDelimitedHeadersToSet(String headers) {
+    return headers == null ?  Collections.emptySet(): new HashSet<>(Arrays.asList(headers.split(",")));
   }
 
   @Configure
   protected void setRequestExcludeHeaders(@Default(" ") String headers) {
     if(!" ".equals(headers)) {
-      this.requestExcludeHeaders = handleCommaSeparatedHeaders(headers);
+      this.requestExcludeHeaders = convertCommaDelimitedHeadersToSet(headers);
     }
   }
 
   @Configure
   protected void setResponseExcludeHeaders(@Default(" ") String headers) {
-    if(!" ".equals(headers)) {
-      this.responseExcludeHeaders = handleCommaSeparatedHeaders(headers);
+    if (!" ".equals(headers)) {
+      final Set<String> headerSet = convertCommaDelimitedHeadersToSet(headers);
+      populateSetCookieHeaderDirectiveExlusions(headerSet);
+      populateHttpHeaderExlusionsOtherThanSetCookie(headerSet);
+    }
+  }
+
+  private void populateSetCookieHeaderDirectiveExlusions(final Set<String> headerSet) {
+    final Optional<String> setCookieHeader = headerSet.stream().filter(s -> s.startsWith(SET_COOKIE)).findFirst();
+    if (setCookieHeader.isPresent()) {
+      final String[] setCookieHeaderParts = setCookieHeader.get().split(":");
+      responseExcludeSetCookieHeaderDirectives = setCookieHeaderParts.length > 1
+          ? new HashSet<>(Arrays.asList(setCookieHeaderParts[1].split(";"))).stream().map(e -> e.trim()).collect(Collectors.toSet())
+              : Collections.emptySet();
+    } else {
+      responseExcludeSetCookieHeaderDirectives = Collections.emptySet();
+    }
+  }
+
+  private void populateHttpHeaderExlusionsOtherThanSetCookie(final Set<String> headerSet) {
+    final Set<String> excludedHeadersOthenThanSetCookie = headerSet.stream().filter(s -> !s.startsWith(SET_COOKIE)).collect(Collectors.toSet());
+    if (!excludedHeadersOthenThanSetCookie.isEmpty()) {
+      this.responseExcludeHeaders = excludedHeadersOthenThanSetCookie;
     }
   }
 
+
   @Configure
   protected void setRemoveUrlEncoding(@Default("false") String removeUrlEncoding) {
     this.removeUrlEncoding = Boolean.parseBoolean(removeUrlEncoding);
@@ -67,12 +89,17 @@ public class ConfigurableDispatch extends DefaultDispatch {
 
   @Override
   public Set<String> getOutboundResponseExcludeHeaders() {
-    return responseExcludeHeaders;
+    return responseExcludeHeaders == null ? Collections.emptySet() : responseExcludeHeaders;
+  }
+
+  @Override
+  public Set<String> getOutboundResponseExcludedSetCookieHeaderDirectives() {
+    return responseExcludeSetCookieHeaderDirectives == null ? Collections.emptySet() : responseExcludeSetCookieHeaderDirectives;
   }
 
   @Override
   public Set<String> getOutboundRequestExcludeHeaders() {
-    return requestExcludeHeaders;
+    return requestExcludeHeaders == null ? Collections.emptySet() : requestExcludeHeaders;
   }
 
   public boolean getRemoveUrlEncoding() {
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 77d38a1..0f5d1f3 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
@@ -52,9 +52,12 @@ import java.io.InputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 public class DefaultDispatch extends AbstractGatewayDispatch {
 
@@ -66,7 +69,9 @@ 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 = new HashSet<>(Arrays.asList(SET_COOKIE, WWW_AUTHENTICATE));
+  protected static final String EXCLUDE_ALL = "*";
+  private Set<String> outboundResponseExcludeHeaders = new HashSet<>(Arrays.asList(WWW_AUTHENTICATE));
+  private Set<String> outboundResponseExcludedSetCookieHeaderDirectives = new HashSet<>(Arrays.asList(EXCLUDE_ALL));
 
   //Buffer size in bytes
   private int replayBufferSize = -1;
@@ -310,23 +315,43 @@ public class DefaultDispatch extends AbstractGatewayDispatch {
   }
 
   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))) {
+    final Map<String, Set<String>> excludedHeaderDirectives = getOutboundResponseExcludeHeaders().stream().collect(Collectors.toMap(k -> k, v -> new HashSet<>(Arrays.asList(EXCLUDE_ALL))));
+    excludedHeaderDirectives.put(SET_COOKIE, getOutboundResponseExcludedSetCookieHeaderDirectives());
+
+    for ( Header header : inboundResponse.getAllHeaders() ) {
+      final String responseHeaderValue = calculateResponseHeaderValue(header, excludedHeaderDirectives);
+      if (responseHeaderValue.isEmpty()) {
         continue;
       }
-      String value = header.getValue();
-      outboundResponse.addHeader(name, value);
+      outboundResponse.addHeader(header.getName(), responseHeaderValue);
     }
   }
 
+  private String calculateResponseHeaderValue(Header headerToCheck, Map<String, Set<String>> excludedHeaderDirectives) {
+    final String headerNameToCheck = headerToCheck.getName();
+    if (excludedHeaderDirectives != null && excludedHeaderDirectives.containsKey(headerNameToCheck.toUpperCase(Locale.ROOT))) {
+      final Set<String> excludedHeaderValues = excludedHeaderDirectives.get(headerNameToCheck.toUpperCase(Locale.ROOT));
+      if (!excludedHeaderValues.isEmpty()) {
+        if (excludedHeaderValues.stream().filter(e -> e.equals(EXCLUDE_ALL)).findAny().isPresent()) {
+          return ""; // we should exclude all -> there should not be any value added with this header
+        } else {
+          final String separator = SET_COOKIE.equalsIgnoreCase(headerNameToCheck) ? "; " : " ";
+          Set<String> headerValuesToCheck = new HashSet<>(Arrays.asList(headerToCheck.getValue().trim().split("\\s+")));
+          headerValuesToCheck = headerValuesToCheck.stream().map(h -> h.replaceAll(separator.trim(), "")).collect(Collectors.toSet());
+          headerValuesToCheck.removeAll(excludedHeaderValues);
+          return headerValuesToCheck.isEmpty() ? "" : headerValuesToCheck.stream().collect(Collectors.joining(separator));
+        }
+      }
+    }
+
+    return headerToCheck.getValue();
+  }
+
   public Set<String> getOutboundResponseExcludeHeaders() {
-    return outboundResponseExcludeHeaders;
+    return outboundResponseExcludeHeaders == null ? Collections.emptySet() : outboundResponseExcludeHeaders;
+  }
+
+  public Set<String> getOutboundResponseExcludedSetCookieHeaderDirectives() {
+    return outboundResponseExcludedSetCookieHeaderDirectives == null ? Collections.emptySet() : outboundResponseExcludedSetCookieHeaderDirectives;
   }
 }
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
index 3e6d2af..07b98a8 100644
--- 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
@@ -212,14 +212,15 @@ public class ConfigurableDispatchTest {
     assertThat(outboundRequestHeaders[0].getName(), is(HttpHeaders.AUTHORIZATION));
   }
 
-  @Test( timeout = TestUtils.SHORT_TIMEOUT )
+  @Test
   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")
+        new BasicHeader("TEST", "testValue"),
+        new BasicHeader("Accept", "applcation/json")
     };
 
     HttpResponse inboundResponse = EasyMock.createNiceMock(HttpResponse.class);
@@ -229,11 +230,12 @@ public class ConfigurableDispatchTest {
     HttpServletResponse outboundResponse = new MockHttpServletResponse();
     dispatch.copyResponseHeaderFields(outboundResponse, inboundResponse);
 
-    assertThat(outboundResponse.getHeaderNames().size(), is(1));
+    assertThat(outboundResponse.getHeaderNames().size(), is(2));
     assertThat(outboundResponse.getHeader("TEST"), is("testValue"));
+    assertThat(outboundResponse.getHeader("Accept"), is("applcation/json"));
   }
 
-  @Test( timeout = TestUtils.SHORT_TIMEOUT )
+  @Test
   public void testResponseExcludeHeadersConfig() {
     ConfigurableDispatch dispatch = new ConfigurableDispatch();
     dispatch.setResponseExcludeHeaders(String.join(",", Collections.singletonList("TEST")));
@@ -255,4 +257,23 @@ public class ConfigurableDispatchTest {
     assertThat(outboundResponse.getHeader(SET_COOKIE), is("abc"));
     assertThat(outboundResponse.getHeader(WWW_AUTHENTICATE), is("negotiate"));
   }
+
+  @Test
+  public void shouldExcludeCertainPartsFromSetCookieHeader() throws Exception {
+    final Header[] headers = new Header[] { new BasicHeader(SET_COOKIE, "Domain=localhost; Secure; HttpOnly; Max-Age=1") };
+    final HttpResponse inboundResponse = EasyMock.createNiceMock(HttpResponse.class);
+    EasyMock.expect(inboundResponse.getAllHeaders()).andReturn(headers).anyTimes();
+    EasyMock.replay(inboundResponse);
+
+    final ConfigurableDispatch dispatch = new ConfigurableDispatch();
+    final String setCookieExludeHeaders = SET_COOKIE + ": Domain=localhost; HttpOnly";
+    dispatch.setResponseExcludeHeaders(setCookieExludeHeaders);
+
+    final HttpServletResponse outboundResponse = new MockHttpServletResponse();
+    dispatch.copyResponseHeaderFields(outboundResponse, inboundResponse);
+
+    assertThat(outboundResponse.getHeaderNames().size(), is(1));
+    assertThat(outboundResponse.getHeader(SET_COOKIE), is("Secure; Max-Age=1"));
+  }
+
 }