Splitting mimeTypes and httpMethod into compress/decompress configs

This commit is contained in:
Joakim Erdfelt 2024-09-25 16:37:59 -05:00
parent ba3e84aae6
commit 38d501a698
No known key found for this signature in database
GPG Key ID: 2D0E1FB8FE4B68B4
3 changed files with 362 additions and 243 deletions

View File

@ -19,7 +19,6 @@ import java.util.Set;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.http.pathmap.PathSpecSet;
import org.eclipse.jetty.server.Request;
@ -34,30 +33,50 @@ import org.eclipse.jetty.util.component.AbstractLifeCycle;
public class CompressionConfig extends AbstractLifeCycle
{
/**
* Set of `Content-Encoding` encodings that are supported by this configuration.
*/
private final IncludeExcludeSet<String, String> decompressEncodings;
/**
* Set of `Accept-Encoding` encodings that are supported by this configuration.
* Set of {@code Accept-Encoding} encodings that are supported for compressing Response content.
*/
private final IncludeExcludeSet<String, String> compressEncodings;
/**
* Set of HTTP Methods that are supported by this configuration.
* Set of {@code Content-Encoding} encodings that are supported for decompressing Request content.
*/
private final IncludeExcludeSet<String, String> decompressEncodings;
/**
* Set of HTTP Methods that are supported for compressing Response content.
*/
private final IncludeExcludeSet<String, String> compressMethods;
/**
* Set of HTTP Methods that are supported for decompressing Request content.
*/
private final IncludeExcludeSet<String, String> decompressMethods;
/**
* Mime-Types that support decompressing of Request content.
*/
private final IncludeExcludeSet<String, String> compressMimeTypes;
/**
* Mime-Types that support compressing Response content.
*/
private final IncludeExcludeSet<String, String> decompressMimeTypes;
/**
* Set of paths that support compressing Response content.
*/
private final IncludeExcludeSet<String, String> methods;
private final IncludeExcludeSet<String, String> mimetypes;
private final IncludeExcludeSet<String, String> decompressPaths;
private final IncludeExcludeSet<String, String> compressPaths;
/**
* Set of paths that support decompressing Request content.
*/
private final IncludeExcludeSet<String, String> decompressPaths;
private final HttpField vary;
private CompressionConfig(Builder builder)
{
this.decompressEncodings = builder.decompressEncodings.asImmutable();
this.compressEncodings = builder.compressEncodings.asImmutable();
this.methods = builder.methods.asImmutable();
this.mimetypes = builder.mimetypes.asImmutable();
this.decompressPaths = builder.decompressPaths.asImmutable();
this.decompressEncodings = builder.decompressEncodings.asImmutable();
this.compressMethods = builder.decompressMethods.asImmutable();
this.decompressMethods = builder.decompressMethods.asImmutable();
this.compressMimeTypes = builder.compressMimeTypes.asImmutable();
this.decompressMimeTypes = builder.decompressMimeTypes.asImmutable();
this.compressPaths = builder.compressPaths.asImmutable();
this.decompressPaths = builder.decompressPaths.asImmutable();
this.vary = builder.vary;
}
@ -66,6 +85,58 @@ public class CompressionConfig extends AbstractLifeCycle
return new Builder();
}
/**
* Get the set of excluded HTTP methods for Response compression.
*
* @return the set of excluded HTTP methods
* @see #getCompressMethodIncludes()
*/
@ManagedAttribute("Set of HTTP Method Exclusions")
public Set<String> getCompressMethodExcludes()
{
Set<String> excluded = compressMethods.getExcluded();
return Collections.unmodifiableSet(excluded);
}
/**
* Get the set of included HTTP methods for Response compression
*
* @return the set of included HTTP methods
* @see #getCompressMethodExcludes()
*/
@ManagedAttribute("Set of HTTP Method Inclusions")
public Set<String> getCompressMethodIncludes()
{
Set<String> includes = compressMethods.getIncluded();
return Collections.unmodifiableSet(includes);
}
/**
* Get the set of excluded MIME types for Response compression.
*
* @return the set of excluded MIME types
* @see #getCompressMimeTypeIncludes()
*/
@ManagedAttribute("Set of Mime-Types Excluded from Response compression")
public Set<String> getCompressMimeTypeExcludes()
{
Set<String> excluded = compressMimeTypes.getExcluded();
return Collections.unmodifiableSet(excluded);
}
/**
* Get the set of included MIME types for Response compression.
*
* @return the filter list of included MIME types
* @see #getCompressMimeTypeExcludes()
*/
@ManagedAttribute("Set of Mime-Types Included in Response compression")
public Set<String> getCompressMimeTypeIncludes()
{
Set<String> includes = compressMimeTypes.getIncluded();
return Collections.unmodifiableSet(includes);
}
/**
* Get the set of excluded Path Specs for response compression.
*
@ -99,7 +170,6 @@ public class CompressionConfig extends AbstractLifeCycle
String matchedEncoding = null;
// TODO: add testcase for `Accept-Encoding: *`
for (String encoding : requestAcceptEncoding)
{
if (compressEncodings.test(encoding))
@ -111,7 +181,7 @@ public class CompressionConfig extends AbstractLifeCycle
if (matchedEncoding == null)
return null;
if (!isMethodSupported(request.getMethod()))
if (!compressMimeTypes.test(request.getMethod()))
return null;
if (!compressPaths.test(pathInContext))
@ -120,6 +190,32 @@ public class CompressionConfig extends AbstractLifeCycle
return matchedEncoding;
}
/**
* Get the set of excluded HTTP methods for Request decompression.
*
* @return the set of excluded HTTP methods
* @see #getDecompressMethodIncludes()
*/
@ManagedAttribute("Set of HTTP Method Exclusions")
public Set<String> getDecompressMethodExcludes()
{
Set<String> excluded = decompressMethods.getExcluded();
return Collections.unmodifiableSet(excluded);
}
/**
* Get the set of included HTTP methods for Request decompression
*
* @return the set of included HTTP methods
* @see #getDecompressMethodExcludes()
*/
@ManagedAttribute("Set of HTTP Method Inclusions")
public Set<String> getDecompressMethodIncludes()
{
Set<String> includes = decompressMethods.getIncluded();
return Collections.unmodifiableSet(includes);
}
/**
* Get the set of excluded Path Specs for request decompression.
*
@ -153,68 +249,16 @@ public class CompressionConfig extends AbstractLifeCycle
if (decompressEncodings.test(requestContentEncoding))
matchedEncoding = requestContentEncoding;
if (!isMethodSupported(request.getMethod()))
if (!decompressMethods.test(request.getMethod()))
return null;
// TODO: testing mime-type is really a response test, not a request test.
if (!isMimeTypeCompressible(request.getContext().getMimeTypes(), pathInContext))
String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
if (!decompressMimeTypes.test(contentType))
return null;
return matchedEncoding;
}
/**
* Get the set of excluded HTTP methods
*
* @return the set of excluded HTTP methods
* @see #getMethodIncludes()
*/
@ManagedAttribute("Set of HTTP Method Exclusions")
public Set<String> getMethodExcludes()
{
Set<String> excluded = methods.getExcluded();
return Collections.unmodifiableSet(excluded);
}
/**
* Get the set of included HTTP methods
*
* @return the set of included HTTP methods
* @see #getMethodExcludes()
*/
@ManagedAttribute("Set of HTTP Method Inclusions")
public Set<String> getMethodIncludes()
{
Set<String> includes = methods.getIncluded();
return Collections.unmodifiableSet(includes);
}
/**
* Get the set of excluded MIME types
*
* @return the set of excluded MIME types
* @see #getMimeTypeIncludes()
*/
@ManagedAttribute("Set of Mime Type Exclusions")
public Set<String> getMimeTypeExcludes()
{
Set<String> excluded = mimetypes.getExcluded();
return Collections.unmodifiableSet(excluded);
}
/**
* Get the set of included MIME types
*
* @return the filter list of included MIME types
* @see #getMimeTypeExcludes()
*/
@ManagedAttribute("Set of Mime Type Inclusions")
public Set<String> getMimeTypeIncludes()
{
Set<String> includes = mimetypes.getIncluded();
return Collections.unmodifiableSet(includes);
}
/**
* @return The VARY field to use.
*/
@ -223,26 +267,24 @@ public class CompressionConfig extends AbstractLifeCycle
return vary;
}
public boolean isMethodSupported(String method)
public boolean isCompressMethodSupported(String method)
{
return methods.test(method);
return compressMethods.test(method);
}
public boolean isMimeTypeCompressible(MimeTypes mimeTypes, String pathInContext)
public boolean isCompressMimeTypeSupported(String mimeType)
{
// Exclude non-compressible mime-types known from URI extension
String mimeType = mimeTypes.getMimeByExtension(pathInContext);
if (mimeType != null)
{
mimeType = HttpField.getValueParameters(mimeType, null);
return isMimeTypeCompressible(mimeType);
}
return true;
return compressMimeTypes.test(mimeType);
}
public boolean isMimeTypeCompressible(String mimeType)
public boolean isDecompressMethodSupported(String method)
{
return mimetypes.test(mimeType);
return decompressMethods.test(method);
}
public boolean isDecompressMimeTypeSupported(String mimeType)
{
return decompressMimeTypes.test(mimeType);
}
/**
@ -274,18 +316,37 @@ public class CompressionConfig extends AbstractLifeCycle
public static class Builder
{
/**
* Set of `Content-Encoding` encodings that are supported by this configuration.
* Set of {@code Content-Encoding} encodings that are supported for decompressing Request content.
*/
private final IncludeExclude<String> decompressEncodings = new IncludeExclude<>();
/**
* Set of `Accept-Encoding` encodings that are supported by this configuration.
* Set of {@code Accept-Encoding} encodings that are supported for compressing Response content.
*/
private final IncludeExclude<String> compressEncodings = new IncludeExclude<>();
private final IncludeExclude<String> methods = new IncludeExclude<>();
private final IncludeExclude<String> mimetypes = new IncludeExclude<>(AsciiLowerCaseSet.class);
/**
* Set of HTTP Methods that are supported for decompressing Request content.
*/
private final IncludeExclude<String> decompressMethods = new IncludeExclude<>();
/**
* Set of HTTP Methods that are supported for compressing Response content.
*/
private final IncludeExclude<String> compressMethods = new IncludeExclude<>();
/**
* Set of paths that support decompressing of Request content.
*/
private final IncludeExclude<String> decompressPaths = new IncludeExclude<>(PathSpecSet.class);
/**
* Set of paths that support compressing Response content.
*/
private final IncludeExclude<String> compressPaths = new IncludeExclude<>(PathSpecSet.class);
/**
* Mime-Types that support decompressing of Request content.
*/
private final IncludeExclude<String> compressMimeTypes = new IncludeExclude<>(AsciiLowerCaseSet.class);
/**
* Mime-Types that support compressing Response content.
*/
private final IncludeExclude<String> decompressMimeTypes = new IncludeExclude<>(AsciiLowerCaseSet.class);
private HttpField vary = new PreEncodedHttpField(HttpHeader.VARY, HttpHeader.ACCEPT_ENCODING.asString());
public CompressionConfig build()
@ -317,6 +378,62 @@ public class CompressionConfig extends AbstractLifeCycle
return this;
}
/**
* An HTTP method to exclude for Response compression.
*
* @param method the method to exclude
* @return this builder
*/
public Builder compressMethodExclude(String method)
{
this.compressMethods.exclude(method);
return this;
}
/**
* An HTTP method to include for Response compression.
*
* @param method the method to include
* @return this builder
*/
public Builder compressMethodInclude(String method)
{
this.compressMethods.include(method);
return this;
}
/**
* A non-compressible mimetype to exclude for Response compression.
*
* <p>
* The response {@code Content-Type} is evaluated.
* </p>
*
* @param mimetype the mimetype to exclude
* @return this builder
*/
public Builder compressMimeTypeExclude(String mimetype)
{
this.compressMimeTypes.exclude(mimetype);
return this;
}
/**
* A compressible mimetype to include for Response compression.
*
* <p>
* The response {@code Content-Type} is evaluated.
* </p>
*
* @param mimetype the mimetype to include
* @return this builder
*/
public Builder compressMimeTypeInclude(String mimetype)
{
this.compressMimeTypes.include(mimetype);
return this;
}
/**
* A path that does not supports response content compression.
*
@ -377,6 +494,62 @@ public class CompressionConfig extends AbstractLifeCycle
return this;
}
/**
* An HTTP method to exclude for Request decompression.
*
* @param method the method to exclude
* @return this builder
*/
public Builder decompressMethodExclude(String method)
{
this.decompressMethods.exclude(method);
return this;
}
/**
* An HTTP method to include for Request decompression.
*
* @param method the method to include
* @return this builder
*/
public Builder decompressMethodInclude(String method)
{
this.decompressMethods.include(method);
return this;
}
/**
* A non-compressed mimetype to exclude for Request decompression.
*
* <p>
* The Request {@code Content-Type} is evaluated.
* </p>
*
* @param mimetype the mimetype to exclude
* @return this builder
*/
public Builder decompressMimeTypeExclude(String mimetype)
{
this.decompressMimeTypes.exclude(mimetype);
return this;
}
/**
* A compressed mimetype to include for Request decompression.
*
* <p>
* The request {@code Content-Type} is evaluated.
* </p>
*
* @param mimetype the mimetype to include
* @return this builder
*/
public Builder decompressMimeTypeInclude(String mimetype)
{
this.decompressMimeTypes.include(mimetype);
return this;
}
/**
* A path that does not support request content decompression.
*
@ -421,72 +594,18 @@ public class CompressionConfig extends AbstractLifeCycle
*/
public Builder from(CompressionConfig config)
{
this.decompressEncodings.addAll(config.decompressEncodings);
this.decompressPaths.addAll(config.decompressPaths);
this.compressEncodings.addAll(config.compressEncodings);
this.decompressEncodings.addAll(config.decompressEncodings);
this.compressMethods.addAll(config.compressMethods);
this.decompressMethods.addAll(config.decompressMethods);
this.compressMimeTypes.addAll(config.compressMimeTypes);
this.decompressMimeTypes.addAll(config.decompressMimeTypes);
this.compressPaths.addAll(config.compressPaths);
this.methods.addAll(config.methods);
this.mimetypes.addAll(config.mimetypes);
this.decompressPaths.addAll(config.decompressPaths);
this.vary = config.vary;
return this;
}
/**
* An HTTP method to exclude.
*
* @param method the method to exclude
* @return this builder
*/
public Builder methodExclude(String method)
{
this.methods.exclude(method);
return this;
}
/**
* An HTTP method to include.
*
* @param method the method to include
* @return this builder
*/
public Builder methodInclude(String method)
{
this.methods.include(method);
return this;
}
/**
* A non-compressible mimetype to exclude.
*
* <p>
* The response {@code Content-Type} is evaluated.
* </p>
*
* @param mimetype the mimetype to exclude
* @return this builder
*/
public Builder mimeTypeExclude(String mimetype)
{
this.mimetypes.exclude(mimetype);
return this;
}
/**
* A compressible mimetype to include.
*
* <p>
* The response {@code Content-Type} is evaluated.
* </p>
*
* @param mimetype the mimetype to include
* @return this builder
*/
public Builder mimeTypeInclude(String mimetype)
{
this.mimetypes.include(mimetype);
return this;
}
/**
* Specify the Response {@code Vary} header field to use.
*

View File

@ -94,7 +94,7 @@ public class CompressionResponse extends Response.Wrapper implements Callback, I
else
{
String mimeType = MimeTypes.getContentTypeWithoutCharset(contentTypeField.getValue());
if (config.isMimeTypeCompressible(mimeType))
if (config.isCompressMimeTypeSupported(mimeType))
{
compressing = state.compareAndSet(State.MIGHT_COMPRESS, State.COMPRESSING);
}

View File

@ -350,7 +350,91 @@ public class CompressionHandlerTest extends AbstractCompressionTest
/**
* Testing how CompressionHandler acts with a single compression implementation added.
* Configuration is only using {@code methods} excluding {@code PUT}, and including both
* Configuration is only using {@code compressMimeTypes} excluding {@code image/png}, and including both
* {@code text/plain} and {@code image/svg+xml}
*/
@ParameterizedTest
@CsvSource(textBlock = """
# type, resourceName, resourceContentType, requestedPath, expectedIsCompressed
br, texts/quotes.txt, text/plain;charset=utf-8, /path/to/quotes.txt, true
br, texts/logo.svg, image/svg+xml, /path/to/logo.svg, true
br, texts/long.txt, text/plain;charset=utf-8, /path/to/long.txt, true
br, images/logo.png, image/png, /images/logo.png, false
br, images/logo.png, image/png, /path/deep/images/logo.png, false
zstandard, texts/quotes.txt, text/plain;charset=utf-8, /path/to/quotes.txt, true
zstandard, texts/logo.svg, image/svg+xml, /path/to/logo.svg, true
zstandard, texts/long.txt, text/plain;charset=utf-8, /path/to/long.txt, true
zstandard, images/logo.png, image/png, /images/logo.png, false
zstandard, images/logo.png, image/png, /path/deep/images/logo.png, false
gzip, texts/quotes.txt, text/plain;charset=utf-8, /path/to/quotes.txt, true
gzip, texts/logo.svg, image/svg+xml, /path/to/logo.svg, true
gzip, texts/long.txt, text/plain;charset=utf-8, /path/to/long.txt, true
gzip, images/logo.png, image/png, /images/logo.png, false
gzip, images/logo.png, image/png, /path/deep/images/logo.png, false
""")
public void testCompressMimeTypesConfig(String compressionType,
String resourceName,
String resourceContentType,
String requestedPath,
boolean expectedIsCompressed) throws Exception
{
newCompression(compressionType);
Path resourcePath = MavenPaths.findTestResourceFile(resourceName);
byte[] resourceBody = Files.readAllBytes(resourcePath);
CompressionHandler compressionHandler = new CompressionHandler();
compressionHandler.addCompression(compression);
CompressionConfig config = CompressionConfig.builder()
.compressMimeTypeInclude("text/plain")
.compressMimeTypeInclude("image/svg+xml")
.compressMimeTypeExclude("image/png")
.build();
compressionHandler.putConfiguration("/", config);
compressionHandler.setHandler(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.setStatus(200);
response.getHeaders().put(HttpHeader.CONTENT_TYPE, resourceContentType);
response.write(true, ByteBuffer.wrap(resourceBody), callback);
return true;
}
});
startServer(compressionHandler);
URI serverURI = server.getURI();
client.getContentDecoderFactories().clear();
ContentResponse response = client.newRequest(serverURI.getHost(), serverURI.getPort())
.method(HttpMethod.GET)
.headers((headers) ->
{
headers.put(HttpHeader.ACCEPT_ENCODING, compression.getEncodingName());
})
.path(requestedPath)
.send();
dumpResponse(response);
assertThat(response.getStatus(), is(200));
if (expectedIsCompressed)
{
assertThat(response.getHeaders().get(HttpHeader.CONTENT_ENCODING), is(compression.getEncodingName()));
byte[] content = decompress(response.getContent());
assertThat(content, is(resourceBody));
}
else
{
assertFalse(response.getHeaders().contains(HttpHeader.CONTENT_ENCODING));
byte[] content = response.getContent();
assertThat(content, is(resourceBody));
}
}
/**
* Testing how CompressionHandler acts with a single compression implementation added.
* Configuration is only using {@code decompressMethods} excluding {@code PUT}, and including both
* {@code GET} and {@code POST}
*/
@ParameterizedTest
@ -366,11 +450,11 @@ public class CompressionHandlerTest extends AbstractCompressionTest
gzip, texts/logo.svg, image/svg+xml, POST, /post/to/
gzip, texts/long.txt, text/plain;charset=utf-8, PUT, /put/to/
""")
public void testMethodsConfig(String compressionType,
String resourceName,
String resourceContentType,
String requestMethod,
String requestedPath) throws Exception
public void testDecompressMethodsConfig(String compressionType,
String resourceName,
String resourceContentType,
String requestMethod,
String requestedPath) throws Exception
{
newCompression(compressionType);
Path resourcePath = MavenPaths.findTestResourceFile(resourceName);
@ -379,9 +463,9 @@ public class CompressionHandlerTest extends AbstractCompressionTest
CompressionHandler compressionHandler = new CompressionHandler();
compressionHandler.addCompression(compression);
CompressionConfig config = CompressionConfig.builder()
.methodInclude("GET")
.methodInclude("POST")
.methodExclude("PUT")
.decompressMethodInclude("GET")
.decompressMethodInclude("POST")
.decompressMethodExclude("PUT")
.compressEncodingExclude(compression.getEncodingName()) // don't compress the responses
.build();
@ -479,90 +563,6 @@ public class CompressionHandlerTest extends AbstractCompressionTest
}
}
/**
* Testing how CompressionHandler acts with a single compression implementation added.
* Configuration is only using {@code mimeTypes} excluding {@code image/png}, and including both
* {@code text/plain} and {@code image/svg+xml}
*/
@ParameterizedTest
@CsvSource(textBlock = """
# type, resourceName, resourceContentType, requestedPath, expectedIsCompressed
br, texts/quotes.txt, text/plain;charset=utf-8, /path/to/quotes.txt, true
br, texts/logo.svg, image/svg+xml, /path/to/logo.svg, true
br, texts/long.txt, text/plain;charset=utf-8, /path/to/long.txt, true
br, images/logo.png, image/png, /images/logo.png, false
br, images/logo.png, image/png, /path/deep/images/logo.png, false
zstandard, texts/quotes.txt, text/plain;charset=utf-8, /path/to/quotes.txt, true
zstandard, texts/logo.svg, image/svg+xml, /path/to/logo.svg, true
zstandard, texts/long.txt, text/plain;charset=utf-8, /path/to/long.txt, true
zstandard, images/logo.png, image/png, /images/logo.png, false
zstandard, images/logo.png, image/png, /path/deep/images/logo.png, false
gzip, texts/quotes.txt, text/plain;charset=utf-8, /path/to/quotes.txt, true
gzip, texts/logo.svg, image/svg+xml, /path/to/logo.svg, true
gzip, texts/long.txt, text/plain;charset=utf-8, /path/to/long.txt, true
gzip, images/logo.png, image/png, /images/logo.png, false
gzip, images/logo.png, image/png, /path/deep/images/logo.png, false
""")
public void testMimeTypesConfig(String compressionType,
String resourceName,
String resourceContentType,
String requestedPath,
boolean expectedIsCompressed) throws Exception
{
newCompression(compressionType);
Path resourcePath = MavenPaths.findTestResourceFile(resourceName);
byte[] resourceBody = Files.readAllBytes(resourcePath);
CompressionHandler compressionHandler = new CompressionHandler();
compressionHandler.addCompression(compression);
CompressionConfig config = CompressionConfig.builder()
.mimeTypeInclude("text/plain")
.mimeTypeInclude("image/svg+xml")
.mimeTypeExclude("image/png")
.build();
compressionHandler.putConfiguration("/", config);
compressionHandler.setHandler(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.setStatus(200);
response.getHeaders().put(HttpHeader.CONTENT_TYPE, resourceContentType);
response.write(true, ByteBuffer.wrap(resourceBody), callback);
return true;
}
});
startServer(compressionHandler);
URI serverURI = server.getURI();
client.getContentDecoderFactories().clear();
ContentResponse response = client.newRequest(serverURI.getHost(), serverURI.getPort())
.method(HttpMethod.GET)
.headers((headers) ->
{
headers.put(HttpHeader.ACCEPT_ENCODING, compression.getEncodingName());
})
.path(requestedPath)
.send();
dumpResponse(response);
assertThat(response.getStatus(), is(200));
if (expectedIsCompressed)
{
assertThat(response.getHeaders().get(HttpHeader.CONTENT_ENCODING), is(compression.getEncodingName()));
byte[] content = decompress(response.getContent());
assertThat(content, is(resourceBody));
}
else
{
assertFalse(response.getHeaders().contains(HttpHeader.CONTENT_ENCODING));
byte[] content = response.getContent();
assertThat(content, is(resourceBody));
}
}
private void dumpResponse(org.eclipse.jetty.client.Response response)
{
System.out.printf(" %s %d %s%n", response.getVersion(), response.getStatus(), response.getReason());