Skip to content

Commit

Permalink
Fix ZLIB decompression in BiDiGzipFilter
Browse files Browse the repository at this point in the history
To decompress deflate-encoded requests one should use the
`InflaterInputStream` class, not `DeflaterInputStream`. The latter
 actually is used for compressing requests on the fly.

 This change adds the parameter `gzipCompatibleInflation` for
 controlling the mode of ZLIB decompression.

 Also it fixes a minor bug, when a broken inflater could stay in the
  thread local storage.
  • Loading branch information
arteam committed Jul 18, 2015
1 parent 634df88 commit c95f9ef
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 26 deletions.
32 changes: 18 additions & 14 deletions docs/source/manual/configuration.rst
Expand Up @@ -73,20 +73,24 @@ GZip
bufferSize: 8KiB bufferSize: 8KiB
+----------------------+------------+---------------------------------------------------------------------------------------------------+ +-------------------------+------------+---------------------------------------------------------------------------------------------------+
| Name | Default | Description | | Name | Default | Description |
+======================+============+===================================================================================================+ +=========================+============+===================================================================================================+
| enabled | true | If true, all requests with gzip in their Accept-Content-Encoding | | enabled | true | If true, all requests with gzip in their Accept-Content-Encoding |
| | | headers will have their response entities encoded with gzip. | | | | headers will have their response entities encoded with gzip. |
+----------------------+------------+---------------------------------------------------------------------------------------------------+ +-------------------------+------------+---------------------------------------------------------------------------------------------------+
| minimumEntitySize | 256 bytes | All response entities under this size are not compressed. | | minimumEntitySize | 256 bytes | All response entities under this size are not compressed. |
+----------------------+------------+---------------------------------------------------------------------------------------------------+ +-------------------------+------------+---------------------------------------------------------------------------------------------------+
| bufferSize | 8KiB | The size of the buffer to use when compressing. | | bufferSize | 8KiB | The size of the buffer to use when compressing. |
+----------------------+------------+---------------------------------------------------------------------------------------------------+ +-------------------------+------------+---------------------------------------------------------------------------------------------------+
| excludedUserAgents | [] | The set of user agents to exclude from compression. | | excludedUserAgents | [] | The set of user agents to exclude from compression. |
+----------------------+------------+---------------------------------------------------------------------------------------------------+ +-------------------------+------------+---------------------------------------------------------------------------------------------------+
| compressedMimeTypes | [] | If specified, the set of mime types to compress. | | compressedMimeTypes | [] | If specified, the set of mime types to compress. |
+----------------------+------------+---------------------------------------------------------------------------------------------------+ +-------------------------+------------+---------------------------------------------------------------------------------------------------+
| gzipCompatibleDeflation | true | If true, then ZLIB deflation(compression) will be performed in the GZIP-compatible mode. |
+-------------------------+------------+---------------------------------------------------------------------------------------------------+
| gzipCompatibleInflation | true | If true, then ZLIB inflation(decompression) will be performed in the GZIP-compatible mode. |
+-------------------------+------------+---------------------------------------------------------------------------------------------------+




.. _man-configuration-requestLog: .. _man-configuration-requestLog:
Expand Down
Expand Up @@ -21,16 +21,22 @@
import java.util.Enumeration; import java.util.Enumeration;
import java.util.Set; import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.zip.Deflater; import java.util.zip.Inflater;
import java.util.zip.DeflaterInputStream; import java.util.zip.InflaterInputStream;
import java.util.zip.GZIPInputStream; import java.util.zip.GZIPInputStream;


