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 Apr 12, 2022
1 parent 8bcb842 commit 5c760ae
Show file tree
Hide file tree
Showing 2 changed files with 180 additions and 1 deletion.
Expand Up @@ -168,6 +168,7 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
// non-static, as other GzipHandler instances may have different configurations
private final IncludeExclude<String> _methods = new IncludeExclude<>();
private final IncludeExclude<String> _paths = new IncludeExclude<>(PathSpecSet.class);
private final IncludeExclude<String> _inflatePaths = new IncludeExclude<>(PathSpecSet.class);
private final IncludeExclude<String> _mimeTypes = new IncludeExclude<>(AsciiLowerCaseSet.class);
private HttpField _vary = GzipHttpOutputInterceptor.VARY_ACCEPT_ENCODING;

Expand Down Expand Up @@ -354,6 +355,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 @@ -440,6 +476,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 @@ -495,6 +563,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 @@ -531,6 +611,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 @@ -585,7 +677,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques

// Handle request inflation
HttpFields httpFields = baseRequest.getHttpFields();
boolean inflated = _inflateBufferSize > 0 && httpFields.contains(HttpHeader.CONTENT_ENCODING, "gzip");
boolean inflated = _inflateBufferSize > 0 && httpFields.contains(HttpHeader.CONTENT_ENCODING, "gzip") && isPathInflatable(path);
if (inflated)
{
if (LOG.isDebugEnabled())
Expand Down Expand Up @@ -750,6 +842,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 @@ -799,6 +905,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 of supported {@link DispatcherType} that this filter will operate on.
*
Expand Down Expand Up @@ -861,6 +981,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 @@ -40,6 +40,7 @@
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.http.CompressedContentFormat;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.server.HttpOutput;
import org.eclipse.jetty.server.LocalConnector;
Expand All @@ -55,6 +56,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 @@ -688,6 +690,49 @@ public void testIncludeGzipHandler() throws Exception
assertEquals(__icontent, testOut.toString("UTF8"));
}

@Test
public void testIncludeExcludeGzipHandlerInflate() throws Exception
{
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 5c760ae

Please sign in to comment.