Skip to content

Commit

Permalink
Issue #4414 - add option to exclude paths from GzipHandler request in…
Browse files Browse the repository at this point in the history
…flation.

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
  • Loading branch information
lachlan-roberts committed May 9, 2022
1 parent 367c410 commit 9d6f0ea
Show file tree
Hide file tree
Showing 2 changed files with 182 additions and 1 deletion.
Expand Up @@ -56,6 +56,7 @@ public class GzipHandler extends Handler.Wrapper implements GzipFactory
private int _inflateBufferSize = -1;
// non-static, as other GzipHandler instances may have different configurations
private final IncludeExclude<String> _methods = new IncludeExclude<>();
private final IncludeExclude<String> _inflatePaths = new IncludeExclude<>(PathSpecSet.class);
private final IncludeExclude<String> _paths = new IncludeExclude<>(PathSpecSet.class);
private final IncludeExclude<String> _mimeTypes = new IncludeExclude<>(AsciiLowerCaseSet.class);
private HttpField _vary = GzipResponse.VARY_ACCEPT_ENCODING;
Expand Down Expand Up @@ -213,6 +214,41 @@ public void addExcludedPaths(String... pathspecs)
}
}

/**
* Adds excluded Path Specs for request filtering on request inflation.
*
* <p>
* There are 2 syntaxes supported, Servlet <code>url-pattern</code> based, and
* Regex based. This means that the initial characters on the path spec
* line are very strict, and determine the behavior of the path matching.
* <ul>
* <li>If the spec starts with <code>'^'</code> the spec is assumed to be
* a regex based path spec and will match with normal Java regex rules.</li>
* <li>If the spec starts with <code>'/'</code> then spec is assumed to be
* a Servlet url-pattern rules path spec for either an exact match
* or prefix based match.</li>
* <li>If the spec starts with <code>'*.'</code> then spec is assumed to be
* a Servlet url-pattern rules path spec for a suffix based match.</li>
* <li>All other syntaxes are unsupported</li>
* </ul>
* <p>
* Note: inclusion takes precedence over exclude.
*
* @param pathspecs Path specs (as per servlet spec) to exclude. If a
* ServletContext is available, the paths are relative to the context path,
* otherwise they are absolute.<br>
* For backward compatibility the pathspecs may be comma separated strings, but this
* will not be supported in future versions.
* @see #addIncludedInflationPaths(String...)
*/
public void addExcludedInflationPaths(String... pathspecs)
{
for (String p : pathspecs)
{
_inflatePaths.exclude(StringUtil.csvSplit(p));
}
}

/**
* Adds included HTTP Methods (eg: POST, PATCH, DELETE) for filtering.
*
Expand Down Expand Up @@ -299,6 +335,38 @@ public void addIncludedPaths(String... pathspecs)
}
}

/**
* Add included Path Specs for filtering on request inflation.
*
* <p>
* There are 2 syntaxes supported, Servlet <code>url-pattern</code> based, and
* Regex based. This means that the initial characters on the path spec
* line are very strict, and determine the behavior of the path matching.
* <ul>
* <li>If the spec starts with <code>'^'</code> the spec is assumed to be
* a regex based path spec and will match with normal Java regex rules.</li>
* <li>If the spec starts with <code>'/'</code> then spec is assumed to be
* a Servlet url-pattern rules path spec for either an exact match
* or prefix based match.</li>
* <li>If the spec starts with <code>'*.'</code> then spec is assumed to be
* a Servlet url-pattern rules path spec for a suffix based match.</li>
* <li>All other syntaxes are unsupported</li>
* </ul>
* <p>
* Note: inclusion takes precedence over exclusion.
*
* @param pathspecs Path specs (as per servlet spec) to include. If a
* ServletContext is available, the paths are relative to the context path,
* otherwise they are absolute
*/
public void addIncludedInflationPaths(String... pathspecs)
{
for (String p : pathspecs)
{
_inflatePaths.include(StringUtil.csvSplit(p));
}
}

@Override
public DeflaterPool.Entry getDeflaterEntry(Request request, long contentLength)
{
Expand Down Expand Up @@ -354,6 +422,18 @@ public String[] getExcludedPaths()
return excluded.toArray(new String[0]);
}

/**
* Get the current filter list of excluded Path Specs for request inflation.
*
* @return the filter list of excluded Path Specs
* @see #getIncludedInflationPaths()
*/
public String[] getExcludedInflationPaths()
{
Set<String> excluded = _inflatePaths.getExcluded();
return excluded.toArray(new String[0]);
}

/**
* Get the current filter list of included HTTP Methods
*
Expand Down Expand Up @@ -390,6 +470,18 @@ public String[] getIncludedPaths()
return includes.toArray(new String[0]);
}

/**
* Get the current filter list of included Path Specs for request inflation.
*
* @return the filter list of included Path Specs
* @see #getExcludedInflationPaths()
*/
public String[] getIncludedInflationPaths()
{
Set<String> includes = _inflatePaths.getIncluded();
return includes.toArray(new String[0]);
}