/** /**
* An extension of {@link IncludableGzipFilter} which decompresses gzip- and deflate-encoded request * An extension of {@link IncludableGzipFilter} which decompresses gzip- and deflate-encoded request
* entities. * entities.
*/ */
public class BiDiGzipFilter extends IncludableGzipFilter { public class BiDiGzipFilter extends IncludableGzipFilter {
private final ThreadLocal<Deflater> localDeflater = new ThreadLocal<>(); private final ThreadLocal<Inflater> localInflater = new ThreadLocal<>();

/**
* Whether inflating (decompressing) of deflate-encoded requests
* should be performed in the GZIP-compatible mode
*/
private boolean inflateNoWrap = true;


public Set<String> getMimeTypes() { public Set<String> getMimeTypes() {
return _mimeTypes; return _mimeTypes;
Expand Down Expand Up @@ -73,6 +79,14 @@ public void setDeflateNoWrap(boolean noWrap) {
this._deflateNoWrap = noWrap; this._deflateNoWrap = noWrap;
} }


public boolean isInflateNoWrap() {
return inflateNoWrap;
}

public void setInflateNoWrap(boolean inflateNoWrap) {
this.inflateNoWrap = inflateNoWrap;
}

public Set<String> getMethods() { public Set<String> getMethods() {
return _methods; return _methods;
} }
Expand Down Expand Up @@ -135,22 +149,28 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
} }
} }


private Deflater buildDeflater() { private Inflater buildInflater() {
final Deflater deflater = localDeflater.get(); final Inflater inflater = localInflater.get();
if (deflater != null) { if (inflater != null) {
return deflater; // The request could fail in the middle of decompressing, so potentially we can get
// a broken inflater in the thread local storage. That's why we need to clear the storage.
localInflater.set(null);

// Reuse the inflater from the thread local storage
inflater.reset();
return inflater;
} else {
return new Inflater(inflateNoWrap);
} }
return new Deflater(_deflateCompressionLevel, _deflateNoWrap);
} }


