/* * 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 on, force, * off or the minimum compression size in * bytes which implies on */ 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.: gorilla|desesplorer|tigrus. * * @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 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 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 headerValues = request.getMimeHeaders().values("accept-encoding"); boolean foundGzip = false; while (!foundGzip && headerValues.hasMoreElements()) { List 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; } }