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

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


The following commit(s) were added to refs/heads/master by this push:
     new a503683a4a Add caching and CSP response headers. (#12609)
a503683a4a is described below

commit a503683a4a98754dbbe06bb4ba0a2d88a10e7b3e
Author: Gian Merlino <[email protected]>
AuthorDate: Sat Jun 4 09:16:49 2022 -0700

    Add caching and CSP response headers. (#12609)
    
    * Add caching and CSP response headers.
    
    * Fix tests.
    
    * Fix checkstyle issues
    
    Co-authored-by: Abhishek Agarwal 
<[email protected]>
---
 docs/configuration/index.md                        |   3 +
 .../server/AsyncManagementForwardingServlet.java   |  16 +-
 .../druid/server/initialization/ServerConfig.java  |  19 ++-
 .../jetty/CliIndexerServerModule.java              |   3 +-
 .../initialization/jetty/JettyServerModule.java    |   7 +-
 .../jetty/StandardResponseHeaderFilterHolder.java  | 170 +++++++++++++++++++++
 .../druid/initialization/ServerConfigTest.java     |   3 +-
 .../StandardResponseHeaderFilterHolderTest.java    | 168 ++++++++++++++++++++
 .../druid/server/AsyncQueryForwardingServlet.java  |  12 ++
 9 files changed, 390 insertions(+), 11 deletions(-)

diff --git a/docs/configuration/index.md b/docs/configuration/index.md
index 5c794930d5..ddf9bb2941 100644
--- a/docs/configuration/index.md
+++ b/docs/configuration/index.md
@@ -1524,6 +1524,7 @@ Druid uses Jetty to serve HTTP requests.
 |`druid.server.http.maxRequestHeaderSize`|Maximum size of a request header in 
bytes. Larger headers consume more memory and can make a server more vulnerable 
to denial of service attacks.|8 * 1024|
 |`druid.server.http.enableForwardedRequestCustomizer`|If enabled, adds Jetty 
ForwardedRequestCustomizer which reads X-Forwarded-* request headers to 
manipulate servlet request object when Druid is used behind a proxy.|false|
 |`druid.server.http.allowedHttpMethods`|List of HTTP methods that should be 
allowed in addition to the ones required by Druid APIs. Druid APIs require GET, 
PUT, POST, and DELETE, which are always allowed. This option is not useful 
unless you have installed an extension that needs these additional HTTP methods 
or that adds functionality related to CORS. None of Druid's bundled extensions 
require these methods.|[]|
+|`druid.server.http.contentSecurityPolicy`|Content-Security-Policy header 
value to set on each non-POST response. Setting this property to an empty 
string, or omitting it, both result in the default `frame-ancestors: none` 
being set.|`frame-ancestors 'none'`|
 
 #### Indexer Processing Resources
 
@@ -1633,6 +1634,7 @@ Druid uses Jetty to serve HTTP requests.
 |`druid.server.http.unannouncePropagationDelay`|How long to wait for ZooKeeper 
unannouncements to propagate before shutting down Jetty. This is a minimum and 
`druid.server.http.gracefulShutdownTimeout` does not start counting down until 
after this period elapses.|`PT0S` (do not wait)|
 |`druid.server.http.maxQueryTimeout`|Maximum allowed value (in milliseconds) 
for `timeout` parameter. See [query-context](../querying/query-context.md) to 
know more about `timeout`. Query is rejected if the query context `timeout` is 
greater than this value. |Long.MAX_VALUE|
 |`druid.server.http.maxRequestHeaderSize`|Maximum size of a request header in 
bytes. Larger headers consume more memory and can make a server more vulnerable 
to denial of service attacks.|8 * 1024|
+|`druid.server.http.contentSecurityPolicy`|Content-Security-Policy header 
value to set on each non-POST response. Setting this property to an empty 
string, or omitting it, both result in the default `frame-ancestors: none` 
being set.|`frame-ancestors 'none'`|
 
 ##### Processing
 
@@ -1772,6 +1774,7 @@ Druid uses Jetty to serve HTTP requests. Each query being 
processed consumes a s
 |`druid.server.http.unannouncePropagationDelay`|How long to wait for ZooKeeper 
unannouncements to propagate before shutting down Jetty. This is a minimum and 
`druid.server.http.gracefulShutdownTimeout` does not start counting down until 
after this period elapses.|`PT0S` (do not wait)|
 |`druid.server.http.maxQueryTimeout`|Maximum allowed value (in milliseconds) 
for `timeout` parameter. See [query-context](../querying/query-context.md) to 
know more about `timeout`. Query is rejected if the query context `timeout` is 
greater than this value. |Long.MAX_VALUE|
 |`druid.server.http.maxRequestHeaderSize`|Maximum size of a request header in 
bytes. Larger headers consume more memory and can make a server more vulnerable 
to denial of service attacks. |8 * 1024|
+|`druid.server.http.contentSecurityPolicy`|Content-Security-Policy header 
value to set on each non-POST response. Setting this property to an empty 
string, or omitting it, both result in the default `frame-ancestors: none` 
being set.|`frame-ancestors 'none'`|
 
 ##### Client Configuration
 
diff --git 
a/server/src/main/java/org/apache/druid/server/AsyncManagementForwardingServlet.java
 
b/server/src/main/java/org/apache/druid/server/AsyncManagementForwardingServlet.java
index 63d8472cd8..65109c9f51 100644
--- 
a/server/src/main/java/org/apache/druid/server/AsyncManagementForwardingServlet.java
+++ 
b/server/src/main/java/org/apache/druid/server/AsyncManagementForwardingServlet.java
@@ -30,10 +30,11 @@ import org.apache.druid.guice.annotations.Global;
 import org.apache.druid.guice.annotations.Json;
 import org.apache.druid.guice.http.DruidHttpClientConfig;
 import org.apache.druid.java.util.common.StringUtils;
-import org.apache.druid.java.util.emitter.EmittingLogger;
+import 
org.apache.druid.server.initialization.jetty.StandardResponseHeaderFilterHolder;
 import org.apache.druid.server.security.AuthConfig;
 import org.eclipse.jetty.client.HttpClient;
 import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
 import org.eclipse.jetty.proxy.AsyncProxyServlet;
 
 import javax.servlet.ServletException;
@@ -44,8 +45,6 @@ import java.util.concurrent.TimeUnit;
 
 public class AsyncManagementForwardingServlet extends AsyncProxyServlet
 {
-  private static final EmittingLogger log = new 
EmittingLogger(AsyncManagementForwardingServlet.class);
-
   private static final String BASE_URI_ATTRIBUTE = 
"org.apache.druid.proxy.to.base.uri";
   private static final String MODIFIED_PATH_ATTRIBUTE = 
"org.apache.druid.proxy.to.path";
 
@@ -169,6 +168,17 @@ public class AsyncManagementForwardingServlet extends 
AsyncProxyServlet
     return client;
   }
 
+  @Override
+  protected void onServerResponseHeaders(
+      HttpServletRequest clientRequest,
+      HttpServletResponse proxyResponse,
+      Response serverResponse
+  )
+  {
+    
StandardResponseHeaderFilterHolder.deduplicateHeadersInProxyServlet(proxyResponse,
 serverResponse);
+    super.onServerResponseHeaders(clientRequest, proxyResponse, 
serverResponse);
+  }
+
   private void handleBadRequest(HttpServletResponse response, String 
errorMessage) throws IOException
   {
     if (!response.isCommitted()) {
diff --git 
a/server/src/main/java/org/apache/druid/server/initialization/ServerConfig.java 
b/server/src/main/java/org/apache/druid/server/initialization/ServerConfig.java
index ff2196e119..3c031e3783 100644
--- 
a/server/src/main/java/org/apache/druid/server/initialization/ServerConfig.java
+++ 
b/server/src/main/java/org/apache/druid/server/initialization/ServerConfig.java
@@ -28,6 +28,7 @@ import 
org.apache.druid.java.util.common.HumanReadableBytesRange;
 import org.apache.druid.utils.JvmUtils;
 import org.joda.time.Period;
 
+import javax.annotation.Nullable;
 import javax.validation.constraints.Max;
 import javax.validation.constraints.Min;
 import javax.validation.constraints.NotNull;
@@ -65,7 +66,8 @@ public class ServerConfig
       boolean enableForwardedRequestCustomizer,
       @NotNull List<String> allowedHttpMethods,
       boolean showDetailedJettyErrors,
-      @NotNull ErrorResponseTransformStrategy errorResponseTransformStrategy
+      @NotNull ErrorResponseTransformStrategy errorResponseTransformStrategy,
+      @Nullable String contentSecurityPolicy
   )
   {
     this.numThreads = numThreads;
@@ -155,6 +157,9 @@ public class ServerConfig
   @NotNull
   private ErrorResponseTransformStrategy errorResponseTransformStrategy = 
NoErrorResponseTransformStrategy.INSTANCE;
 
+  @JsonProperty("contentSecurityPolicy")
+  private String contentSecurityPolicy;
+
   @JsonProperty
   private boolean showDetailedJettyErrors = true;
 
@@ -244,6 +249,11 @@ public class ServerConfig
     return allowedHttpMethods;
   }
 
+  public String getContentSecurityPolicy()
+  {
+    return contentSecurityPolicy;
+  }
+
   @Override
   public boolean equals(Object o)
   {
@@ -270,7 +280,8 @@ public class ServerConfig
            gracefulShutdownTimeout.equals(that.gracefulShutdownTimeout) &&
            unannouncePropagationDelay.equals(that.unannouncePropagationDelay) 
&&
            allowedHttpMethods.equals(that.allowedHttpMethods) &&
-           
errorResponseTransformStrategy.equals(that.errorResponseTransformStrategy);
+           
errorResponseTransformStrategy.equals(that.errorResponseTransformStrategy) &&
+           Objects.equals(contentSecurityPolicy, 
that.getContentSecurityPolicy());
   }
 
   @Override
@@ -293,7 +304,8 @@ public class ServerConfig
         enableForwardedRequestCustomizer,
         allowedHttpMethods,
         errorResponseTransformStrategy,
-        showDetailedJettyErrors
+        showDetailedJettyErrors,
+        contentSecurityPolicy
     );
   }
 
@@ -318,6 +330,7 @@ public class ServerConfig
            ", allowedHttpMethods=" + allowedHttpMethods +
            ", errorResponseTransformStrategy=" + 
errorResponseTransformStrategy +
            ", showDetailedJettyErrors=" + showDetailedJettyErrors +
+           ", contentSecurityPolicy=" + contentSecurityPolicy +
            '}';
   }
 