private ServletRequest wrapDeflatedRequest(HttpServletRequest request) throws IOException { private ServletRequest wrapDeflatedRequest(HttpServletRequest request) throws IOException {
final Deflater deflater = buildDeflater(); final Inflater inflater = buildInflater();
final DeflaterInputStream input = new DeflaterInputStream(request.getInputStream(), deflater, _bufferSize) { final InflaterInputStream input = new InflaterInputStream(request.getInputStream(), inflater, _bufferSize) {
@Override @Override
public void close() throws IOException { public void close() throws IOException {
deflater.reset();
localDeflater.set(deflater);
super.close(); super.close();
localInflater.set(inflater);
} }
}; };
return new WrappedServletRequest(request, input); return new WrappedServletRequest(request, input);
Expand Down
Expand Up @@ -27,6 +27,7 @@ public class GzipFilterFactory {
private Set<String> compressedMimeTypes = Sets.newHashSet(); private Set<String> compressedMimeTypes = Sets.newHashSet();
private Set<String> includedMethods = Sets.newHashSet(); private Set<String> includedMethods = Sets.newHashSet();
private boolean gzipCompatibleDeflation = true; private boolean gzipCompatibleDeflation = true;
private boolean gzipCompatibleInflation = true;
private String vary = "Accept-Encoding"; private String vary = "Accept-Encoding";


@Min(Deflater.DEFAULT_COMPRESSION) @Min(Deflater.DEFAULT_COMPRESSION)
Expand Down Expand Up @@ -103,6 +104,16 @@ public void setGzipCompatibleDeflation(boolean compatible) {
this.gzipCompatibleDeflation = compatible; this.gzipCompatibleDeflation = compatible;
} }


@JsonProperty
public boolean isGzipCompatibleInflation() {
return gzipCompatibleInflation;
}

@JsonProperty
public void setGzipCompatibleInflation(boolean gzipCompatibleInflation) {
this.gzipCompatibleInflation = gzipCompatibleInflation;
}

@JsonProperty @JsonProperty
public Set<Pattern> getExcludedUserAgentPatterns() { public Set<Pattern> getExcludedUserAgentPatterns() {
return excludedUserAgentPatterns; return excludedUserAgentPatterns;
Expand Down Expand Up @@ -162,6 +173,7 @@ public BiDiGzipFilter build() {
} }


filter.setDeflateNoWrap(gzipCompatibleDeflation); filter.setDeflateNoWrap(gzipCompatibleDeflation);
filter.setInflateNoWrap(gzipCompatibleInflation);


return filter; return filter;
} }
Expand Down
@@ -0,0 +1,202 @@
package io.dropwizard.jetty;

import com.google.common.base.Charsets;
import com.google.common.io.ByteStreams;
import com.google.common.io.Resources;
import com.google.common.net.HttpHeaders;
import com.google.common.net.MediaType;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletTester;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import javax.servlet.DispatcherType;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.ByteBuffer;
import java.util.EnumSet;
import java.util.zip.*;

import static org.assertj.core.api.Assertions.assertThat;

public class BiDiGzipFilterTest {

private static final String PLAIN_TEXT_UTF_8 = MediaType.PLAIN_TEXT_UTF_8.toString().replace(" ", "");

private final BiDiGzipFilter gzipFilter = new BiDiGzipFilter();
private final ServletTester servletTester = new ServletTester();

@Before
public void setUp() throws Exception {
gzipFilter.setVary("Accept-Encoding");
gzipFilter.setDeflateNoWrap(false);
gzipFilter.setInflateNoWrap(false);

servletTester.addServlet(BannerServlet.class, "/banner");
servletTester.addFilter(new FilterHolder(gzipFilter), "/*", EnumSet.allOf(DispatcherType.class));
servletTester.start();
}

@After
public void tearDown() throws Exception {
servletTester.stop();
}

@Test
public void testCompressResponseWithGzip() throws Exception {
final ByteBuffer request = getRequest("gzip");
final HttpTester.Response response = run(request);

assertSuccessfulGetResponse(response, "gzip");
assertThat(decompress(response.getContentBytes(), "gzip")).isEqualTo(getResourceAsByteArray("assets/banner.txt"));
}

@Test
public void testCompressResponseWithDeflate() throws Exception {
final ByteBuffer request = getRequest("deflate");
final HttpTester.Response response = run(request);

assertSuccessfulGetResponse(response, "deflate");
assertThat(decompress(response.getContentBytes(), "deflate")).isEqualTo(getResourceAsByteArray("assets/banner.txt"));
}

@Test
public void testCompressResponseWithDeflateNoWrap() throws Exception {
gzipFilter.setDeflateNoWrap(true);
final ByteBuffer request = getRequest("deflate");
final HttpTester.Response response = run(request);

assertSuccessfulGetResponse(response, "deflate");
assertThat(decompress(response.getContentBytes(), "deflateNoWrap")).isEqualTo(getResourceAsByteArray("assets/banner.txt"));
}

@Test
public void testDecompressGzipRequest() throws Exception {
final ByteBuffer request = postRequest(compress("assets/new-banner.txt", "gzip"), "gzip");
final HttpTester.Response response = run(request);
assertSuccessfulPostResponse(response);
}

@Test
public void testDecompressDeflateRequest() throws Exception {
final ByteBuffer request = postRequest(compress("assets/new-banner.txt", "deflate"), "deflate");
final HttpTester.Response response = run(request);
assertSuccessfulPostResponse(response);
}

@Test
public void testDecompressDeflateRequestWithInflateNoWrap() throws Exception {
gzipFilter.setInflateNoWrap(true);
final ByteBuffer request = postRequest(compress("assets/new-banner.txt", "deflateNoWrap"), "deflate");

final HttpTester.Response response = run(request);
assertSuccessfulPostResponse(response);
}

private static ByteBuffer getRequest(String encoding) {
final HttpTester.Request request = HttpTester.newRequest();
request.setMethod("GET");
request.setURI("/banner");
request.setHeader(HttpHeaders.ACCEPT_ENCODING, encoding);
return request.generate();
}

private static ByteBuffer postRequest(byte[] content, String encoding) {
final HttpTester.Request request = HttpTester.newRequest();
request.setMethod("POST");
request.setURI("/banner");
request.setHeader(HttpHeaders.CONTENT_ENCODING, encoding);
request.setHeader(HttpHeaders.CONTENT_TYPE, PLAIN_TEXT_UTF_8);
request.setContent(content);
return request.generate();
}

private HttpTester.Response run(ByteBuffer request) throws Exception {
return HttpTester.parseResponse(servletTester.getResponses(request));
}

private static void assertSuccessfulGetResponse(HttpTester.Response response, String encoding) {
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.get(HttpHeader.CONTENT_ENCODING)).isEqualTo(encoding);
assertThat(response.get(HttpHeader.VARY)).isEqualTo(HttpHeaders.ACCEPT_ENCODING);
assertThat(response.get(HttpHeader.CONTENT_TYPE)).isEqualToIgnoringCase(PLAIN_TEXT_UTF_8);
}

private static void assertSuccessfulPostResponse(HttpTester.Response response) {
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContent()).isEqualTo("Banner has been updated");
}

