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.servlets.gzip;
20  
21  import java.io.IOException;
22  import java.io.OutputStream;
23  import java.io.OutputStreamWriter;
24  import java.io.PrintWriter;
25  import java.io.UnsupportedEncodingException;
26  import java.util.Set;
27  import java.util.zip.DeflaterOutputStream;
28  import java.util.zip.GZIPOutputStream;
29  
30  import javax.servlet.AsyncEvent;
31  import javax.servlet.AsyncListener;
32  import javax.servlet.ServletContext;
33  import javax.servlet.ServletException;
34  import javax.servlet.http.HttpServletRequest;
35  import javax.servlet.http.HttpServletResponse;
36  
37  import org.eclipse.jetty.http.HttpMethod;
38  import org.eclipse.jetty.http.MimeTypes;
39  import org.eclipse.jetty.http.pathmap.PathSpecSet;
40  import org.eclipse.jetty.server.Request;
41  import org.eclipse.jetty.server.handler.HandlerWrapper;
42  import org.eclipse.jetty.util.IncludeExclude;
43  import org.eclipse.jetty.util.RegexSet;
44  import org.eclipse.jetty.util.StringUtil;
45  import org.eclipse.jetty.util.URIUtil;
46  import org.eclipse.jetty.util.log.Log;
47  import org.eclipse.jetty.util.log.Logger;
48  
49  /* ------------------------------------------------------------ */
50  /**
51   * GZIP Handler This handler will gzip the content of a response if:
52   * <ul>
53   * <li>The handler is mapped to a matching path</li>
54   * <li>The response status code is >=200 and <300
55   * <li>The content length is unknown or more than the <code>minGzipSize</code> initParameter or the minGzipSize is 0(default)</li>
56   * <li>The content-type matches one of the set of mimetypes to be compressed</li>
57   * <li>The content-type does NOT match one of the set of mimetypes AND setExcludeMimeTypes is <code>true</code></li>
58   * <li>No content-encoding is specified by the resource</li>
59   * </ul>
60   *
61   * <p>
62   * Compressing the content can greatly improve the network bandwidth usage, but at a cost of memory and CPU cycles. If this handler is used for static content,
63   * then use of efficient direct NIO may be prevented, thus use of the gzip mechanism of the <code>org.eclipse.jetty.servlet.DefaultServlet</code> is advised instead.
64   * </p>
65   */
66  public class GzipHandler extends HandlerWrapper
67  {
68      private static final Logger LOG = Log.getLogger(GzipHandler.class);
69  
70      protected int _bufferSize = 8192;
71      protected int _minGzipSize = 256;
72      protected String _vary = "Accept-Encoding, User-Agent";
73      
74      private final IncludeExclude<String> _agentPatterns=new IncludeExclude<>(RegexSet.class);
75      private final IncludeExclude<String> _methods = new IncludeExclude<>();
76      private final IncludeExclude<String> _paths = new IncludeExclude<String>(PathSpecSet.class);
77      private final IncludeExclude<String> _mimeTypes = new IncludeExclude<>();
78  
79      /* ------------------------------------------------------------ */
80      /**
81       * Instantiates a new gzip handler.
82       */
83      public GzipHandler()
84      {
85          _methods.include(HttpMethod.GET.asString());
86          for (String type:MimeTypes.getKnownMimeTypes())
87          {
88              if ("image/svg+xml".equals(type))
89                  _paths.exclude("*.svgz");
90              else if (type.startsWith("image/")||
91                  type.startsWith("audio/")||
92                  type.startsWith("video/"))
93                  _mimeTypes.exclude(type);
94          }
95          _mimeTypes.exclude("application/compress");
96          _mimeTypes.exclude("application/zip");
97          _mimeTypes.exclude("application/gzip");
98          _mimeTypes.exclude("application/bzip2");
99          _mimeTypes.exclude("application/x-rar-compressed");
100         LOG.debug("{} mime types {}",this,_mimeTypes);
101         
102         _agentPatterns.exclude(".*MSIE 6.0.*");
103     }
104     
105     /* ------------------------------------------------------------ */
106     /**
107      * @param patterns Regular expressions matching user agents to exclude
108      */
109     public void addExcludedAgentPatterns(String... patterns)
110     {
111         _agentPatterns.exclude(patterns);
112     }
113 
114     /* ------------------------------------------------------------ */
115     /**
116      * @param methods The methods to exclude in compression
117      */
118     public void addExcludedMethods(String... methods)
119     {
120         for (String m : methods)
121             _methods.exclude(m);
122     }
123 
124     /* ------------------------------------------------------------ */
125     /**
126      * Set the mime types.
127      * @param types The mime types to exclude (without charset or other parameters).
128      * For backward compatibility the mimetypes may be comma separated strings, but this
129      * will not be supported in future versions.
130      */
131     public void addExcludedMimeTypes(String... types)
132     {
133         for (String t : types)
134             _mimeTypes.exclude(StringUtil.csvSplit(t));
135     }
136 
137     /* ------------------------------------------------------------ */
138     /**
139      * Add path to excluded paths list.
140      * <p>
141      * There are 2 syntaxes supported, Servlet <code>url-pattern</code> based, and
142      * Regex based.  This means that the initial characters on the path spec
143      * line are very strict, and determine the behavior of the path matching.
144      * <ul>
145      *  <li>If the spec starts with <code>'^'</code> the spec is assumed to be
146      *      a regex based path spec and will match with normal Java regex rules.</li>
147      *  <li>If the spec starts with <code>'/'</code> then spec is assumed to be
148      *      a Servlet url-pattern rules path spec for either an exact match
149      *      or prefix based match.</li>
150      *  <li>If the spec starts with <code>'*.'</code> then spec is assumed to be
151      *      a Servlet url-pattern rules path spec for a suffix based match.</li>
152      *  <li>All other syntaxes are unsupported</li> 
153      * </ul>
154      * <p>
155      * Note: inclusion takes precedence over exclude.
156      * 
157      * @param pathspecs Path specs (as per servlet spec) to exclude. If a 
158      * ServletContext is available, the paths are relative to the context path,
159      * otherwise they are absolute.<br>
160      * For backward compatibility the pathspecs may be comma separated strings, but this
161      * will not be supported in future versions.
162      */
163     public void addExcludedPaths(String... pathspecs)
164     {
165         for (String p : pathspecs)
166             _paths.exclude(StringUtil.csvSplit(p));
167     }
168 
169     /* ------------------------------------------------------------ */
170     /**
171      * @param patterns Regular expressions matching user agents to exclude
172      */
173     public void addIncludedAgentPatterns(String... patterns)
174     {
175         _agentPatterns.include(patterns);
176     }
177     
178     /* ------------------------------------------------------------ */
179     /**
180      * @param methods The methods to include in compression
181      */
182     public void addIncludedMethods(String... methods)
183     {
184         for (String m : methods)
185             _methods.include(m);
186     }
187 
188     /* ------------------------------------------------------------ */
189     /**
190      * Add included mime types. Inclusion takes precedence over
191      * exclusion.
192      * @param types The mime types to include (without charset or other parameters)
193      * For backward compatibility the mimetypes may be comma separated strings, but this
194      * will not be supported in future versions.
195      */
196     public void addIncludedMimeTypes(String... types)
197     {
198         for (String t : types)
199             _mimeTypes.include(StringUtil.csvSplit(t));
200     }
201 
202     /* ------------------------------------------------------------ */
203     /**
204      * Add path specs to include.
205      * <p>
206      * There are 2 syntaxes supported, Servlet <code>url-pattern</code> based, and
207      * Regex based.  This means that the initial characters on the path spec
208      * line are very strict, and determine the behavior of the path matching.
209      * <ul>
210      *  <li>If the spec starts with <code>'^'</code> the spec is assumed to be
211      *      a regex based path spec and will match with normal Java regex rules.</li>
212      *  <li>If the spec starts with <code>'/'</code> then spec is assumed to be
213      *      a Servlet url-pattern rules path spec for either an exact match
214      *      or prefix based match.</li>
215      *  <li>If the spec starts with <code>'*.'</code> then spec is assumed to be
216      *      a Servlet url-pattern rules path spec for a suffix based match.</li>
217      *  <li>All other syntaxes are unsupported</li> 
218      * </ul>
219      * <p>
220      * Note: inclusion takes precedence over exclude.
221      * 
222      * @param pathspecs Path specs (as per servlet spec) to include. If a 
223      * ServletContext is available, the paths are relative to the context path,
224      * otherwise they are absolute
225      */
226     public void addIncludedPaths(String... pathspecs)
227     {
228         for (String p : pathspecs)
229             _paths.include(StringUtil.csvSplit(p));
230     }
231     
232     /* ------------------------------------------------------------ */
233     public String[] getExcludedAgentPatterns()
234     {
235         Set<String> excluded=_agentPatterns.getExcluded();
236         return excluded.toArray(new String[excluded.size()]);
237     }
238 
239     /* ------------------------------------------------------------ */
240     public String[] getExcludedMethods()
241     {
242         Set<String> excluded=_methods.getExcluded();
243         return excluded.toArray(new String[excluded.size()]);
244     }
245 
246     /* ------------------------------------------------------------ */
247     public String[] getExcludedMimeTypes()
248     {
249         Set<String> excluded=_mimeTypes.getExcluded();
250         return excluded.toArray(new String[excluded.size()]);
251     }
252 
253     /* ------------------------------------------------------------ */
254     public String[] getExcludedPaths()
255     {
256         Set<String> excluded=_paths.getExcluded();
257         return excluded.toArray(new String[excluded.size()]);
258     }
259 
260     /* ------------------------------------------------------------ */
261     public String[] getIncludedAgentPatterns()
262     {
263         Set<String> includes=_agentPatterns.getIncluded();
264         return includes.toArray(new String[includes.size()]);
265     }
266     
267     /* ------------------------------------------------------------ */
268     public String[] getIncludedMethods()
269     {
270         Set<String> includes=_methods.getIncluded();
271         return includes.toArray(new String[includes.size()]);
272     }
273 
274     /* ------------------------------------------------------------ */
275     public String[] getIncludedMimeTypes()
276     {
277         Set<String> includes=_mimeTypes.getIncluded();
278         return includes.toArray(new String[includes.size()]);
279     }
280 
281     /* ------------------------------------------------------------ */
282     public String[] getIncludedPaths()
283     {
284         Set<String> includes=_paths.getIncluded();
285         return includes.toArray(new String[includes.size()]);
286     }
287 
288     /* ------------------------------------------------------------ */
289     /**
290      * Get the mime types.
291      *
292      * @return mime types to set
293      * @deprecated use {@link #getExcludedMimeTypes()} or {@link #getIncludedMimeTypes()} instead
294      */
295     @Deprecated
296     public Set<String> getMimeTypes()
297     {
298         throw new UnsupportedOperationException("Use getIncludedMimeTypes or getExcludedMimeTypes instead");
299     }
300 
301     /* ------------------------------------------------------------ */
302     /**
303      * Set the mime types.
304      *
305      * @param mimeTypes
306      *            the mime types to set
307      * @deprecated use {@link #setExcludedMimeTypes()} or {@link #setIncludedMimeTypes()} instead
308      */
309     @Deprecated
310     public void setMimeTypes(Set<String> mimeTypes)
311     {
312         throw new UnsupportedOperationException("Use setIncludedMimeTypes or setExcludedMimeTypes instead");
313     }
314 
315     /* ------------------------------------------------------------ */
316     /**
317      * Set the mime types.
318      *
319      * @param mimeTypes
320      *            the mime types to set
321      * @deprecated use {@link #setExcludedMimeTypes()} or {@link #setIncludedMimeTypes()} instead
322      */
323     @Deprecated
324     public void setMimeTypes(String mimeTypes)
325     {
326         throw new UnsupportedOperationException("Use setIncludedMimeTypes or setExcludedMimeTypes instead");
327     }
328 
329     /* ------------------------------------------------------------ */
330     /**
331      * Set the mime types.
332      * @deprecated use {@link #setExcludedMimeTypes()} instead
333      */
334     @Deprecated
335     public void setExcludeMimeTypes(boolean exclude)
336     {
337         throw new UnsupportedOperationException("Use setExcludedMimeTypes instead");
338     }
339 
340     /* ------------------------------------------------------------ */
341     /**
342      * Get the excluded user agents.
343      *
344      * @return excluded user agents
345      */
346     public Set<String> getExcluded()
347     {
348         return _agentPatterns.getExcluded();
349     }
350 
351     /* ------------------------------------------------------------ */
352     /**
353      * Set the excluded user agents.
354      *
355      * @param excluded
356      *            excluded user agents to set
357      */
358     public void setExcluded(Set<String> excluded)
359     {
360         _agentPatterns.getExcluded().clear();
361         _agentPatterns.getExcluded().addAll(excluded);
362     }
363 
364     /* ------------------------------------------------------------ */
365     /**
366      * Set the excluded user agents.
367      *
368      * @param excluded
369      *            excluded user agents to set
370      */
371     public void setExcluded(String excluded)
372     {
373         _agentPatterns.getExcluded().clear();
374 
375         if (excluded != null)
376         {
377             _agentPatterns.exclude(StringUtil.csvSplit(excluded));
378         }
379     }
380 
381     /* ------------------------------------------------------------ */
382     /**
383      * @return The value of the Vary header set if a response can be compressed.
384      */
385     public String getVary()
386     {
387         return _vary;
388     }
389 
390     /* ------------------------------------------------------------ */
391     /**
392      * Set the value of the Vary header sent with responses that could be compressed.  
393      * <p>
394      * By default it is set to 'Accept-Encoding, User-Agent' since IE6 is excluded by 
395      * default from the excludedAgents. If user-agents are not to be excluded, then 
396      * this can be set to 'Accept-Encoding'.  Note also that shared caches may cache 
397      * many copies of a resource that is varied by User-Agent - one per variation of the 
398      * User-Agent, unless the cache does some normalization of the UA string.
399      * @param vary The value of the Vary header set if a response can be compressed.
400      */
401     public void setVary(String vary)
402     {
403         _vary = vary;
404     }
405 
406     /* ------------------------------------------------------------ */
407     /**
408      * Get the buffer size.
409      *
410      * @return the buffer size
411      */
412     public int getBufferSize()
413     {
414         return _bufferSize;
415     }
416 
417     /* ------------------------------------------------------------ */
418     /**
419      * Set the buffer size.
420      *
421      * @param bufferSize
422      *            buffer size to set
423      */
424     public void setBufferSize(int bufferSize)
425     {
426         _bufferSize = bufferSize;
427     }
428 
429     /* ------------------------------------------------------------ */
430     /**
431      * Get the minimum reponse size.
432      *
433      * @return minimum reponse size
434      */
435     public int getMinGzipSize()
436     {
437         return _minGzipSize;
438     }
439 
440     /* ------------------------------------------------------------ */
441     /**
442      * Set the minimum reponse size.
443      *
444      * @param minGzipSize
445      *            minimum reponse size
446      */
447     public void setMinGzipSize(int minGzipSize)
448     {
449         _minGzipSize = minGzipSize;
450     }
451     
452     /* ------------------------------------------------------------ */
453     @Override
454     protected void doStart() throws Exception
455     {
456         super.doStart();
457     }
458 
459     /* ------------------------------------------------------------ */
460     /**
461      * @see org.eclipse.jetty.server.handler.HandlerWrapper#handle(java.lang.String, org.eclipse.jetty.server.Request, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
462      */
463     @Override
464     public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
465     {
466         if(_handler == null || !isStarted())
467         {
468             // do nothing
469             return;
470         }
471         
472         if(isGzippable(baseRequest, request, response))
473         {
474             final CompressedResponseWrapper wrappedResponse = newGzipResponseWrapper(request,response);
475 
476             boolean exceptional=true;
477             try
478             {
479                 _handler.handle(target, baseRequest, request, wrappedResponse);
480                 exceptional=false;
481             }
482             finally
483             {
484                 if (request.isAsyncStarted())
485                 {
486                     request.getAsyncContext().addListener(new AsyncListener()
487                     {
488                         
489                         @Override
490                         public void onTimeout(AsyncEvent event) throws IOException
491                         {
492                         }
493                         
494                         @Override
495                         public void onStartAsync(AsyncEvent event) throws IOException
496                         {
497                         }
498                         
499                         @Override
500                         public void onError(AsyncEvent event) throws IOException
501                         {
502                         }
503                         
504                         @Override
505                         public void onComplete(AsyncEvent event) throws IOException
506                         {
507                             try
508                             {
509                                 wrappedResponse.finish();
510                             }
511                             catch(IOException e)
512                             {
513                                 LOG.warn(e);
514                             }
515                         }
516                     });
517                 }
518                 else if (exceptional && !response.isCommitted())
519                 {
520                     wrappedResponse.resetBuffer();
521                     wrappedResponse.noCompression();
522                 }
523                 else
524                     wrappedResponse.finish();
525             }
526         }
527         else
528         {
529             _handler.handle(target,baseRequest, request, response);
530         }
531     }
532 
533     private boolean isGzippable(Request baseRequest, HttpServletRequest request, HttpServletResponse response)
534     {
535         String ae = request.getHeader("accept-encoding");
536         if (ae == null || !ae.contains("gzip"))
537         {
538             // Request not indicated for Gzip
539             return false;
540         }
541         
542         if(response.containsHeader("Content-Encoding"))
543         {
544             // Response is already declared, can't gzip
545             LOG.debug("{} excluded as Content-Encoding already declared {}",this,request);
546             return false;
547         }
548         
549         if(HttpMethod.HEAD.is(request.getMethod()))
550         {
551             // HEAD is never Gzip'd
552             LOG.debug("{} excluded by method {}",this,request);
553             return false;
554         }
555         
556         // Exclude based on Request Method
557         if (!_methods.matches(baseRequest.getMethod()))
558         {
559             LOG.debug("{} excluded by method {}",this,request);
560             return false;
561         }
562         
563         // Exclude based on Request Path
564         ServletContext context = baseRequest.getServletContext();
565         String path = context==null?baseRequest.getRequestURI():URIUtil.addPaths(baseRequest.getServletPath(),baseRequest.getPathInfo());
566 
567         if(path != null && !_paths.matches(path))
568         {
569             LOG.debug("{} excluded by path {}",this,request);
570             return false;
571         }
572         
573         
574         // Exclude non compressible mime-types known from URI extension. - no Vary because no matter what client, this URI is always excluded
575         String mimeType = context==null?null:context.getMimeType(path);
576         if (mimeType!=null)
577         {
578             mimeType = MimeTypes.getContentTypeWithoutCharset(mimeType);
579             if (!_mimeTypes.matches(mimeType))
580             {
581                 LOG.debug("{} excluded by path suffix mime type {}",this,request);
582                 return false;
583             }
584         }
585         
586         // Exclude on User Agent
587         String ua = request.getHeader("User-Agent");
588         if(ua != null && !_agentPatterns.matches(ua))
589         {
590             LOG.debug("{} excluded by user-agent {}",this,request);
591             return false;
592         }
593         
594         return true;
595     }
596 
597     /**
598      * Allows derived implementations to replace ResponseWrapper implementation.
599      *
600      * @param request the request
601      * @param response the response
602      * @return the gzip response wrapper
603      */
604     protected CompressedResponseWrapper newGzipResponseWrapper(HttpServletRequest request, HttpServletResponse response)
605     {
606         return new CompressedResponseWrapper(request,response)
607         {
608             {
609                 super.setMimeTypes(GzipHandler.this._mimeTypes);
610                 super.setBufferSize(GzipHandler.this._bufferSize);
611                 super.setMinCompressSize(GzipHandler.this._minGzipSize);
612             }
613 
614             @Override
615             protected AbstractCompressedStream newCompressedStream(HttpServletRequest request,HttpServletResponse response) throws IOException
616             {
617                 return new AbstractCompressedStream("gzip",request,this,_vary)
618                 {
619                     @Override
620                     protected DeflaterOutputStream createStream() throws IOException
621                     {
622                         return new GZIPOutputStream(_response.getOutputStream(),_bufferSize);
623                     }
624                 };
625             }
626 
627             @Override
628             protected PrintWriter newWriter(OutputStream out,String encoding) throws UnsupportedEncodingException
629             {
630                 return GzipHandler.this.newWriter(out,encoding);
631             }
632         };
633     }
634 
635     /**
636      * Allows derived implementations to replace PrintWriter implementation.
637      *
638      * @param out the out
639      * @param encoding the encoding
640      * @return the prints the writer
641      * @throws UnsupportedEncodingException
642      */
643     protected PrintWriter newWriter(OutputStream out,String encoding) throws UnsupportedEncodingException
644     {
645         return encoding==null?new PrintWriter(out):new PrintWriter(new OutputStreamWriter(out,encoding));
646     }
647 }