Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 91 additions & 23 deletions java/org/apache/coyote/CompressionConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,10 @@

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.*;
import java.util.regex.Pattern;

import org.apache.coyote.http11.filters.GzipOutputFilter;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.buf.MessageBytes;
Expand All @@ -47,7 +43,11 @@ public class CompressionConfig {
"text/javascript,application/javascript,application/json,application/xml";
private String[] compressibleMimeTypes = null;
private int compressionMinSize = 2048;

private Set<String> noCompressionEncodings = new HashSet<String>(Arrays.asList(
"br", "compress", "dcb", "dcz", "deflate", "gzip", "pack200-gzip", "zstd"
));
private int gzipLevel = -1;
private int gzipBufferSize = GzipOutputFilter.DEFAULT_BUFFER_SIZE;

/**
* Set compression level.
Expand Down Expand Up @@ -162,6 +162,63 @@ public int getCompressionMinSize() {
return compressionMinSize;
}

/**
* Set the compression level for gzip.
* @param gzipLevel The compression level. Valid values are -1 (default), or 1-9.
* -1 uses the default compression level.
* 1 gives best speed, 9 gives best compression.
*/
public void setGzipLevel(int gzipLevel) {
if (gzipLevel < -1 || gzipLevel > 9) {
throw new IllegalArgumentException(sm.getString("compressionConfig.invalidGzipLevel", Integer.valueOf(gzipLevel)));
}
this.gzipLevel = gzipLevel;
}

public int getGzipLevel() {
return gzipLevel;
}

/**
* Set the buffer size for gzip compression stream.
*
* @param gzipBufferSize The buffer size in bytes. Must be positive.
*/
public void setGzipBufferSize(int gzipBufferSize) {
if (gzipBufferSize <= 0) {
throw new IllegalArgumentException(sm.getString("compressionConfig.invalidGzipBufferSize", Integer.valueOf(gzipBufferSize)));
}
this.gzipBufferSize = gzipBufferSize;
}

public int getGzipBufferSize() {
return gzipBufferSize;
}

public String getNoCompressionEncodings() {
return String.join(",", noCompressionEncodings);
}

/**
* Set the list of content encodings that indicate already-compressed content.
* When content is already encoded with one of these encodings, compression will not be applied
* to prevent double compression.
*
* @param encodings Comma-separated list of encoding names (e.g., "gzip,br.dflate")
*/
public void setNoCompressionEncodings(String encodings) {
Set<String> newEncodings = new HashSet<String>();
if (encodings != null && !encodings.isEmpty()) {
StringTokenizer tokens = new StringTokenizer(encodings, ",");
while (tokens.hasMoreTokens()) {
String token = tokens.nextToken().trim();
if(!token.isEmpty()) {
newEncodings.add(token);
}
}
}
this.noCompressionEncodings = newEncodings;
}

/**
* Set Minimum size to trigger compression.
Expand All @@ -172,17 +229,30 @@ public void setCompressionMinSize(int compressionMinSize) {
this.compressionMinSize = compressionMinSize;
}

/**
* Determines if gzip compression should be enabled for the given response and if it is, sets any necessary headers to
* mark it as such.
*
* @param request The request that triggered the response
* @param response The response to consider compressing
*
* @return {@code true} if compression was enabled for the given response, otherwise {@code false}
*/
public boolean useCompression(Request request, Response response) {
return this.useCompression(request, response, "gzip");
}

/**
* Determines if compression should be enabled for the given response and if it is, sets any necessary headers to
* mark it as such.
*
* @param request The request that triggered the response
* @param response The response to consider compressing
* @param encoding The compression encoding to use (e.g., "gzip", "br", "deflate", "zstd")
*
* @return {@code true} if compression was enabled for the given response, otherwise {@code false}
*/
public boolean useCompression(Request request, Response response) {
public boolean useCompression(Request request, Response response, String encoding) {
// Check if compression is enabled
if (compressionLevel == 0) {
return false;
Expand Down Expand Up @@ -210,9 +280,7 @@ public boolean useCompression(Request request, Response response) {
if (tokens.contains("identity")) {
// If identity, do not do content modifications
useContentEncoding = false;
} else if (tokens.contains("br") || tokens.contains("compress") || tokens.contains("dcb") ||
tokens.contains("dcz") || tokens.contains("deflate") || tokens.contains("gzip") ||
tokens.contains("pack200-gzip") || tokens.contains("zstd")) {
} else if (noCompressionEncodings.stream().anyMatch(tokens::contains)) {
// Content should not be compressed twice
return false;
}
Expand All @@ -235,9 +303,9 @@ public boolean useCompression(Request request, Response response) {
}

Enumeration<String> headerValues = request.getMimeHeaders().values("TE");
boolean foundGzip = false;
boolean foundEncoding = false;
// TE and accept-encoding seem to have equivalent syntax
while (!foundGzip && headerValues.hasMoreElements()) {
while (!foundEncoding && headerValues.hasMoreElements()) {
List<TE> tes;
try {
tes = TE.parse(new StringReader(headerValues.nextElement()));
Expand All @@ -247,9 +315,9 @@ public boolean useCompression(Request request, Response response) {
}

for (TE te : tes) {
if ("gzip".equalsIgnoreCase(te.getEncoding())) {
if (encoding.equalsIgnoreCase(te.getEncoding())) {
useTransferEncoding = true;
foundGzip = true;
foundEncoding = true;
break;
}
}
Expand All @@ -268,11 +336,11 @@ public boolean useCompression(Request request, Response response) {
// Therefore, set the Vary header to keep proxies happy
ResponseUtil.addVaryFieldName(responseHeaders, "accept-encoding");

// Check if user-agent supports gzip encoding
// Only interested in whether gzip encoding is supported. Other
// Check if user-agent supports the specified encoding
// Only interested in whether the encoding is supported. Other
// encodings and weights can be ignored.
headerValues = request.getMimeHeaders().values("accept-encoding");
while (!foundGzip && headerValues.hasMoreElements()) {
while (!foundEncoding && headerValues.hasMoreElements()) {
List<AcceptEncoding> acceptEncodings;
try {
acceptEncodings = AcceptEncoding.parse(new StringReader(headerValues.nextElement()));
Expand All @@ -282,15 +350,15 @@ public boolean useCompression(Request request, Response response) {
}

for (AcceptEncoding acceptEncoding : acceptEncodings) {
if ("gzip".equalsIgnoreCase(acceptEncoding.getEncoding())) {
foundGzip = true;
if (encoding.equalsIgnoreCase(acceptEncoding.getEncoding())) {
foundEncoding = true;
break;
}
}
}
}

if (!foundGzip) {
if (!foundEncoding) {
return false;
}

Expand All @@ -315,10 +383,10 @@ public boolean useCompression(Request request, Response response) {
response.setContentLength(-1);
if (useTransferEncoding) {
// Configure the transfer encoding for compressed content
responseHeaders.addValue("Transfer-Encoding").setString("gzip");
responseHeaders.addValue("Transfer-Encoding").setString(encoding);
} else {
// Configure the content encoding for compressed content
responseHeaders.addValue("Content-Encoding").setString("gzip");
responseHeaders.addValue("Content-Encoding").setString(encoding);
}

return true;
Expand Down
2 changes: 2 additions & 0 deletions java/org/apache/coyote/LocalStrings.properties
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ asyncStateMachine.invalidAsyncState=Calling [{0}] is not valid for a request wit
asyncStateMachine.stateChange=Changing async state from [{0}] to [{1}]

compressionConfig.ContentEncodingParseFail=Failed to parse Content-Encoding header when checking to see if compression was already in use
compressionConfig.invalidGzipLevel=The gzip compression level [{0}] is not valid. Valid values are -1 (default) or 1-9.
compressionConfig.invalidGzipBufferSize=The gzip buffer size [{0}] is not valid. Must be a positive integer.

continueResponseTiming.invalid=The value [{0}] is not a valid configuration option for continueResponseTiming

Expand Down
45 changes: 44 additions & 1 deletion java/org/apache/coyote/http11/AbstractHttp11Protocol.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
import org.apache.coyote.Response;
import org.apache.coyote.UpgradeProtocol;
import org.apache.coyote.UpgradeToken;
import org.apache.coyote.http11.filters.GzipOutputFilterFactory;
import org.apache.coyote.http11.filters.OutputFilterFactory;
import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler;
import org.apache.coyote.http11.upgrade.UpgradeGroupInfo;
import org.apache.coyote.http11.upgrade.UpgradeProcessorExternal;
Expand All @@ -59,6 +61,7 @@ public abstract class AbstractHttp11Protocol<S> extends AbstractProtocol<S> {
protected static final StringManager sm = StringManager.getManager(AbstractHttp11Protocol.class);

private final CompressionConfig compressionConfig = new CompressionConfig();
private OutputFilterFactory outputFilterFactory = new GzipOutputFilterFactory();

private HttpParser httpParser = null;

Expand Down Expand Up @@ -339,9 +342,49 @@ public void setCompressionMinSize(int compressionMinSize) {
compressionConfig.setCompressionMinSize(compressionMinSize);
}

public int getGzipLevel() {
return compressionConfig.getGzipLevel();
}

public void setGzipLevel(int gzipLevel) {
compressionConfig.setGzipLevel(gzipLevel);
}

public int getGzipBufferSize() {
return compressionConfig.getGzipBufferSize();
}

public void setGzipBufferSize(int gzipBufferSize) {
compressionConfig.setGzipBufferSize(gzipBufferSize);
}

public String getNoCompressionEncodings() {
return compressionConfig.getNoCompressionEncodings();
}

public void setNoCompressionEncodings(String encodings) {
compressionConfig.setNoCompressionEncodings(encodings);
}

public OutputFilterFactory getOutputFilterFactory() {
return outputFilterFactory;
}

public void setOutputFilterFactory(OutputFilterFactory outputFilterFactory) {
this.outputFilterFactory = outputFilterFactory;
}

public void setOutputFilterFactory(String className) {
try {
Class<?> clazz = Class.forName(className);
this.outputFilterFactory = (OutputFilterFactory) clazz.getDeclaredConstructor().newInstance();
} catch (ReflectiveOperationException e) {
throw new IllegalArgumentException(sm.getString("abstractHttp11Protocol.invalidOutputFilterFactory", className), e);
}
}

public boolean useCompression(Request request, Response response) {
return compressionConfig.useCompression(request, response);
return compressionConfig.useCompression(request, response, outputFilterFactory.getEncodingName());
}


Expand Down
5 changes: 4 additions & 1 deletion java/org/apache/coyote/http11/Http11Processor.java
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,10 @@ public Http11Processor(AbstractHttp11Protocol<?> protocol, Adapter adapter) {

// Create and add the gzip filters.
// inputBuffer.addFilter(new GzipInputFilter());
outputBuffer.addFilter(new GzipOutputFilter());
GzipOutputFilter gzipOutputFilter = new GzipOutputFilter();
gzipOutputFilter.setLevel(protocol.getGzipLevel());
gzipOutputFilter.setBufferSize(protocol.getGzipBufferSize());
outputBuffer.addFilter(gzipOutputFilter);

pluggableFilterIndex = inputBuffer.getFilters().length;
}
Expand Down
1 change: 1 addition & 0 deletions java/org/apache/coyote/http11/LocalStrings.properties
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ abstractHttp11Protocol.alpnWithNoAlpn=The upgrade handler [{0}] for [{1}] only s
abstractHttp11Protocol.httpUpgradeConfigured=The [{0}] connector has been configured to support HTTP upgrade to [{1}]
abstractHttp11Protocol.upgradeJmxNameFail=Failed to create ObjectName with which to register upgrade protocol in JMX
abstractHttp11Protocol.upgradeJmxRegistrationFail=Failed to register upgrade protocol in JMX
abstractHttp11Protocol.invalidOutputFilterFactory=The output filter factory class [{0}] could not be loaded or instantiated.

http11processor.fallToDebug=\n\
\ Note: further occurrences of HTTP request parsing errors will be logged at DEBUG level.
Expand Down
Loading