private static byte[] getResourceAsByteArray(String resourceName) throws IOException {
return Resources.toByteArray(Resources.getResource(resourceName));
}

private static byte[] compress(String resourceName, String encoding) throws IOException {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (OutputStream os = createCompressionStream(baos, encoding)) {
Resources.copy(Resources.getResource(resourceName), os);
}
return baos.toByteArray();
}

private static byte[] decompress(byte[] compressedData, String encoding) throws IOException {
try (InputStream is = createDecompressionStream(new ByteArrayInputStream(compressedData), encoding)) {
return ByteStreams.toByteArray(is);
}
}

private static OutputStream createCompressionStream(ByteArrayOutputStream baos, String encoding)
throws IOException {
switch (encoding) {
case "gzip":
return new GZIPOutputStream(baos);
case "deflate":
return new DeflaterOutputStream(baos);
case "deflateNoWrap":
return new DeflaterOutputStream(baos, new Deflater(Deflater.DEFAULT_COMPRESSION, true));
default:
throw new IllegalArgumentException("Wrong encoding:" + encoding);
}
}

private static InputStream createDecompressionStream(ByteArrayInputStream bais, String encoding)
throws IOException {
switch (encoding) {
case "gzip":
return new GZIPInputStream(bais);
case "deflate":
return new InflaterInputStream(bais);
case "deflateNoWrap":
return new InflaterInputStream(bais, new Inflater(true));
default:
throw new IllegalArgumentException("Wrong encoding:" + encoding);
}
}

public static class BannerServlet extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setCharacterEncoding(Charsets.UTF_8.toString());
resp.setContentType(PLAIN_TEXT_UTF_8);
Resources.asCharSource(Resources.getResource("assets/banner.txt"), Charsets.UTF_8).copyTo(resp.getWriter());
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
assertThat(req.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualToIgnoringCase(PLAIN_TEXT_UTF_8);
assertThat(req.getHeader(HttpHeaders.CONTENT_ENCODING)).isNull();
assertThat(ByteStreams.toByteArray(req.getInputStream()))
.isEqualTo(getResourceAsByteArray("assets/new-banner.txt"));

resp.setContentType(PLAIN_TEXT_UTF_8);
resp.getWriter().write("Banner has been updated");
}
}
}
8 changes: 8 additions & 0 deletions dropwizard-jetty/src/test/resources/assets/banner.txt
@@ -0,0 +1,8 @@
| |
__| |_ _ _ __ ___ _ __ ___ _ _
/ _` | | | | '_ ` _ \| '_ ` _ \| | | |
| (_| | |_| | | | | | | | | | | | |_| |
\__,_|\__,_|_| |_| |_|_| |_| |_|\__, |
__/ |
|___/

7 changes: 7 additions & 0 deletions dropwizard-jetty/src/test/resources/assets/new-banner.txt
@@ -0,0 +1,7 @@
d8888b. db db .88b d88. .88b d88. db db
88 `8D 88 88 88'YbdP`88 88'YbdP`88 `8b d8'
88 88 88 88 88 88 88 88 88 88 `8bd8'
88 88 88 88 88 88 88 88 88 88 88
88 .8D 88b d88 88 88 88 88 88 88 88
Y8888D' ~Y8888P' YP YP YP YP YP YP YP

0 comments on commit c95f9ef

Please sign in to comment.