diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java index 9a8418dcd482..82941267ffe7 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java @@ -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 _methods = new IncludeExclude<>(); + private final IncludeExclude _inflatePaths = new IncludeExclude<>(PathSpecSet.class); private final IncludeExclude _paths = new IncludeExclude<>(PathSpecSet.class); private final IncludeExclude _mimeTypes = new IncludeExclude<>(AsciiLowerCaseSet.class); private HttpField _vary = GzipResponse.VARY_ACCEPT_ENCODING; @@ -213,6 +214,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. * @@ -299,6 +335,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) { @@ -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 excluded = _inflatePaths.getExcluded(); + return excluded.toArray(new String[0]); + } + /** * Get the current filter list of included HTTP Methods * @@ -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 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. @@ -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? @@ -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) * @@ -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) * @@ -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. *

diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/gzip/GzipHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/gzip/GzipHandlerTest.java index 6ca07a84973f..c2a037861b12 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/gzip/GzipHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/gzip/GzipHandlerTest.java @@ -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; @@ -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() {