View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.server.handler.gzip;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.util.Set;
24  import java.util.zip.Deflater;
25  
26  import javax.servlet.ServletContext;
27  import javax.servlet.ServletException;
28  import javax.servlet.http.HttpServletRequest;
29  import javax.servlet.http.HttpServletResponse;
30  
31  import org.eclipse.jetty.http.HttpField;
32  import org.eclipse.jetty.http.HttpHeader;
33  import org.eclipse.jetty.http.HttpMethod;
34  import org.eclipse.jetty.http.HttpVersion;
35  import org.eclipse.jetty.http.MimeTypes;
36  import org.eclipse.jetty.http.PathMap;
37  import org.eclipse.jetty.server.HttpOutput;
38  import org.eclipse.jetty.server.Request;
39  import org.eclipse.jetty.server.handler.HandlerWrapper;
40  import org.eclipse.jetty.util.IncludeExclude;
41  import org.eclipse.jetty.util.RegexSet;
42  import org.eclipse.jetty.util.StringUtil;
43  import org.eclipse.jetty.util.URIUtil;
44  import org.eclipse.jetty.util.log.Log;
45  import org.eclipse.jetty.util.log.Logger;
46  
47  
48  /**
49   * A Handler that can dynamically GZIP compress responses.   Unlike 
50   * previous and 3rd party GzipFilters, this mechanism works with asynchronously
51   * generated responses and does not need to wrap the response or it's output
52   * stream.  Instead it uses the efficient {@link org.eclipse.jetty.server.HttpOutput.Interceptor} mechanism.
53   * <p>
54   * The handler can be applied to the entire server (a gzip.mod is included in
55   * the distribution) or it may be applied to individual contexts.
56   * </p>
57   */
58  public class GzipHandler extends HandlerWrapper implements GzipFactory
59  {
60      private static final Logger LOG = Log.getLogger(GzipHandler.class);
61  
62      public final static String GZIP = "gzip";
63      public final static String DEFLATE = "deflate";
64      public final static String ETAG_GZIP="--gzip";
65      public final static String ETAG = "o.e.j.s.Gzip.ETag";
66      public final static int DEFAULT_MIN_GZIP_SIZE=16;
67  
68      private int _minGzipSize=DEFAULT_MIN_GZIP_SIZE;
69      private int _compressionLevel=Deflater.DEFAULT_COMPRESSION;
70      private boolean _checkGzExists = true;
71      
72      // non-static, as other GzipHandler instances may have different configurations
73      private final ThreadLocal<Deflater> _deflater = new ThreadLocal<Deflater>();
74  
75      private final IncludeExclude<String> _agentPatterns=new IncludeExclude<>(RegexSet.class,RegexSet.MATCHER);
76      private final IncludeExclude<String> _methods = new IncludeExclude<>();
77      private final IncludeExclude<String> _paths = new IncludeExclude<>(PathMap.PathSet.class,PathMap.PathSet.MATCHER);
78      private final IncludeExclude<String> _mimeTypes = new IncludeExclude<>();
79      
80      private HttpField _vary;
81  
82      /* ------------------------------------------------------------ */
83      /**
84       * Instantiates a new gzip handler.
85       * The excluded Mime Types are initialized to common known 
86       * images, audio, video and other already compressed types.
87       * The included methods is initialized to GET.
88       * The excluded agent patterns are set to exclude MSIE 6.0
89       */
90      public GzipHandler()
91      {
92          _methods.include(HttpMethod.GET.asString());
93          for (String type:MimeTypes.getKnownMimeTypes())
94          {
95              if ("image/svg+xml".equals(type))
96                  _paths.exclude("*.svgz");
97              else if (type.startsWith("image/")||
98                  type.startsWith("audio/")||
99                  type.startsWith("video/"))
100                 _mimeTypes.exclude(type);
101         }
102         _mimeTypes.exclude("application/compress");
103         _mimeTypes.exclude("application/zip");
104         _mimeTypes.exclude("application/gzip");
105         _mimeTypes.exclude("application/bzip2");
106         _mimeTypes.exclude("application/x-rar-compressed");
107         LOG.debug("{} mime types {}",this,_mimeTypes);
108         
109         _agentPatterns.exclude(".*MSIE 6.0.*");
110     }
111 
112     /* ------------------------------------------------------------ */
113     /**
114      * @param patterns Regular expressions matching user agents to exclude
115      */
116     public void addExcludedAgentPatterns(String... patterns)
117     {
118         _agentPatterns.exclude(patterns);
119     }
120 
121     /* ------------------------------------------------------------ */
122     /**
123      * @param methods The methods to exclude in compression
124      */
125     public void addExcludedMethods(String... methods)
126     {
127         for (String m : methods)
128             _methods.exclude(m);
129     }
130 
131     /* ------------------------------------------------------------ */
132     /**
133      * Set the mime types.
134      * @param types The mime types to exclude (without charset or other parameters).
135      * For backward compatibility the mimetypes may be comma separated strings, but this
136      * will not be supported in future versions.
137      */
138     public void addExcludedMimeTypes(String... types)
139     {
140         for (String t : types)
141             _mimeTypes.exclude(StringUtil.csvSplit(t));
142     }
143 
144     /* ------------------------------------------------------------ */
145     /**
146      * @param pathspecs Path specs (as per servlet spec) to exclude. If a 
147      * ServletContext is available, the paths are relative to the context path,
148      * otherwise they are absolute.
149      * For backward compatibility the pathspecs may be comma separated strings, but this
150      * will not be supported in future versions.
151      */
152     public void addExcludedPaths(String... pathspecs)
153     {
154         for (String p : pathspecs)
155             _paths.exclude(StringUtil.csvSplit(p));
156     }
157 
158     /* ------------------------------------------------------------ */
159     /**
160      * @param patterns Regular expressions matching user agents to exclude
161      */
162     public void addIncludedAgentPatterns(String... patterns)
163     {
164         _agentPatterns.include(patterns);
165     }
166     
167     /* ------------------------------------------------------------ */
168     /**
169      * @param methods The methods to include in compression
170      */
171     public void addIncludedMethods(String... methods)
172     {
173         for (String m : methods)
174             _methods.include(m);
175     }
176 
177     /* ------------------------------------------------------------ */
178     /**
179      * Add included mime types. Inclusion takes precedence over
180      * exclusion.
181      * @param types The mime types to include (without charset or other parameters)
182      * For backward compatibility the mimetypes may be comma separated strings, but this
183      * will not be supported in future versions.
184      */
185     public void addIncludedMimeTypes(String... types)
186     {
187         for (String t : types)
188             _mimeTypes.include(StringUtil.csvSplit(t));
189     }
190 
191     /* ------------------------------------------------------------ */
192     /**
193      * Add path specs to include. Inclusion takes precedence over exclusion.
194      * @param pathspecs Path specs (as per servlet spec) to include. If a 
195      * ServletContext is available, the paths are relative to the context path,
196      * otherwise they are absolute
197      * For backward compatibility the pathspecs may be comma separated strings, but this
198      * will not be supported in future versions.
199      */
200     public void addIncludedPaths(String... pathspecs)
201     {
202         for (String p : pathspecs)
203             _paths.include(StringUtil.csvSplit(p));
204     }
205     
206     /* ------------------------------------------------------------ */
207     @Override
208     protected void doStart() throws Exception
209     {
210         _vary=(_agentPatterns.size()>0)?GzipHttpOutputInterceptor.VARY_ACCEPT_ENCODING_USER_AGENT:GzipHttpOutputInterceptor.VARY_ACCEPT_ENCODING;
211         super.doStart();
212     }
213 
214     /* ------------------------------------------------------------ */
215     public boolean getCheckGzExists()
216     {
217         return _checkGzExists;
218     }
219 
220     /* ------------------------------------------------------------ */
221     public int getCompressionLevel()
222     {
223         return _compressionLevel;
224     }
225     
226     /* ------------------------------------------------------------ */
227     @Override
228     public Deflater getDeflater(Request request, long content_length)
229     {
230         String ua = request.getHttpFields().get(HttpHeader.USER_AGENT);
231         if (ua!=null && !isAgentGzipable(ua))
232         {
233             LOG.debug("{} excluded user agent {}",this,request);
234             return null;
235         }
236         
237         if (content_length>=0 && content_length<_minGzipSize)
238         {
239             LOG.debug("{} excluded minGzipSize {}",this,request);
240             return null;
241         }
242 
243         // If not HTTP/2, then we must check the accept encoding header
244         if (request.getHttpVersion()!=HttpVersion.HTTP_2)
245         {
246             HttpField accept = request.getHttpFields().getField(HttpHeader.ACCEPT_ENCODING);
247 
248             if (accept==null)
249             {
250                 LOG.debug("{} excluded !accept {}",this,request);
251                 return null;
252             }
253             boolean gzip = accept.contains("gzip");
254 
255             if (!gzip)
256             {
257                 LOG.debug("{} excluded not gzip accept {}",this,request);
258                 return null;
259             }
260         }
261         
262         Deflater df = _deflater.get();
263         if (df==null)
264             df=new Deflater(_compressionLevel,true);        
265         else
266             _deflater.set(null);
267         
268         return df;
269     }
270     
271     /* ------------------------------------------------------------ */
272     public String[] getExcludedAgentPatterns()
273     {
274         Set<String> excluded=_agentPatterns.getExcluded();
275         return excluded.toArray(new String[excluded.size()]);
276     }
277 
278     /* ------------------------------------------------------------ */
279     public String[] getExcludedMethods()
280     {
281         Set<String> excluded=_methods.getExcluded();
282         return excluded.toArray(new String[excluded.size()]);
283     }
284 
285     /* ------------------------------------------------------------ */
286     public String[] getExcludedMimeTypes()
287     {
288         Set<String> excluded=_mimeTypes.getExcluded();
289         return excluded.toArray(new String[excluded.size()]);
290     }
291 
292     /* ------------------------------------------------------------ */
293     public String[] getExcludedPaths()
294     {
295         Set<String> excluded=_paths.getExcluded();
296         return excluded.toArray(new String[excluded.size()]);
297     }
298 
299     /* ------------------------------------------------------------ */
300     public String[] getIncludedAgentPatterns()
301     {
302         Set<String> includes=_agentPatterns.getIncluded();
303         return includes.toArray(new String[includes.size()]);
304     }
305     
306     /* ------------------------------------------------------------ */
307     public String[] getIncludedMethods()
308     {
309         Set<String> includes=_methods.getIncluded();
310         return includes.toArray(new String[includes.size()]);
311     }
312 
313     /* ------------------------------------------------------------ */
314     public String[] getIncludedMimeTypes()
315     {
316         Set<String> includes=_mimeTypes.getIncluded();
317         return includes.toArray(new String[includes.size()]);
318     }
319 
320     /* ------------------------------------------------------------ */
321     public String[] getIncludedPaths()
322     {
323         Set<String> includes=_paths.getIncluded();
324         return includes.toArray(new String[includes.size()]);
325     }
326 
327     /* ------------------------------------------------------------ */
328     @Deprecated
329     public String[] getMethods()
330     {
331         return getIncludedMethods();
332     }
333 
334     /* ------------------------------------------------------------ */
335     /**
336      * Get the minimum reponse size.
337      *
338      * @return minimum reponse size
339      */
340     public int getMinGzipSize()
341     {
342         return _minGzipSize;
343     }
344 
345     /* ------------------------------------------------------------ */
346     /**
347      * @see org.eclipse.jetty.server.handler.HandlerWrapper#handle(java.lang.String, org.eclipse.jetty.server.Request, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
348      */
349     @Override
350     public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
351     {
352         ServletContext context = baseRequest.getServletContext();
353         String path = context==null?baseRequest.getRequestURI():URIUtil.addPaths(baseRequest.getServletPath(),baseRequest.getPathInfo());
354         LOG.debug("{} handle {} in {}",this,baseRequest,context);
355         
356         HttpOutput out = baseRequest.getResponse().getHttpOutput();   
357         // Are we already being gzipped?
358         HttpOutput.Interceptor interceptor = out.getInterceptor();
359         while (interceptor!=null)
360         {
361             if (interceptor instanceof GzipHttpOutputInterceptor)
362             {
363                 LOG.debug("{} already intercepting {}",this,request);
364                 _handler.handle(target,baseRequest, request, response);
365                 return;
366             }
367             interceptor=interceptor.getNextInterceptor();
368         }
369         
370         // If not a supported method - no Vary because no matter what client, this URI is always excluded
371         if (!_methods.matches(baseRequest.getMethod()))
372         {
373             LOG.debug("{} excluded by method {}",this,request);
374             _handler.handle(target,baseRequest, request, response);
375             return;
376         }
377         
378         // If not a supported URI- no Vary because no matter what client, this URI is always excluded
379         // Use pathInfo because this is be
380         if (!isPathGzipable(path))
381         {
382             LOG.debug("{} excluded by path {}",this,request);
383             _handler.handle(target,baseRequest, request, response);
384             return;
385         }
386 
387         // Exclude non compressible mime-types known from URI extension. - no Vary because no matter what client, this URI is always excluded
388         String mimeType = context==null?null:context.getMimeType(path);
389         if (mimeType!=null)
390         {
391             mimeType = MimeTypes.getContentTypeWithoutCharset(mimeType);
392             if (!isMimeTypeGzipable(mimeType))
393             {
394                 LOG.debug("{} excluded by path suffix mime type {}",this,request);
395                 // handle normally without setting vary header
396                 _handler.handle(target,baseRequest, request, response);
397                 return;
398             }
399         }
400         
401         if (_checkGzExists && context!=null)
402         {
403             String realpath=request.getServletContext().getRealPath(path);
404             if (realpath!=null)
405             {
406                 File gz=new File(realpath+".gz");
407                 if (gz.exists())
408                 {
409                     LOG.debug("{} gzip exists {}",this,request);
410                     // allow default servlet to handle
411                     _handler.handle(target,baseRequest, request, response);
412                     return;
413                 }
414             }
415         }
416         
417         // Special handling for etags
418         String etag = request.getHeader("If-None-Match"); 
419         if (etag!=null)
420         {
421             if (etag.contains(ETAG_GZIP))
422                 request.setAttribute(ETAG,etag.replace(ETAG_GZIP,""));
423         }
424 
425         // install interceptor and handle
426         out.setInterceptor(new GzipHttpOutputInterceptor(this,_vary,baseRequest.getHttpChannel(),out.getInterceptor()));
427         if (_handler!=null)
428             _handler.handle(target,baseRequest, request, response);
429     }
430 
431     /* ------------------------------------------------------------ */
432     /**
433      * Checks to see if the userAgent is excluded
434      *
435      * @param ua the user agent
436      * @return boolean true if excluded
437      */
438     protected boolean isAgentGzipable(String ua)
439     {
440         if (ua == null)
441             return false;
442         
443         return _agentPatterns.matches(ua);
444     }
445 
446     /* ------------------------------------------------------------ */
447     @Override
448     public boolean isMimeTypeGzipable(String mimetype)
449     {
450         return _mimeTypes.matches(mimetype);
451     }
452 
453     /* ------------------------------------------------------------ */
454     /**
455      * Checks to see if the path is included or not excluded 
456      *
457      * @param requestURI
458      *            the request uri
459      * @return boolean true if gzipable
460      */
461     protected boolean isPathGzipable(String requestURI)
462     {
463         if (requestURI == null)
464             return true;
465         
466         return _paths.matches(requestURI);
467     }
468 
469     /* ------------------------------------------------------------ */
470     @Override
471     public void recycle(Deflater deflater)
472     {
473         deflater.reset();
474         if (_deflater.get()==null)
475             _deflater.set(deflater);
476     }
477 
478     /* ------------------------------------------------------------ */
479     /**
480      * @param checkGzExists If true, check if a static gz file exists for
481      * the resource that the DefaultServlet may serve as precompressed.
482      */
483     public void setCheckGzExists(boolean checkGzExists)
484     {
485         _checkGzExists = checkGzExists;
486     }
487     
488     /* ------------------------------------------------------------ */
489     /**
490      * @param compressionLevel  The compression level to use to initialize {@link Deflater#setLevel(int)}
491      */
492     public void setCompressionLevel(int compressionLevel)
493     {
494         _compressionLevel = compressionLevel;
495     }
496 
497     /* ------------------------------------------------------------ */
498     /**
499      * @param patterns Regular expressions matching user agents to exclude
500      */
501     public void setExcludedAgentPatterns(String... patterns)
502     {
503         _agentPatterns.getExcluded().clear();
504         addExcludedAgentPatterns(patterns);
505     }
506 
507     /* ------------------------------------------------------------ */
508     /**
509      * @param method to exclude
510      */
511     public void setExcludedMethods(String... method)
512     {
513         _methods.getExcluded().clear();
514         _methods.exclude(method);
515     }
516 
517     /* ------------------------------------------------------------ */
518     /**
519      * Set the mime types.
520      * @param types The mime types to exclude (without charset or other parameters)
521      */
522     public void setExcludedMimeTypes(String... types)
523     {
524         _mimeTypes.getExcluded().clear();
525         _mimeTypes.exclude(types);
526     }
527 
528     /* ------------------------------------------------------------ */
529     /**
530      * @param pathspecs Path specs (as per servlet spec) to exclude. If a 
531      * ServletContext is available, the paths are relative to the context path,
532      * otherwise they are absolute.
533      */
534     public void setExcludedPaths(String... pathspecs)
535     {
536         _paths.getExcluded().clear();
537         _paths.exclude(pathspecs);
538     }
539 
540     /* ------------------------------------------------------------ */
541     /**
542      * @param patterns Regular expressions matching user agents to include
543      */
544     public void setIncludedAgentPatterns(String... patterns)
545     {
546         _agentPatterns.getIncluded().clear();
547         addIncludedAgentPatterns(patterns);
548     }
549     
550     /* ------------------------------------------------------------ */
551     /**
552      * @param methods The methods to include in compression
553      */
554     public void setIncludedMethods(String... methods)
555     {
556         _methods.getIncluded().clear();
557         _methods.include(methods);
558     }
559     
560     /* ------------------------------------------------------------ */
561     /**
562      * Set included mime types. Inclusion takes precedence over
563      * exclusion.
564      * @param types The mime types to include (without charset or other parameters)
565      */
566     public void setIncludedMimeTypes(String... types)
567     {
568         _mimeTypes.getIncluded().clear();
569         _mimeTypes.include(types);
570     }
571 
572     /* ------------------------------------------------------------ */
573     /**
574      * Set the path specs to include. Inclusion takes precedence over exclusion.
575      * @param pathspecs Path specs (as per servlet spec) to include. If a 
576      * ServletContext is available, the paths are relative to the context path,
577      * otherwise they are absolute
578      */
579     public void setIncludedPaths(String... pathspecs)
580     {
581         _paths.getIncluded().clear();
582         _paths.include(pathspecs);
583     }
584 
585     /* ------------------------------------------------------------ */
586     /**
587      * Set the minimum response size to trigger dynamic compresssion
588      *
589      * @param minGzipSize minimum response size in bytes
590      */
591     public void setMinGzipSize(int minGzipSize)
592     {
593         _minGzipSize = minGzipSize;
594     }
595 }