Commit 5f250800 authored by Ivan Sopov's avatar Ivan Sopov Committed by Phillip Webb

Add HTTP compression excludeUserAgents property

Closes gh-3363
parent 5e243b28
...@@ -68,6 +68,7 @@ content into your application; rather pick only the properties that you need. ...@@ -68,6 +68,7 @@ content into your application; rather pick only the properties that you need.
server.port=8080 server.port=8080
server.address= # bind to a specific NIC server.address= # bind to a specific NIC
server.compression.enabled=false # if response compression is enabled server.compression.enabled=false # if response compression is enabled
server.compression.exclude-user-agents= # list of user-agents to exclude from compression
server.compression.mime-types=text/html,text/xml,text/plain,text/css # comma-separated list of MIME types that should be compressed server.compression.mime-types=text/html,text/xml,text/plain,text/css # comma-separated list of MIME types that should be compressed
server.compression.min-response-size=2048 # minimum response size that is required for compression to be performed server.compression.min-response-size=2048 # minimum response size that is required for compression to be performed
server.context-parameters.*= # Servlet context init parameters, e.g. server.context-parameters.a=alpha server.context-parameters.*= # Servlet context init parameters, e.g. server.context-parameters.a=alpha
......
...@@ -37,7 +37,12 @@ public class Compression { ...@@ -37,7 +37,12 @@ public class Compression {
"text/css" }; "text/css" };
/** /**
* Minimum response size that is required for compression to be performed * Comma-separated list of user agents for which responses should not be compressed.
*/
private String[] excludedUserAgents = null;
/**
* Minimum response size that is required for compression to be performed.
*/ */
private int minResponseSize = 2048; private int minResponseSize = 2048;
...@@ -65,4 +70,11 @@ public class Compression { ...@@ -65,4 +70,11 @@ public class Compression {
this.minResponseSize = minSize; this.minResponseSize = minSize;
} }
public String[] getExcludedUserAgents() {
return this.excludedUserAgents;
}
public void setExcludedUserAgents(String[] excludedUserAgents) {
this.excludedUserAgents = excludedUserAgents;
}
} }
...@@ -588,6 +588,12 @@ public class JettyEmbeddedServletContainerFactory extends ...@@ -588,6 +588,12 @@ public class JettyEmbeddedServletContainerFactory extends
.invoke(handler, .invoke(handler,
new HashSet<String>(Arrays.asList(compression new HashSet<String>(Arrays.asList(compression
.getMimeTypes()))); .getMimeTypes())));
if (compression.getExcludedUserAgents() != null) {
ReflectionUtils.findMethod(handlerClass, "setExcluded", Set.class)
.invoke(handler,
new HashSet<String>(Arrays.asList(compression
.getExcludedUserAgents())));
}
return handler; return handler;
} }
catch (Exception ex) { catch (Exception ex) {
...@@ -605,6 +611,10 @@ public class JettyEmbeddedServletContainerFactory extends ...@@ -605,6 +611,10 @@ public class JettyEmbeddedServletContainerFactory extends
gzipHandler.setMinGzipSize(compression.getMinResponseSize()); gzipHandler.setMinGzipSize(compression.getMinResponseSize());
gzipHandler.setMimeTypes(new HashSet<String>(Arrays.asList(compression gzipHandler.setMimeTypes(new HashSet<String>(Arrays.asList(compression
.getMimeTypes()))); .getMimeTypes())));
if (compression.getExcludedUserAgents() != null) {
gzipHandler.setExcluded(new HashSet<String>(Arrays.asList(compression
.getExcludedUserAgents())));
}
return gzipHandler; return gzipHandler;
} }
...@@ -623,6 +633,11 @@ public class JettyEmbeddedServletContainerFactory extends ...@@ -623,6 +633,11 @@ public class JettyEmbeddedServletContainerFactory extends
ReflectionUtils.findMethod(handlerClass, "setIncludedMimeTypes", ReflectionUtils.findMethod(handlerClass, "setIncludedMimeTypes",
String[].class).invoke(handler, String[].class).invoke(handler,
new Object[] { compression.getMimeTypes() }); new Object[] { compression.getMimeTypes() });
if (compression.getExcludedUserAgents() != null) {
ReflectionUtils.findMethod(handlerClass, "setExcludedAgentPatterns",
String[].class).invoke(handler,
new Object[] { compression.getExcludedUserAgents() });
}
return handler; return handler;
} }
catch (Exception ex) { catch (Exception ex) {
......
...@@ -280,6 +280,11 @@ public class TomcatEmbeddedServletContainerFactory extends ...@@ -280,6 +280,11 @@ public class TomcatEmbeddedServletContainerFactory extends
protocol.setCompressionMinSize(compression.getMinResponseSize()); protocol.setCompressionMinSize(compression.getMinResponseSize());
protocol.setCompressableMimeTypes(StringUtils protocol.setCompressableMimeTypes(StringUtils
.arrayToCommaDelimitedString(compression.getMimeTypes())); .arrayToCommaDelimitedString(compression.getMimeTypes()));
if (getCompression().getExcludedUserAgents() != null) {
protocol.setNoCompressionUserAgents(StringUtils
.arrayToCommaDelimitedString(getCompression()
.getExcludedUserAgents()));
}
} }
} }
......
...@@ -19,6 +19,7 @@ package org.springframework.boot.context.embedded.undertow; ...@@ -19,6 +19,7 @@ package org.springframework.boot.context.embedded.undertow;
import io.undertow.Handlers; import io.undertow.Handlers;
import io.undertow.Undertow; import io.undertow.Undertow;
import io.undertow.Undertow.Builder; import io.undertow.Undertow.Builder;
import io.undertow.attribute.RequestHeaderAttribute;
import io.undertow.predicate.Predicate; import io.undertow.predicate.Predicate;
import io.undertow.predicate.Predicates; import io.undertow.predicate.Predicates;
import io.undertow.server.HttpHandler; import io.undertow.server.HttpHandler;
...@@ -27,6 +28,7 @@ import io.undertow.server.handlers.encoding.ContentEncodingRepository; ...@@ -27,6 +28,7 @@ import io.undertow.server.handlers.encoding.ContentEncodingRepository;
import io.undertow.server.handlers.encoding.EncodingHandler; import io.undertow.server.handlers.encoding.EncodingHandler;
import io.undertow.server.handlers.encoding.GzipEncodingProvider; import io.undertow.server.handlers.encoding.GzipEncodingProvider;
import io.undertow.servlet.api.DeploymentManager; import io.undertow.servlet.api.DeploymentManager;
import io.undertow.util.HttpString;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.net.ServerSocket; import java.net.ServerSocket;
...@@ -100,8 +102,8 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine ...@@ -100,8 +102,8 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine
private Undertow createUndertowServer() { private Undertow createUndertowServer() {
try { try {
HttpHandler servletHandler = this.manager.start(); HttpHandler httpHandler = this.manager.start();
this.builder.setHandler(getContextHandler(servletHandler)); this.builder.setHandler(getContextHandler(httpHandler));
return this.builder.build(); return this.builder.build();
} }
catch (ServletException ex) { catch (ServletException ex) {
...@@ -110,25 +112,36 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine ...@@ -110,25 +112,36 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine
} }
} }
private HttpHandler getContextHandler(HttpHandler servletHandler) { private HttpHandler getContextHandler(HttpHandler httpHandler) {
HttpHandler contextHandler = configurationCompressionIfNecessary(servletHandler); HttpHandler contextHandler = configurationCompressionIfNecessary(httpHandler);
if (StringUtils.isEmpty(this.contextPath)) { if (StringUtils.isEmpty(this.contextPath)) {
return contextHandler; return contextHandler;
} }
return Handlers.path().addPrefixPath(this.contextPath, contextHandler); return Handlers.path().addPrefixPath(this.contextPath, contextHandler);
} }
private HttpHandler configurationCompressionIfNecessary(HttpHandler servletHandler) { private HttpHandler configurationCompressionIfNecessary(HttpHandler httpHandler) {
if (this.compression == null || !this.compression.getEnabled()) { if (this.compression == null || !this.compression.getEnabled()) {
return servletHandler; return httpHandler;
} }
ContentEncodingRepository encodingRepository = new ContentEncodingRepository(); ContentEncodingRepository repository = new ContentEncodingRepository();
Predicate mimeAndSizePredicate = Predicates.and(Predicates repository.addEncodingHandler("gzip", new GzipEncodingProvider(), 50,
.maxContentSize(this.compression.getMinResponseSize()), Predicates Predicates.and(getCompressionPredicates(this.compression)));
.or(new CompressibleMimeTypePredicate(this.compression.getMimeTypes()))); return new EncodingHandler(repository).setNext(httpHandler);
encodingRepository.addEncodingHandler("gzip", new GzipEncodingProvider(), 50, }
mimeAndSizePredicate);
return new EncodingHandler(encodingRepository).setNext(servletHandler); private Predicate[] getCompressionPredicates(Compression compression) {
List<Predicate> predicates = new ArrayList<Predicate>();
predicates.add(Predicates.maxContentSize(compression.getMinResponseSize()));
predicates.add(new CompressibleMimeTypePredicate(compression.getMimeTypes()));
if (compression.getExcludedUserAgents() != null) {
for (String agent : compression.getExcludedUserAgents()) {
RequestHeaderAttribute agentHeader = new RequestHeaderAttribute(
new HttpString(HttpHeaders.USER_AGENT));
predicates.add(Predicates.not(Predicates.regex(agentHeader, agent)));
}
}
return predicates.toArray(new Predicate[predicates.size()]);
} }
private String getPortsDescription() { private String getPortsDescription() {
......
...@@ -531,35 +531,43 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { ...@@ -531,35 +531,43 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests {
@Test @Test
public void compression() throws Exception { public void compression() throws Exception {
assertTrue(doTestCompression(10000, null)); assertTrue(doTestCompression(10000, null, null));
} }
@Test @Test
public void noCompressionForSmallResponse() throws Exception { public void noCompressionForSmallResponse() throws Exception {
assertFalse(doTestCompression(100, null)); assertFalse(doTestCompression(100, null, null));
} }
@Test @Test
public void noCompressionForMimeType() throws Exception { public void noCompressionForMimeType() throws Exception {
String[] mimeTypes = new String[] { "text/html", "text/xml", "text/css" }; String[] mimeTypes = new String[] { "text/html", "text/xml", "text/css" };
assertFalse(doTestCompression(10000, mimeTypes)); assertFalse(doTestCompression(10000, mimeTypes, null));
} }
private boolean doTestCompression(int contentSize, String[] mimeTypes) @Test
throws Exception { public void noCompressionForUserAgent() throws Exception {
String testContent = setUpFactoryForCompression(contentSize, mimeTypes); assertFalse(doTestCompression(10000, null, new String[] { "testUserAgent" }));
}
private boolean doTestCompression(int contentSize, String[] mimeTypes,
String[] excludedUserAgents) throws Exception {
String testContent = setUpFactoryForCompression(contentSize, mimeTypes,
excludedUserAgents);
TestGzipInputStreamFactory inputStreamFactory = new TestGzipInputStreamFactory(); TestGzipInputStreamFactory inputStreamFactory = new TestGzipInputStreamFactory();
Map<String, InputStreamFactory> contentDecoderMap = singletonMap("gzip", Map<String, InputStreamFactory> contentDecoderMap = singletonMap("gzip",
(InputStreamFactory) inputStreamFactory); (InputStreamFactory) inputStreamFactory);
String response = getResponse(getLocalUrl("/test.txt"), String response = getResponse(
getLocalUrl("/test.txt"),
new HttpComponentsClientHttpRequestFactory(HttpClientBuilder.create() new HttpComponentsClientHttpRequestFactory(HttpClientBuilder.create()
.setUserAgent("testUserAgent")
.setContentDecoderRegistry(contentDecoderMap).build())); .setContentDecoderRegistry(contentDecoderMap).build()));
assertThat(response, equalTo(testContent)); assertThat(response, equalTo(testContent));
return inputStreamFactory.wasCompressionUsed(); return inputStreamFactory.wasCompressionUsed();
} }
protected String setUpFactoryForCompression(int contentSize, String[] mimeTypes) protected String setUpFactoryForCompression(int contentSize, String[] mimeTypes,
throws Exception { String[] excludedUserAgents) throws Exception {
char[] chars = new char[contentSize]; char[] chars = new char[contentSize];
Arrays.fill(chars, 'F'); Arrays.fill(chars, 'F');
String testContent = new String(chars); String testContent = new String(chars);
...@@ -572,6 +580,9 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { ...@@ -572,6 +580,9 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests {
if (mimeTypes != null) { if (mimeTypes != null) {
compression.setMimeTypes(mimeTypes); compression.setMimeTypes(mimeTypes);
} }
if (excludedUserAgents != null) {
compression.setExcludedUserAgents(excludedUserAgents);
}
factory.setCompression(compression); factory.setCompression(compression);
this.container = factory.getEmbeddedServletContainer(); this.container = factory.getEmbeddedServletContainer();
this.container.start(); this.container.start();
......
...@@ -186,8 +186,8 @@ public class JettyEmbeddedServletContainerFactoryTests extends ...@@ -186,8 +186,8 @@ public class JettyEmbeddedServletContainerFactoryTests extends
@Override @Override
@SuppressWarnings("serial") @SuppressWarnings("serial")
// Workaround for Jetty issue - https://bugs.eclipse.org/bugs/show_bug.cgi?id=470646 // Workaround for Jetty issue - https://bugs.eclipse.org/bugs/show_bug.cgi?id=470646
protected String setUpFactoryForCompression(final int contentSize, String[] mimeTypes) protected String setUpFactoryForCompression(final int contentSize,
throws Exception { String[] mimeTypes, String[] excludedUserAgents) throws Exception {
char[] chars = new char[contentSize]; char[] chars = new char[contentSize];
Arrays.fill(chars, 'F'); Arrays.fill(chars, 'F');
final String testContent = new String(chars); final String testContent = new String(chars);
...@@ -197,6 +197,9 @@ public class JettyEmbeddedServletContainerFactoryTests extends ...@@ -197,6 +197,9 @@ public class JettyEmbeddedServletContainerFactoryTests extends
if (mimeTypes != null) { if (mimeTypes != null) {
compression.setMimeTypes(mimeTypes); compression.setMimeTypes(mimeTypes);
} }
if (excludedUserAgents != null) {
compression.setExcludedUserAgents(excludedUserAgents);
}
factory.setCompression(compression); factory.setCompression(compression);
this.container = factory.getEmbeddedServletContainer(new ServletRegistrationBean( this.container = factory.getEmbeddedServletContainer(new ServletRegistrationBean(
new HttpServlet() { new HttpServlet() {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment