352 lines
12 KiB
Java
352 lines
12 KiB
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[values.size()]);
|
|
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 (int i = 0; i < sArray.length; i++) {
|
|
if (value.startsWith(sArray[i])) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|