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 c175d7e85fc1..61ade98f2267 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 @@ -178,6 +178,7 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory private final IncludeExclude _agentPatterns = new IncludeExclude<>(RegexSet.class); private final IncludeExclude _methods = new IncludeExclude<>(); private final IncludeExclude _paths = new IncludeExclude<>(PathSpecSet.class); + private final IncludeExclude _inflationPaths = new IncludeExclude<>(PathSpecSet.class); private final IncludeExclude _mimeTypes = new IncludeExclude<>(); private HttpField _vary; @@ -320,6 +321,41 @@ public void addExcludedPaths(String... pathspecs) } } + /** + * Adds excluded Path Specs for request inflation filtering. + * + *

+ * 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 addExcludedInflatePaths(String... pathspecs) + { + for (String p: pathspecs) + { + _inflationPaths.exclude(StringUtil.csvSplit(p)); + } + } + /** * Adds included User-Agents for filtering. * @@ -417,6 +453,38 @@ public void addIncludedPaths(String... pathspecs) } } + /** + * Add included Path Specs for inflation filtering. + * + *

+ * 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) + { + _inflationPaths.include(StringUtil.csvSplit(p)); + } + } + @Override protected void doStart() throws Exception { @@ -514,6 +582,18 @@ public String[] getExcludedPaths() return excluded.toArray(new String[excluded.size()]); } + /** + * Get the current filter list of excluded Path Specs + * + * @return the filter list of excluded Path Specs + * @see #getIncludedInflationPaths() + */ + public String[] getExcludedInflationPaths() + { + Set excluded = _inflationPaths.getExcluded(); + return excluded.toArray(new String[excluded.size()]); + } + /** * Get the current filter list of included User-Agent patterns * @@ -562,6 +642,18 @@ public String[] getIncludedPaths() return includes.toArray(new String[includes.size()]); } + /** + * Get the current filter list of included inflation Path Specs + * + * @return the filter list of included inflation Path Specs + * @see #getExcludedInflationPaths() + */ + public String[] getIncludedInflationPaths() + { + Set includes = _inflationPaths.getIncluded(); + return includes.toArray(new String[includes.size()]); + } + /** * Get the current filter list of included HTTP methods * @@ -627,7 +719,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques } // Handle request inflation - if (_inflateBufferSize > 0) + if (_inflateBufferSize > 0 && isPathInflatable(path)) { boolean inflate = false; for (ListIterator i = baseRequest.getHttpFields().listIterator(); i.hasNext(); ) @@ -814,6 +906,23 @@ protected boolean isPathGzipable(String requestURI) return _paths.test(requestURI); } + /** + * Test if the provided Request URI is allowed based on the Path Specs inflation filters. + * + * @param requestURI the request uri + * @return whether inflation is allowed for the given the path + */ + protected boolean isPathInflatable(String requestURI) + { + if (_inflationPaths.isEmpty()) + return true; + + if (requestURI == null) + return true; + + return _inflationPaths.test(requestURI); + } + @Override public void recycle(Deflater deflater) { 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 e7c3c6118c08..9bbdbb38aa4e 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 @@ -64,6 +64,7 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; @SuppressWarnings("serial") public class GzipHandlerTest @@ -105,6 +106,9 @@ public void init() throws Exception gzipHandler.setMinGzipSize(16); gzipHandler.setInflateBufferSize(4096); + gzipHandler.addIncludedMethods("POST"); + gzipHandler.addExcludedInflatePaths("/ctx/noinflation"); + ServletContextHandler context = new ServletContextHandler(gzipHandler, "/ctx"); ServletHandler servlets = context.getServletHandler(); @@ -119,11 +123,28 @@ public void init() throws Exception servlets.addServletWithMapping(DumpServlet.class, "/dump/*"); servlets.addServletWithMapping(AsyncServlet.class, "/async/*"); servlets.addServletWithMapping(BufferServlet.class, "/buffer/*"); + servlets.addServletWithMapping(NoInflationServlet.class, "/noinflation"); servlets.addFilterWithMapping(CheckFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); _server.start(); } + public static class NoInflationServlet extends HttpServlet + { + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + try(InputStream ignored = new GZIPInputStream(request.getInputStream())) + { + response.setStatus(200); + IO.copy(ignored, response.getOutputStream()); + response.flushBuffer(); + } catch (IOException ignore) { + fail("The inputstream was already inflated. addExcludedInflatePaths does not seem to be effective."); + } + } + } + public static class MicroServlet extends HttpServlet { @Override @@ -676,6 +697,13 @@ public void testAddGetPaths() String[] includedPaths = gzip.getIncludedPaths(); assertThat("Included Paths.size", includedPaths.length, is(2)); assertThat("Included Paths", Arrays.asList(includedPaths), contains("/foo", "^/bar.*$")); + + gzip.addIncludedInflationPaths("/fiz"); + gzip.addIncludedInflationPaths("^/buz.*$"); + + String[] includedInflationPaths = gzip.getIncludedInflationPaths(); + assertThat("Included Paths.size", includedInflationPaths.length, is(2)); + assertThat("Included Paths", Arrays.asList(includedInflationPaths), contains("/fiz", "^/buz.*$")); } @Test @@ -801,6 +829,32 @@ public void testGzipBomb() throws Exception assertThat(response.getContentBytes().length, is(512 * 1024)); } + @Test + public void testExcludeInflationButShouldBeZipped() throws Exception + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + GZIPOutputStream output = new GZIPOutputStream(baos); + output.write(__content.getBytes(StandardCharsets.UTF_8)); + output.close(); + byte[] bytes = baos.toByteArray(); + + // generated and parsed test + HttpTester.Request request = HttpTester.newRequest(); + HttpTester.Response response; + + request.setMethod("POST"); + request.setURI("/ctx/noinflation"); + request.setVersion("HTTP/1.0"); + request.setHeader("Host", "tester"); + request.setHeader("Content-Type", "text/plain"); + request.setHeader("Content-Encoding", "gzip"); + request.setContent(bytes); + + response = HttpTester.parseResponse(_connector.getResponse(request.generate())); + assertThat(response.getStatus(), is(200)); + assertThat(response.getContent(), is(__content)); + } + public static class CheckFilter implements Filter { @Override