CompressionConfig.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote;
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.regex.Pattern;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.buf.MessageBytes;
import org.apache.tomcat.util.http.MimeHeaders;
import org.apache.tomcat.util.http.ResponseUtil;
import org.apache.tomcat.util.http.parser.AcceptEncoding;
import org.apache.tomcat.util.http.parser.TokenList;
import org.apache.tomcat.util.res.StringManager;
public class CompressionConfig {
private static final Log log = LogFactory.getLog(CompressionConfig.class);
private static final StringManager sm = StringManager.getManager(CompressionConfig.class);
private int compressionLevel = 0;
private Pattern noCompressionUserAgents = null;
private String compressibleMimeType = "text/html,text/xml,text/plain,text/css," +
"text/javascript,application/javascript,application/json,application/xml";
private String[] compressibleMimeTypes = null;
private int compressionMinSize = 2048;
private boolean noCompressionStrongETag = true;
/**
* Set compression level.
*
* @param compression One of <code>on</code>, <code>force</code>, <code>off</code> or the minimum compression size
* in bytes which implies <code>on</code>
*/
public void setCompression(String compression) {
if (compression.equals("on")) {
this.compressionLevel = 1;
} else if (compression.equals("force")) {
this.compressionLevel = 2;
} else if (compression.equals("off")) {
this.compressionLevel = 0;
} else {
try {
// Try to parse compression as an int, which would give the
// minimum compression size
setCompressionMinSize(Integer.parseInt(compression));
this.compressionLevel = 1;
} catch (Exception e) {
this.compressionLevel = 0;
}
}
}
/**
* Return compression level.
*
* @return The current compression level in string form (off/on/force)
*/
public String getCompression() {
switch (compressionLevel) {
case 0:
return "off";
case 1:
return "on";
case 2:
return "force";
}
return "off";
}
public int getCompressionLevel() {
return compressionLevel;
}
/**
* Obtain the String form of the regular expression that defines the user agents to not use gzip with.
*
* @return The regular expression as a String
*/
public String getNoCompressionUserAgents() {
if (noCompressionUserAgents == null) {
return null;
} else {
return noCompressionUserAgents.toString();
}
}
public Pattern getNoCompressionUserAgentsPattern() {
return noCompressionUserAgents;
}
/**
* Set no compression user agent pattern. Regular expression as supported by {@link Pattern}. e.g.:
* <code>gorilla|desesplorer|tigrus</code>.
*
* @param noCompressionUserAgents The regular expression for user agent strings for which compression should not be
* applied
*/
public void setNoCompressionUserAgents(String noCompressionUserAgents) {
if (noCompressionUserAgents == null || noCompressionUserAgents.length() == 0) {
this.noCompressionUserAgents = null;
} else {
this.noCompressionUserAgents = Pattern.compile(noCompressionUserAgents);
}
}
public String getCompressibleMimeType() {
return compressibleMimeType;
}
public void setCompressibleMimeType(String valueS) {
compressibleMimeType = valueS;
compressibleMimeTypes = null;
}
public String[] getCompressibleMimeTypes() {
String[] result = compressibleMimeTypes;
if (result != null) {
return result;
}
List<String> values = new ArrayList<>();
StringTokenizer tokens = new StringTokenizer(compressibleMimeType, ",");
while (tokens.hasMoreTokens()) {
String token = tokens.nextToken().trim();
if (token.length() > 0) {
values.add(token);
}
}
result = values.toArray(new String[0]);
compressibleMimeTypes = result;
return result;
}
public int getCompressionMinSize() {
return compressionMinSize;
}
/**
* Set Minimum size to trigger compression.
*
* @param compressionMinSize The minimum content length required for compression in bytes
*/
public void setCompressionMinSize(int compressionMinSize) {
this.compressionMinSize = compressionMinSize;
}
/**
* Determine if compression is disabled if the resource has a strong ETag.
*
* @return {@code true} if compression is disabled, otherwise {@code false}
*
* @deprecated Will be removed in Tomcat 10 where it will be hard-coded to {@code true}
*/
@Deprecated
public boolean getNoCompressionStrongETag() {
return noCompressionStrongETag;
}
/**
* Set whether compression is disabled for resources with a strong ETag.
*
* @param noCompressionStrongETag {@code true} if compression is disabled, otherwise {@code false}
*
* @deprecated Will be removed in Tomcat 10 where it will be hard-coded to {@code true}
*/
@Deprecated
public void setNoCompressionStrongETag(boolean noCompressionStrongETag) {
this.noCompressionStrongETag = noCompressionStrongETag;
}
/**
* 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
*
* @return {@code true} if compression was enabled for the given response, otherwise {@code false}
*/
public boolean useCompression(Request request, Response response) {
// Check if compression is enabled
if (compressionLevel == 0) {
return false;
}
MimeHeaders responseHeaders = response.getMimeHeaders();
// Check if content is not already compressed
MessageBytes contentEncodingMB = responseHeaders.getValue("Content-Encoding");
if (contentEncodingMB != null) {
// Content-Encoding values are ordered but order is not important
// for this check so use a Set rather than a List
Set<String> tokens = new HashSet<>();
try {
TokenList.parseTokenList(responseHeaders.values("Content-Encoding"), tokens);
} catch (IOException e) {
// Because we are using StringReader, any exception here is a
// Tomcat bug.
log.warn(sm.getString("compressionConfig.ContentEncodingParseFail"), e);
return false;
}
if (tokens.contains("gzip") || tokens.contains("br")) {
return false;
}
}
// If force mode, the length and MIME type checks are skipped
if (compressionLevel != 2) {
// Check if the response is of sufficient length to trigger the compression
long contentLength = response.getContentLengthLong();
if (contentLength != -1 && contentLength < compressionMinSize) {
return false;
}
// Check for compatible MIME-TYPE
String[] compressibleMimeTypes = getCompressibleMimeTypes();
if (compressibleMimeTypes != null &&
!startsWithStringArray(compressibleMimeTypes, response.getContentType())) {
return false;
}
}
// Check if the resource has a strong ETag
if (noCompressionStrongETag) {
String eTag = responseHeaders.getHeader("ETag");
if (eTag != null && !eTag.trim().startsWith("W/")) {
// Has an ETag that doesn't start with "W/..." so it must be a
// strong ETag
return false;
}
}
// If processing reaches this far, the response might be compressed.
// 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
// encodings and weights can be ignored.
Enumeration<String> headerValues = request.getMimeHeaders().values("accept-encoding");
boolean foundGzip = false;
while (!foundGzip && headerValues.hasMoreElements()) {
List<AcceptEncoding> acceptEncodings = null;
try {
acceptEncodings = AcceptEncoding.parse(new StringReader(headerValues.nextElement()));
} catch (IOException ioe) {
// If there is a problem reading the header, disable compression
return false;
}
for (AcceptEncoding acceptEncoding : acceptEncodings) {
if ("gzip".equalsIgnoreCase(acceptEncoding.getEncoding())) {
foundGzip = true;
break;
}
}
}
if (!foundGzip) {
return false;
}
// If force mode, the browser checks are skipped
if (compressionLevel != 2) {
// Check for incompatible Browser
Pattern noCompressionUserAgents = this.noCompressionUserAgents;
if (noCompressionUserAgents != null) {
MessageBytes userAgentValueMB = request.getMimeHeaders().getValue("user-agent");
if (userAgentValueMB != null) {
String userAgentValue = userAgentValueMB.toString();
if (noCompressionUserAgents.matcher(userAgentValue).matches()) {
return false;
}
}
}
}
// All checks have passed. Compression is enabled.
// Compressed content length is unknown so mark it as such.
response.setContentLength(-1);
// Configure the content encoding for compressed content
responseHeaders.setValue("Content-Encoding").setString("gzip");
return true;
}
/**
* Checks if any entry in the string array starts with the specified value
*
* @param sArray the StringArray
* @param value string
*/
private static boolean startsWithStringArray(String sArray[], String value) {
if (value == null) {
return false;
}
for (String s : sArray) {
if (value.startsWith(s)) {
return true;
}
}
return false;
}
}