Merge pull request #7873 from eclipse/jetty-10.0.x-4414-gzipInflationExclusion

Issue #4414 - add option to exclude paths from GzipHandler request inflation
This commit is contained in:
Lachlan 2022-05-09 15:58:19 +10:00 committed by GitHub
commit 7a5ea2bac0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 180 additions and 1 deletions

View File

@ -168,6 +168,7 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
// non-static, as other GzipHandler instances may have different configurations // non-static, as other GzipHandler instances may have different configurations
private final IncludeExclude<String> _methods = new IncludeExclude<>(); private final IncludeExclude<String> _methods = new IncludeExclude<>();
private final IncludeExclude<String> _paths = new IncludeExclude<>(PathSpecSet.class); 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 final IncludeExclude<String> _mimeTypes = new IncludeExclude<>(AsciiLowerCaseSet.class);
private HttpField _vary = GzipHttpOutputInterceptor.VARY_ACCEPT_ENCODING; private HttpField _vary = GzipHttpOutputInterceptor.VARY_ACCEPT_ENCODING;
@ -354,6 +355,41 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
} }
} }
/**
* 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. * Adds included HTTP Methods (eg: POST, PATCH, DELETE) for filtering.
* *
@ -440,6 +476,38 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
} }
} }
/**
* 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 @Override
public DeflaterPool.Entry getDeflaterEntry(Request request, long contentLength) public DeflaterPool.Entry getDeflaterEntry(Request request, long contentLength)
{ {
@ -495,6 +563,18 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
return excluded.toArray(new String[0]); 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 * Get the current filter list of included HTTP Methods
* *
@ -531,6 +611,18 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
return includes.toArray(new String[0]); 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 * Get the minimum size, in bytes, that a response {@code Content-Length} must be
* before compression will trigger. * before compression will trigger.
@ -585,7 +677,7 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
// Handle request inflation // Handle request inflation
HttpFields httpFields = baseRequest.getHttpFields(); 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 (inflated)
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
@ -750,6 +842,20 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
return _paths.test(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) * Set the excluded filter list of HTTP methods (replacing any previously set)
* *
@ -799,6 +905,20 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
_paths.exclude(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. * Set of supported {@link DispatcherType} that this filter will operate on.
* *
@ -861,6 +981,20 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
_paths.include(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. * Set the minimum response size to trigger dynamic compression.
* <p> * <p>

View File

@ -40,6 +40,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.CompressedContentFormat; import org.eclipse.jetty.http.CompressedContentFormat;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.server.HttpOutput; import org.eclipse.jetty.server.HttpOutput;
import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.LocalConnector;
@ -55,6 +56,7 @@ import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.hamcrest.Matchers.equalToIgnoringCase;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
@ -688,6 +690,49 @@ public class GzipHandlerTest
assertEquals(__icontent, testOut.toString("UTF8")); 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 @Test
public void testAddGetPaths() public void testAddGetPaths()
{ {