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