1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.coyote;
18
19 import java.io.IOException;
20 import java.io.StringReader;
21 import java.util.ArrayList;
22 import java.util.Enumeration;
23 import java.util.HashSet;
24 import java.util.List;
25 import java.util.Set;
26 import java.util.StringTokenizer;
27 import java.util.regex.Pattern;
28
29 import org.apache.juli.logging.Log;
30 import org.apache.juli.logging.LogFactory;
31 import org.apache.tomcat.util.buf.MessageBytes;
32 import org.apache.tomcat.util.http.MimeHeaders;
33 import org.apache.tomcat.util.http.ResponseUtil;
34 import org.apache.tomcat.util.http.parser.AcceptEncoding;
35 import org.apache.tomcat.util.http.parser.TokenList;
36 import org.apache.tomcat.util.res.StringManager;
37
38 public class CompressionConfig {
39
40 private static final Log log = LogFactory.getLog(CompressionConfig.class);
41 private static final StringManager sm = StringManager.getManager(CompressionConfig.class);
42
43 private int compressionLevel = 0;
44 private Pattern noCompressionUserAgents = null;
45 private String compressibleMimeType = "text/html,text/xml,text/plain,text/css," +
46 "text/javascript,application/javascript,application/json,application/xml";
47 private String[] compressibleMimeTypes = null;
48 private int compressionMinSize = 2048;
49 private boolean noCompressionStrongETag = true;
50
51
52 /**
53 * Set compression level.
54 *
55 * @param compression One of <code>on</code>, <code>force</code>,
56 * <code>off</code> or the minimum compression size in
57 * bytes which implies <code>on</code>
58 */
59 public void setCompression(String compression) {
60 if (compression.equals("on")) {
61 this.compressionLevel = 1;
62 } else if (compression.equals("force")) {
63 this.compressionLevel = 2;
64 } else if (compression.equals("off")) {
65 this.compressionLevel = 0;
66 } else {
67 try {
68 // Try to parse compression as an int, which would give the
69 // minimum compression size
70 setCompressionMinSize(Integer.parseInt(compression));
71 this.compressionLevel = 1;
72 } catch (Exception e) {
73 this.compressionLevel = 0;
74 }
75 }
76 }
77
78
79 /**
80 * Return compression level.
81 *
82 * @return The current compression level in string form (off/on/force)
83 */
84 public String getCompression() {
85 switch (compressionLevel) {
86 case 0:
87 return "off";
88 case 1:
89 return "on";
90 case 2:
91 return "force";
92 }
93 return "off";
94 }
95
96
97 public int getCompressionLevel() {
98 return compressionLevel;
99 }
100
101
102 /**
103 * Obtain the String form of the regular expression that defines the user
104 * agents to not use gzip with.
105 *
106 * @return The regular expression as a String
107 */
108 public String getNoCompressionUserAgents() {
109 if (noCompressionUserAgents == null) {
110 return null;
111 } else {
112 return noCompressionUserAgents.toString();
113 }
114 }
115
116
117 public Pattern getNoCompressionUserAgentsPattern() {
118 return noCompressionUserAgents;
119 }
120
121
122 /**
123 * Set no compression user agent pattern. Regular expression as supported
124 * by {@link Pattern}. e.g.: <code>gorilla|desesplorer|tigrus</code>.
125 *
126 * @param noCompressionUserAgents The regular expression for user agent
127 * strings for which compression should not
128 * be applied
129 */
130 public void setNoCompressionUserAgents(String noCompressionUserAgents) {
131 if (noCompressionUserAgents == null || noCompressionUserAgents.length() == 0) {
132 this.noCompressionUserAgents = null;
133 } else {
134 this.noCompressionUserAgents =
135 Pattern.compile(noCompressionUserAgents);
136 }
137 }
138
139
140 public String getCompressibleMimeType() {
141 return compressibleMimeType;
142 }
143
144
145 public void setCompressibleMimeType(String valueS) {
146 compressibleMimeType = valueS;
147 compressibleMimeTypes = null;
148 }
149
150
151 public String[] getCompressibleMimeTypes() {
152 String[] result = compressibleMimeTypes;
153 if (result != null) {
154 return result;
155 }
156 List<String> values = new ArrayList<>();
157 StringTokenizer tokens = new StringTokenizer(compressibleMimeType, ",");
158 while (tokens.hasMoreTokens()) {
159 String token = tokens.nextToken().trim();
160 if (token.length() > 0) {
161 values.add(token);
162 }
163 }
164 result = values.toArray(new String[values.size()]);
165 compressibleMimeTypes = result;
166 return result;
167 }
168
169
170 public int getCompressionMinSize() {
171 return compressionMinSize;
172 }
173
174
175 /**
176 * Set Minimum size to trigger compression.
177 *
178 * @param compressionMinSize The minimum content length required for
179 * compression in bytes
180 */
181 public void setCompressionMinSize(int compressionMinSize) {
182 this.compressionMinSize = compressionMinSize;
183 }
184
185
186 /**
187 * Determine if compression is disabled if the resource has a strong ETag.
188 *
189 * @return {@code true} if compression is disabled, otherwise {@code false}
190 *
191 * @deprecated Will be removed in Tomcat 10 where it will be hard-coded to
192 * {@code true}
193 */
194 @Deprecated
195 public boolean getNoCompressionStrongETag() {
196 return noCompressionStrongETag;
197 }
198
199
200 /**
201 * Set whether compression is disabled for resources with a strong ETag.
202 *
203 * @param noCompressionStrongETag {@code true} if compression is disabled,
204 * otherwise {@code false}
205 *
206 * @deprecated Will be removed in Tomcat 10 where it will be hard-coded to
207 * {@code true}
208 */
209 @Deprecated
210 public void setNoCompressionStrongETag(boolean noCompressionStrongETag) {
211 this.noCompressionStrongETag = noCompressionStrongETag;
212 }
213
214
215 /**
216 * Determines if compression should be enabled for the given response and if
217 * it is, sets any necessary headers to mark it as such.
218 *
219 * @param request The request that triggered the response
220 * @param response The response to consider compressing
221 *
222 * @return {@code true} if compression was enabled for the given response,
223 * otherwise {@code false}
224 */
225 public boolean useCompression(Request request, Response response) {
226 // Check if compression is enabled
227 if (compressionLevel == 0) {
228 return false;
229 }
230
231 MimeHeaders responseHeaders = response.getMimeHeaders();
232
233 // Check if content is not already compressed
234 MessageBytes contentEncodingMB = responseHeaders.getValue("Content-Encoding");
235 if (contentEncodingMB != null) {
236 // Content-Encoding values are ordered but order is not important
237 // for this check so use a Set rather than a List
238 Set<String> tokens = new HashSet<>();
239 try {
240 TokenList.parseTokenList(responseHeaders.values("Content-Encoding"), tokens);
241 } catch (IOException e) {
242 // Because we are using StringReader, any exception here is a
243 // Tomcat bug.
244 log.warn(sm.getString("compressionConfig.ContentEncodingParseFail"), e);
245 return false;
246 }
247 if (tokens.contains("gzip") || tokens.contains("br")) {
248 return false;
249 }
250 }
251
252 // If force mode, the length and MIME type checks are skipped
253 if (compressionLevel != 2) {
254 // Check if the response is of sufficient length to trigger the compression
255 long contentLength = response.getContentLengthLong();
256 if (contentLength != -1 && contentLength < compressionMinSize) {
257 return false;
258 }
259
260 // Check for compatible MIME-TYPE
261 String[] compressibleMimeTypes = getCompressibleMimeTypes();
262 if (compressibleMimeTypes != null &&
263 !startsWithStringArray(compressibleMimeTypes, response.getContentType())) {
264 return false;
265 }
266 }
267
268 // Check if the resource has a strong ETag
269 if (noCompressionStrongETag) {
270 String eTag = responseHeaders.getHeader("ETag");
271 if (eTag != null && !eTag.trim().startsWith("W/")) {
272 // Has an ETag that doesn't start with "W/..." so it must be a
273 // strong ETag
274 return false;
275 }
276 }
277
278 // If processing reaches this far, the response might be compressed.
279 // Therefore, set the Vary header to keep proxies happy
280 ResponseUtil.addVaryFieldName(responseHeaders, "accept-encoding");
281
282 // Check if user-agent supports gzip encoding
283 // Only interested in whether gzip encoding is supported. Other
284 // encodings and weights can be ignored.
285 Enumeration<String> headerValues = request.getMimeHeaders().values("accept-encoding");
286 boolean foundGzip = false;
287 while (!foundGzip && headerValues.hasMoreElements()) {
288 List<AcceptEncoding> acceptEncodings = null;
289 try {
290 acceptEncodings = AcceptEncoding.parse(new StringReader(headerValues.nextElement()));
291 } catch (IOException ioe) {
292 // If there is a problem reading the header, disable compression
293 return false;
294 }
295
296 for (AcceptEncoding acceptEncoding : acceptEncodings) {
297 if ("gzip".equalsIgnoreCase(acceptEncoding.getEncoding())) {
298 foundGzip = true;
299 break;
300 }
301 }
302 }
303
304 if (!foundGzip) {
305 return false;
306 }
307
308 // If force mode, the browser checks are skipped
309 if (compressionLevel != 2) {
310 // Check for incompatible Browser
311 Pattern noCompressionUserAgents = this.noCompressionUserAgents;
312 if (noCompressionUserAgents != null) {
313 MessageBytes userAgentValueMB = request.getMimeHeaders().getValue("user-agent");
314 if(userAgentValueMB != null) {
315 String userAgentValue = userAgentValueMB.toString();
316 if (noCompressionUserAgents.matcher(userAgentValue).matches()) {
317 return false;
318 }
319 }
320 }
321 }
322
323 // All checks have passed. Compression is enabled.
324
325 // Compressed content length is unknown so mark it as such.
326 response.setContentLength(-1);
327 // Configure the content encoding for compressed content
328 responseHeaders.setValue("Content-Encoding").setString("gzip");
329
330 return true;
331 }
332
333
334 /**
335 * Checks if any entry in the string array starts with the specified value
336 *
337 * @param sArray the StringArray
338 * @param value string
339 */
340 private static boolean startsWithStringArray(String sArray[], String value) {
341 if (value == null) {
342 return false;
343 }
344 for (int i = 0; i < sArray.length; i++) {
345 if (value.startsWith(sArray[i])) {
346 return true;
347 }
348 }
349 return false;
350 }
351 }
352