From 5c760ae5a3970745c7107a73ccdf4b7271da0a5e Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 12 Apr 2022 14:53:53 +1000 Subject: [PATCH] Issue #4414 - add option to exclude paths from GzipHandler request inflation. Signed-off-by: Lachlan Roberts --- .../server/handler/gzip/GzipHandler.java | 136 +++++++++++++++++- .../jetty/servlet/GzipHandlerTest.java | 45 ++++++ 2 files changed, 180 insertions(+), 1 deletion(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java index a65547806b89..65becfe88bd6 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java @@ -168,6 +168,7 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory // non-static, as other GzipHandler instances may have different configurations private final IncludeExclude _methods = new IncludeExclude<>(); private final IncludeExclude _paths = new IncludeExclude<>(PathSpecSet.class); + private final IncludeExclude _inflatePaths = new IncludeExclude<>(PathSpecSet.class); private final IncludeExclude _mimeTypes = new IncludeExclude<>(AsciiLowerCaseSet.class); private HttpField _vary = GzipHttpOutputInterceptor.VARY_ACCEPT_ENCODING; @@ -354,6 +355,41 @@ public void addExcludedPaths(String... pathspecs) } } + /** + * Adds excluded Path Specs for request filtering on request inflation. + * + *

+ * There are 2 syntaxes supported, Servlet url-pattern 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. + *

    + *
  • If the spec starts with '^' the spec is assumed to be + * a regex based path spec and will match with normal Java regex rules.
  • + *
  • If the spec starts with '/' then spec is assumed to be + * a Servlet url-pattern rules path spec for either an exact match + * or prefix based match.
  • + *
  • If the spec starts with '*.' then spec is assumed to be + * a Servlet url-pattern rules path spec for a suffix based match.
  • + *
  • All other syntaxes are unsupported
  • + *
+ *

+ * 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.
+ * 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. * @@ -440,6 +476,38 @@ public void addIncludedPaths(String... pathspecs) } } + /** + * Add included Path Specs for filtering on request inflation. + * + *

+ * There are 2 syntaxes supported, Servlet url-pattern 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. + *

    + *
  • If the spec starts with '^' the spec is assumed to be + * a regex based path spec and will match with normal Java regex rules.
  • + *
  • If the spec starts with '/' then spec is assumed to be + * a Servlet url-pattern rules path spec for either an exact match + * or prefix based match.
  • + *
  • If the spec starts with '*.' then spec is assumed to be + * a Servlet url-pattern rules path spec for a suffix based match.
  • + *
  • All other syntaxes are unsupported
  • + *
+ *

+ * 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) { @@ -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 excluded = _inflatePaths.getExcluded(); + return excluded.toArray(new String[0]); + } + /** * Get the current filter list of included HTTP Methods * @@ -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 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. @@ -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()) @@ -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) * @@ -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. * @@ -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. *

diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerTest.java index e754fe0784e4..e71eed85a187 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerTest.java @@ -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; @@ -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; @@ -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() {