diff --git 
a/server/src/main/java/org/apache/druid/server/initialization/jetty/CliIndexerServerModule.java
 
b/server/src/main/java/org/apache/druid/server/initialization/jetty/CliIndexerServerModule.java
index 06561b6d86..ee60b8e59b 100644
--- 
a/server/src/main/java/org/apache/druid/server/initialization/jetty/CliIndexerServerModule.java
+++ 
b/server/src/main/java/org/apache/druid/server/initialization/jetty/CliIndexerServerModule.java
@@ -162,7 +162,8 @@ public class CliIndexerServerModule implements Module
         oldConfig.isEnableForwardedRequestCustomizer(),
         oldConfig.getAllowedHttpMethods(),
         oldConfig.isShowDetailedJettyErrors(),
-        oldConfig.getErrorResponseTransformStrategy()
+        oldConfig.getErrorResponseTransformStrategy(),
+        oldConfig.getContentSecurityPolicy()
     );
   }
 }
diff --git 
a/server/src/main/java/org/apache/druid/server/initialization/jetty/JettyServerModule.java
 
b/server/src/main/java/org/apache/druid/server/initialization/jetty/JettyServerModule.java
index b722fb2c32..b28f061cec 100644
--- 
a/server/src/main/java/org/apache/druid/server/initialization/jetty/JettyServerModule.java
+++ 
b/server/src/main/java/org/apache/druid/server/initialization/jetty/JettyServerModule.java
@@ -131,10 +131,11 @@ public class JettyServerModule extends JerseyServletModule
     Jerseys.addResource(binder, StatusResource.class);
     binder.bind(StatusResource.class).in(LazySingleton.class);
 
