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         // check the accept encoding header
264         HttpField accept = request.getHttpFields().getField(HttpHeader.ACCEPT_ENCODING);
265 
266         if (accept==null)
267         {
268             LOG.debug("{} excluded !accept {}",this,request);
269             return null;
270         }
271         boolean gzip = accept.contains("gzip");
272 
273         if (!gzip)
274         {
275             LOG.debug("{} excluded not gzip accept {}",this,request);
276             return null;
277         }
278 
279         Deflater df = _deflater.get();
280         if (df==null)
281             df=new Deflater(_compressionLevel,true);
282         else
283             _deflater.set(null);
284 
285         return df;
286     }
287 
288     /* ------------------------------------------------------------ */
289     public String[] getExcludedAgentPatterns()
290     {
291         Set<String> excluded=_agentPatterns.getExcluded();
292         return excluded.toArray(new String[excluded.size()]);
293     }
294 
295     /* ------------------------------------------------------------ */
296     public String[] getExcludedMethods()
297     {
298         Set<String> excluded=_methods.getExcluded();
299         return excluded.toArray(new String[excluded.size()]);
300     }
301 
302     /* ------------------------------------------------------------ */
303     public String[] getExcludedMimeTypes()
304     {
305         Set<String> excluded=_mimeTypes.getExcluded();
306         return excluded.toArray(new String[excluded.size()]);
307     }
308 
309     /* ------------------------------------------------------------ */
310     public String[] getExcludedPaths()
311     {
312         Set<String> excluded=_paths.getExcluded();
313         return excluded.toArray(new String[excluded.size()]);
314     }
315 
316     /* ------------------------------------------------------------ */
317     public String[] getIncludedAgentPatterns()
318     {
319         Set<String> includes=_agentPatterns.getIncluded();
320         return includes.toArray(new String[includes.size()]);
321     }
322 
323     /* ------------------------------------------------------------ */
324     public String[] getIncludedMethods()
325     {
326         Set<String> includes=_methods.getIncluded();
327         return includes.toArray(new String[includes.size()]);
328     }
329 
330     /* ------------------------------------------------------------ */
331     public String[] getIncludedMimeTypes()
332     {
333         Set<String> includes=_mimeTypes.getIncluded();
334         return includes.toArray(new String[includes.size()]);
335     }
336 
337     /* ------------------------------------------------------------ */
338     public String[] getIncludedPaths()
339     {
340         Set<String> includes=_paths.getIncluded();
341         return includes.toArray(new String[includes.size()]);
342     }
343 
344     /* ------------------------------------------------------------ */
345     @Deprecated
346     public String[] getMethods()
347     {
348         return getIncludedMethods();
349     }
350 
351     /* ------------------------------------------------------------ */
352     /**
353      * Get the minimum reponse size.
354      *
355      * @return minimum reponse size
356      */
357     public int getMinGzipSize()
358     {
359         return _minGzipSize;
360     }
361 
362     protected HttpField getVaryField()
363     {
364         return _vary;
365     }
366 
367     /* ------------------------------------------------------------ */
368     /**
369      * @see org.eclipse.jetty.server.handler.HandlerWrapper#handle(java.lang.String, org.eclipse.jetty.server.Request, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
370      */
371     @Override
372     public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
373     {
374         ServletContext context = baseRequest.getServletContext();
375         String path = context==null?baseRequest.getRequestURI():URIUtil.addPaths(baseRequest.getServletPath(),baseRequest.getPathInfo());
376         LOG.debug("{} handle {} in {}",this,baseRequest,context);
377 
378         HttpOutput out = baseRequest.getResponse().getHttpOutput();
379         // Are we already being gzipped?
380         HttpOutput.Interceptor interceptor = out.getInterceptor();
381         while (interceptor!=null)
382         {
383             if (interceptor instanceof GzipHttpOutputInterceptor)
384             {
385                 LOG.debug("{} already intercepting {}",this,request);
386                 _handler.handle(target,baseRequest, request, response);
387                 return;
388             }
389             interceptor=interceptor.getNextInterceptor();
390         }
391 
392         // If not a supported method - no Vary because no matter what client, this URI is always excluded
393         if (!_methods.matches(baseRequest.getMethod()))
394         {
395             LOG.debug("{} excluded by method {}",this,request);
396             _handler.handle(target,baseRequest, request, response);
397             return;
398         }
399 
400         // If not a supported URI- no Vary because no matter what client, this URI is always excluded
401         // Use pathInfo because this is be
402         if (!isPathGzipable(path))
403         {
404             LOG.debug("{} excluded by path {}",this,request);
405             _handler.handle(target,baseRequest, request, response);
406             return;
407         }
408 
409         // Exclude non compressible mime-types known from URI extension. - no Vary because no matter what client, this URI is always excluded
410         String mimeType = context==null?null:context.getMimeType(path);
411         if (mimeType!=null)
412         {
413             mimeType = MimeTypes.getContentTypeWithoutCharset(mimeType);
414             if (!isMimeTypeGzipable(mimeType))
415             {
416                 LOG.debug("{} excluded by path suffix mime type {}",this,request);
417                 // handle normally without setting vary header
418                 _handler.handle(target,baseRequest, request, response);
419                 return;
420             }
421         }
422 
423         if (_checkGzExists && context!=null)
424         {
425             String realpath=request.getServletContext().getRealPath(path);
426             if (realpath!=null)
427             {
428                 File gz=new File(realpath+".gz");
429                 if (gz.exists())
430                 {
431                     LOG.debug("{} gzip exists {}",this,request);
432                     // allow default servlet to handle
433                     _handler.handle(target,baseRequest, request, response);
434                     return;
435                 }
436             }
437         }
438 
439         // Special handling for etags
440         String etag = baseRequest.getHttpFields().get(HttpHeader.IF_NONE_MATCH);
441         if (etag!=null)
442         {
443             int i=etag.indexOf(GzipHttpContent.ETAG_GZIP_QUOTE);
444             if (i>0)
445             {
446                 while (i>=0)
447                 {
448                     etag=etag.substring(0,i)+etag.substring(i+GzipHttpContent.ETAG_GZIP.length());
449                     i=etag.indexOf(GzipHttpContent.ETAG_GZIP_QUOTE,i);
450                 }
451                 baseRequest.getHttpFields().put(new HttpField(HttpHeader.IF_NONE_MATCH,etag));
452             }
453         }
454 
455         // install interceptor and handle
456         out.setInterceptor(new GzipHttpOutputInterceptor(this,getVaryField(),baseRequest.getHttpChannel(),out.getInterceptor(),isSyncFlush()));
457 
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 }