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 trueif 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 trueif 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 trueif 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