Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #4414 - add option to exclude paths from GzipHandler request inflation #7873

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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