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