-    // Adding empty binding for ServletFilterHolders and Handlers so that 
injector returns an empty set if none
-    // are provided by extensions.
+    // Add empty binding for Handlers so that the injector returns an empty 
set if none are provided by extensions.
     Multibinder.newSetBinder(binder, Handler.class);
-    Multibinder.newSetBinder(binder, ServletFilterHolder.class);
+    Multibinder.newSetBinder(binder, ServletFilterHolder.class)
+               .addBinding()
+               .to(StandardResponseHeaderFilterHolder.class);
 
     MetricsModule.register(binder, JettyMonitor.class);
   }
diff --git 
a/server/src/main/java/org/apache/druid/server/initialization/jetty/StandardResponseHeaderFilterHolder.java
 
b/server/src/main/java/org/apache/druid/server/initialization/jetty/StandardResponseHeaderFilterHolder.java
new file mode 100644
index 0000000000..4f36beb05c
--- /dev/null
+++ 
b/server/src/main/java/org/apache/druid/server/initialization/jetty/StandardResponseHeaderFilterHolder.java
@@ -0,0 +1,170 @@
+/*
+ * 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.druid.server.initialization.jetty;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.Inject;
+import org.apache.commons.lang.CharUtils;
+import org.apache.druid.java.util.common.IAE;
+import org.apache.druid.server.initialization.ServerConfig;
+import org.eclipse.jetty.client.api.Response;
+
+import javax.annotation.Nullable;
+import javax.servlet.DispatcherType;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.HttpMethod;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Adds response headers that we want to have on all responses.
+ */
+public class StandardResponseHeaderFilterHolder implements ServletFilterHolder
+{
+  private static final Set<String> STANDARD_HEADERS = 
ImmutableSet.of("Cache-Control", "Content-Security-Policy");
+  private static final String DEFAULT_CONTENT_SECURITY_POLICY = 
"frame-ancestors 'none'";
+
+  private final String contentSecurityPolicy;
+
+  @Inject
+  public StandardResponseHeaderFilterHolder(final ServerConfig serverConfig)
+  {
+    this.contentSecurityPolicy = 
asContentSecurityPolicyHeaderValue(serverConfig.getContentSecurityPolicy());
+  }
+
+  /**
+   * Remove any standard headers in proxyResponse if they were also set in the 
origin response, serverResponse.
+   * This prevents duplicates headers from appearing in proxy responses.
+   *
+   * Used by implementations of {@link 
org.eclipse.jetty.proxy.AsyncProxyServlet}.
+   */
+  public static void deduplicateHeadersInProxyServlet(
+      final HttpServletResponse proxyResponse,
+      final Response serverResponse
+  )
+  {
+    for (final String headerName : 
StandardResponseHeaderFilterHolder.STANDARD_HEADERS) {
+      if (serverResponse.getHeaders().containsKey(headerName) && 
proxyResponse.containsHeader(headerName)) {
+        ((org.eclipse.jetty.server.Response) 
proxyResponse).getHttpFields().remove(headerName);
+      }
+    }
+  }
+
+  static String asContentSecurityPolicyHeaderValue(@Nullable final String 
contentSecurityPolicy)
+  {
+    if (contentSecurityPolicy == null || 
contentSecurityPolicy.trim().isEmpty()) {
+      return DEFAULT_CONTENT_SECURITY_POLICY;
+    } else {
+      // Header values must be ASCII or RFC 2047 encoded. We don't have an RFC 
2047 encoder handy, so require
+      // that the value be plain ASCII.
+      for (int i = 0; i < contentSecurityPolicy.length(); i++) {
+        if (!CharUtils.isAscii(contentSecurityPolicy.charAt(i))) {
+          throw new IAE("Content-Security-Policy header value must be fully 
ASCII");
+        }
+      }
+
+      return contentSecurityPolicy;
+    }
+  }
+
+  @Override
+  public Filter getFilter()
+  {
+    return new StandardResponseHeaderFilter(contentSecurityPolicy);
+  }
+
+  @Override
+  public Class<? extends Filter> getFilterClass()
+  {
+    return StandardResponseHeaderFilter.class;
+  }
+
+  @Override
+  public Map<String, String> getInitParameters()
+  {
+    return Collections.emptyMap();
+  }
+
+  @Override
+  public String getPath()
+  {
+    return "/*";
+  }
+
+  @Nullable
+  @Override
+  public EnumSet<DispatcherType> getDispatcherType()
+  {
+    return null;
+  }
+
+  static class StandardResponseHeaderFilter implements Filter
+  {
+    private final String contentSecurityPolicy;
+
+    public StandardResponseHeaderFilter(final String contentSecurityPolicy)
+    {
+      this.contentSecurityPolicy = contentSecurityPolicy;
+    }
+
+    @Override
+    public void init(FilterConfig filterConfig)
+    {
+      // Nothing to do.
+    }
+
+    @Override
+    public void doFilter(
+        ServletRequest request,
+        ServletResponse response,
+        FilterChain chain
+    ) throws IOException, ServletException
+    {
+      final HttpServletRequest httpRequest = (HttpServletRequest) request;
+      final HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+      if (!HttpMethod.POST.equals(httpRequest.getMethod())) {
+        // Disable client-side caching on non-POSTs. (POST requests are not 
typically cached.)
+        httpResponse.setHeader("Cache-Control", "no-cache, no-store, 
max-age=0");
+
+        // Set the desired Content-Security-Policy on non-POSTs. (It's for web 
pages, which we don't serve via POST.)
+        httpResponse.setHeader("Content-Security-Policy", 
contentSecurityPolicy);
+      }
+
+      chain.doFilter(request, response);
+    }
+
+    @Override
+    public void destroy()
+    {
+      // Nothing to do.
+    }
+  }
+}
diff --git 
a/server/src/test/java/org/apache/druid/initialization/ServerConfigTest.java 
b/server/src/test/java/org/apache/druid/initialization/ServerConfigTest.java
index e542ca8279..02aa4d2c1c 100644
--- a/server/src/test/java/org/apache/druid/initialization/ServerConfigTest.java
+++ b/server/src/test/java/org/apache/druid/initialization/ServerConfigTest.java
@@ -60,7 +60,8 @@ public class ServerConfigTest
         true,
         ImmutableList.of(HttpMethod.OPTIONS),
         true,
-        new AllowedRegexErrorResponseTransformStrategy(ImmutableList.of(".*"))
+        new AllowedRegexErrorResponseTransformStrategy(ImmutableList.of(".*")),
+        defaultConfig.getContentSecurityPolicy()
     );
     String modifiedConfigJson = 