/**
* Get the minimum size, in bytes, that a response {@code Content-Length} must be
* before compression will trigger.
Expand Down Expand Up @@ -439,7 +531,7 @@ public Request.Processor handle(Request request) throws Exception
// TODO: support more than GZIP.
// Handle request inflation
HttpFields httpFields = request.getHeaders();
boolean inflated = _inflateBufferSize > 0 && httpFields.contains(HttpHeader.CONTENT_ENCODING, "gzip");
boolean inflated = _inflateBufferSize > 0 && httpFields.contains(HttpHeader.CONTENT_ENCODING, "gzip") && isPathInflatable(path);

// TODO: do we need this?
// Are we already being gzipped?
Expand Down Expand Up @@ -575,6 +667,20 @@ protected boolean isPathGzipable(String requestURI)
return _paths.test(requestURI);
}

/**
* Test if the provided Request URI is allowed to be inflated based on the Path Specs filters.
*
* @param requestURI the request uri
* @return whether decompressing is allowed for the given the path.
*/
protected boolean isPathInflatable(String requestURI)
{
if (requestURI == null)
return true;

return _inflatePaths.test(requestURI);
}

/**
* Set the excluded filter list of HTTP methods (replacing any previously set)
*
Expand Down Expand Up @@ -624,6 +730,20 @@ public void setExcludedPaths(String... pathspecs)
_paths.exclude(pathspecs);
}

/**
* Set the excluded filter list of Path specs (replacing any previously set)
*
* @param pathspecs Path specs (as per servlet spec) to exclude from inflation. If a
* ServletContext is available, the paths are relative to the context path,
* otherwise they are absolute.
* @see #setIncludedInflatePaths(String...)
*/
public void setExcludedInflatePaths(String... pathspecs)
{
_inflatePaths.getExcluded().clear();
_inflatePaths.exclude(pathspecs);
}

/**
* Set the included filter list of HTTP methods (replacing any previously set)
*
Expand Down Expand Up @@ -673,6 +793,20 @@ public void setIncludedPaths(String... pathspecs)
_paths.include(pathspecs);
}

/**
* Set the included filter list of Path specs (replacing any previously set)
*
* @param pathspecs Path specs (as per servlet spec) to include for inflation. If a
* ServletContext is available, the paths are relative to the context path,
* otherwise they are absolute
* @see #setExcludedInflatePaths(String...)
*/
public void setIncludedInflatePaths(String... pathspecs)
{
_inflatePaths.getIncluded().clear();
_inflatePaths.include(pathspecs);
}

/**
* Set the minimum response size to trigger dynamic compression.
* <p>
Expand Down
Expand Up @@ -54,6 +54,7 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.equalToIgnoringCase;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
Expand Down Expand Up @@ -625,6 +626,52 @@ public void testDeleteETagGzipHandler() throws Exception
assertThat(response.get("Content-Encoding"), not(Matchers.equalToIgnoringCase("gzip")));
}

@Test
public void testIncludeExcludeGzipHandlerInflate() throws Exception
{
_contextHandler.setHandler(new EchoHandler());
_server.start();

_gziphandler.addExcludedInflationPaths("/ctx/echo/exclude");
_gziphandler.addIncludedInflationPaths("/ctx/echo/include");

String message = "hello world";
byte[] gzippedMessage = gzipContent(message);

// The included path does deflate the content.
HttpTester.Response response = sendGzipRequest("/ctx/echo/include", message);
assertThat(response.getStatus(), equalTo(HttpStatus.OK_200));
assertThat(response.getContent(), equalTo(message));

// The excluded path does not deflate the content.
response = sendGzipRequest("/ctx/echo/exclude", message);
assertThat(response.getStatus(), equalTo(HttpStatus.OK_200));
assertThat(response.getContentBytes(), equalTo(gzippedMessage));
}

private byte[] gzipContent(String content) throws IOException
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream output = new GZIPOutputStream(baos);
output.write(content.getBytes(StandardCharsets.UTF_8));
output.close();
return baos.toByteArray();
}

private HttpTester.Response sendGzipRequest(String uri, String data) throws Exception
{
HttpTester.Request request = HttpTester.newRequest();
request.setMethod("GET");
request.setURI(uri);
request.setVersion("HTTP/1.0");
request.setHeader("Host", "tester");
request.setHeader("Content-Type", "text/plain");
request.setHeader("Content-Encoding", "gzip");
request.setContent(gzipContent(data));

return HttpTester.parseResponse(_connector.getResponse(request.generate()));
}

@Test
public void testAddGetPaths()
{
Expand Down

0 comments on commit 9d6f0ea

Please sign in to comment.