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]