OBJECT_MAPPER.writeValueAsString(modifiedConfig);
     ServerConfig modifiedConfig2 = OBJECT_MAPPER.readValue(modifiedConfigJson, 
ServerConfig.class);
diff --git 
a/server/src/test/java/org/apache/druid/server/initialization/jetty/StandardResponseHeaderFilterHolderTest.java
 
b/server/src/test/java/org/apache/druid/server/initialization/jetty/StandardResponseHeaderFilterHolderTest.java
new file mode 100644
index 0000000000..e85dc6a862
--- /dev/null
+++ 
b/server/src/test/java/org/apache/druid/server/initialization/jetty/StandardResponseHeaderFilterHolderTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.druid.server.initialization.jetty;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.druid.server.initialization.ServerConfig;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.internal.matchers.ThrowableMessageMatcher;
+
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.HttpMethod;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class StandardResponseHeaderFilterHolderTest
+{
+  public ServerConfig serverConfig;
+  public HttpServletRequest httpRequest;
+  public HttpServletResponse httpResponse;
+  public FilterChain filterChain;
+
+  @Before
+  public void setUp()
+  {
+    serverConfig = EasyMock.strictMock(ServerConfig.class);
+    httpRequest = EasyMock.strictMock(HttpServletRequest.class);
+    httpResponse = EasyMock.strictMock(HttpServletResponse.class);
+    filterChain = EasyMock.strictMock(FilterChain.class);
+  }
+
+  @After
+  public void tearDown()
+  {
+    EasyMock.verify(serverConfig, httpRequest, httpResponse, filterChain);
+  }
+
+  @Test
+  public void test_get_nullContentSecurityPolicy() throws Exception
+  {
+    
EasyMock.expect(serverConfig.getContentSecurityPolicy()).andReturn("").once();
+    
EasyMock.expect(httpRequest.getMethod()).andReturn(HttpMethod.GET).anyTimes();
+
+    runFilterAndVerifyHeaders(
+        ImmutableMap.<String, String>builder()
+                    .put("Cache-Control", "no-cache, no-store, max-age=0")
+                    .put("Content-Security-Policy", "frame-ancestors 'none'")
+                    .build()
+    );
+  }
+
+  @Test
+  public void test_post_nullContentSecurityPolicy() throws Exception
+  {
+    
EasyMock.expect(serverConfig.getContentSecurityPolicy()).andReturn("").once();
+    
EasyMock.expect(httpRequest.getMethod()).andReturn(HttpMethod.POST).anyTimes();
+
+    runFilterAndVerifyHeaders(Collections.emptyMap());
+  }
+
+  @Test
+  public void test_get_emptyContentSecurityPolicy() throws Exception
+  {
+    
EasyMock.expect(serverConfig.getContentSecurityPolicy()).andReturn("").once();
+    
EasyMock.expect(httpRequest.getMethod()).andReturn(HttpMethod.GET).anyTimes();
+
+    runFilterAndVerifyHeaders(
+        ImmutableMap.<String, String>builder()
+                    .put("Cache-Control", "no-cache, no-store, max-age=0")
+                    .put("Content-Security-Policy", "frame-ancestors 'none'")
+                    .build()
+    );
+  }
+
+  @Test
+  public void test_get_overrideContentSecurityPolicy() throws Exception
+  {
+    
EasyMock.expect(serverConfig.getContentSecurityPolicy()).andReturn("frame-ancestors
 'self'").once();
+    
EasyMock.expect(httpRequest.getMethod()).andReturn(HttpMethod.GET).anyTimes();
+    
EasyMock.expect(httpResponse.getContentType()).andReturn("text/html").anyTimes();
+
+    runFilterAndVerifyHeaders(
+        ImmutableMap.<String, String>builder()
+                    .put("Cache-Control", "no-cache, no-store, max-age=0")
+                    .put("Content-Security-Policy", "frame-ancestors 'self'")
+                    .build()
+    );
+  }
+
+  @Test
+  public void test_get_invalidContentSecurityPolicy()
+  {
+    
EasyMock.expect(serverConfig.getContentSecurityPolicy()).andReturn("erroné").once();
+
+    replayAllMocks();
+
+    final RuntimeException e = Assert.assertThrows(RuntimeException.class, 
this::makeFilter);
+
+    MatcherAssert.assertThat(
+        e,
+        ThrowableMessageMatcher.hasMessage(
+            CoreMatchers.containsString("Content-Security-Policy header value 
must be fully ASCII")
+        )
+    );
+  }
+
+  private StandardResponseHeaderFilterHolder.StandardResponseHeaderFilter 
makeFilter()
+  {
+    return (StandardResponseHeaderFilterHolder.StandardResponseHeaderFilter)
+        new StandardResponseHeaderFilterHolder(serverConfig).getFilter();
+  }
+
+  private void runFilterAndVerifyHeaders(final Map<String, String> 
expectedHeaders) throws Exception
+  {
+    final Map<String, Capture<String>> captureMap = new HashMap<>();
+
+    for (final Map.Entry<String, String> entry : expectedHeaders.entrySet()) {
+      final String headerName = entry.getKey();
+      final Capture<String> headerValueCapture = Capture.newInstance();
+      captureMap.put(headerName, headerValueCapture);
+
+      httpResponse.setHeader(EasyMock.eq(headerName), 
EasyMock.capture(headerValueCapture));
+      EasyMock.expectLastCall();
+    }
+
+    filterChain.doFilter(httpRequest, httpResponse);
+    EasyMock.expectLastCall();
+
+    replayAllMocks();
+    final StandardResponseHeaderFilterHolder.StandardResponseHeaderFilter 
filter = makeFilter();
+    filter.doFilter(httpRequest, httpResponse, filterChain);
+
+    for (final Map.Entry<String, String> entry : expectedHeaders.entrySet()) {
+      Assert.assertEquals(entry.getKey(), entry.getValue(), 
captureMap.get(entry.getKey()).getValue());
+    }
+  }
+
+  private void replayAllMocks()
+  {
+    EasyMock.replay(serverConfig, httpRequest, httpResponse, filterChain);
+  }
+}
diff --git 
a/services/src/main/java/org/apache/druid/server/AsyncQueryForwardingServlet.java
 
b/services/src/main/java/org/apache/druid/server/AsyncQueryForwardingServlet.java
index 87c1c15382..3668543bf4 100644
--- 
a/services/src/main/java/org/apache/druid/server/AsyncQueryForwardingServlet.java
+++ 
b/services/src/main/java/org/apache/druid/server/AsyncQueryForwardingServlet.java
@@ -46,6 +46,7 @@ import org.apache.druid.query.QueryInterruptedException;
 import org.apache.druid.query.QueryMetrics;
 import org.apache.druid.query.QueryToolChestWarehouse;
 import org.apache.druid.server.initialization.ServerConfig;
+import 
org.apache.druid.server.initialization.jetty.StandardResponseHeaderFilterHolder;
 import org.apache.druid.server.log.RequestLogger;
 import org.apache.druid.server.metrics.QueryCountStatsProvider;
 import org.apache.druid.server.router.QueryHostFinder;
@@ -532,6 +533,17 @@ public class AsyncQueryForwardingServlet extends 
AsyncProxyServlet implements Qu
     return 0L;
   }
 
+  @Override
+  protected void onServerResponseHeaders(
+      HttpServletRequest clientRequest,
+      HttpServletResponse proxyResponse,
+      Response serverResponse
+  )
+  {
+    
StandardResponseHeaderFilterHolder.deduplicateHeadersInProxyServlet(proxyResponse,
 serverResponse);
+    super.onServerResponseHeaders(clientRequest, proxyResponse, 
serverResponse);
+  }
+
   @VisibleForTesting
   static String getAvaticaConnectionId(Map<String, Object> requestMap)
   {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to