View Javadoc

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