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.nio.ByteBuffer;
22  import java.nio.channels.WritePendingException;
23  import java.util.concurrent.atomic.AtomicReference;
24  import java.util.zip.CRC32;
25  import java.util.zip.Deflater;
26  
27  import org.eclipse.jetty.http.GzipHttpContent;
28  import org.eclipse.jetty.http.HttpField;
29  import org.eclipse.jetty.http.HttpFields;
30  import org.eclipse.jetty.http.HttpHeader;
31  import org.eclipse.jetty.http.MimeTypes;
32  import org.eclipse.jetty.http.PreEncodedHttpField;
33  import org.eclipse.jetty.server.HttpChannel;
34  import org.eclipse.jetty.server.HttpOutput;
35  import org.eclipse.jetty.util.BufferUtil;
36  import org.eclipse.jetty.util.Callback;
37  import org.eclipse.jetty.util.IteratingNestedCallback;
38  import org.eclipse.jetty.util.StringUtil;
39  import org.eclipse.jetty.util.log.Log;
40  import org.eclipse.jetty.util.log.Logger;
41  
42  public class GzipHttpOutputInterceptor implements HttpOutput.Interceptor
43  {
44      public static Logger LOG = Log.getLogger(GzipHttpOutputInterceptor.class);
45      private final static byte[] GZIP_HEADER = new byte[] { (byte)0x1f, (byte)0x8b, Deflater.DEFLATED, 0, 0, 0, 0, 0, 0, 0 };
46  
47      public final static HttpField VARY_ACCEPT_ENCODING_USER_AGENT=new PreEncodedHttpField(HttpHeader.VARY,HttpHeader.ACCEPT_ENCODING+", "+HttpHeader.USER_AGENT);
48      public final static HttpField VARY_ACCEPT_ENCODING=new PreEncodedHttpField(HttpHeader.VARY,HttpHeader.ACCEPT_ENCODING.asString());
49      
50      private enum GZState {  MIGHT_COMPRESS, NOT_COMPRESSING, COMMITTING, COMPRESSING, FINISHED};
51      private final AtomicReference<GZState> _state = new AtomicReference<>(GZState.MIGHT_COMPRESS);
52      private final CRC32 _crc = new CRC32();
53  
54      private final GzipFactory _factory;
55      private final HttpOutput.Interceptor _interceptor;
56      private final HttpChannel _channel;
57      private final HttpField _vary;
58      private final int _bufferSize;
59      private final boolean _syncFlush;
60      
61      private Deflater _deflater;
62      private ByteBuffer _buffer;
63  
64      public GzipHttpOutputInterceptor(GzipFactory factory, HttpChannel channel, HttpOutput.Interceptor next,boolean syncFlush)
65      {
66          this(factory,VARY_ACCEPT_ENCODING_USER_AGENT,channel.getHttpConfiguration().getOutputBufferSize(),channel,next,syncFlush);
67      }
68      
69      public GzipHttpOutputInterceptor(GzipFactory factory, HttpField vary, HttpChannel channel, HttpOutput.Interceptor next,boolean syncFlush)
70      {
71          this(factory,vary,channel.getHttpConfiguration().getOutputBufferSize(),channel,next,syncFlush);
72      }
73      
74      public GzipHttpOutputInterceptor(GzipFactory factory, HttpField vary, int bufferSize, HttpChannel channel, HttpOutput.Interceptor next,boolean syncFlush)
75      {
76          _factory=factory;
77          _channel=channel;
78          _interceptor=next;
79          _vary=vary;
80          _bufferSize=bufferSize;
81          _syncFlush=syncFlush;
82      }
83  
84      public HttpOutput.Interceptor getNextInterceptor()
85      {
86          return _interceptor;
87      }
88      
89      @Override
90      public boolean isOptimizedForDirectBuffers()
91      {
92          return false; // No point as deflator is in user space.
93      }
94      
95      
96      @Override
97      public void write(ByteBuffer content, boolean complete, Callback callback)
98      {
99          switch (_state.get())
100         {
101             case MIGHT_COMPRESS:
102                 commit(content,complete,callback);
103                 break;
104                 
105             case NOT_COMPRESSING:
106                 _interceptor.write(content, complete, callback);
107                 return;
108                 
109             case COMMITTING:
110                 callback.failed(new WritePendingException());
111                 break;
112 
113             case COMPRESSING:
114                 gzip(content,complete,callback);
115                 break;
116 
117             default:
118                 callback.failed(new IllegalStateException("state="+_state.get()));
119                 break;
120         }
121     }
122 
123     private void addTrailer()
124     {
125         int i=_buffer.limit();
126         _buffer.limit(i+8);
127         
128         int v=(int)_crc.getValue();
129         _buffer.put(i++,(byte)(v & 0xFF));
130         _buffer.put(i++,(byte)((v>>>8) & 0xFF));
131         _buffer.put(i++,(byte)((v>>>16) & 0xFF));
132         _buffer.put(i++,(byte)((v>>>24) & 0xFF));
133         
134         v=_deflater.getTotalIn();
135         _buffer.put(i++,(byte)(v & 0xFF));
136         _buffer.put(i++,(byte)((v>>>8) & 0xFF));
137         _buffer.put(i++,(byte)((v>>>16) & 0xFF));
138         _buffer.put(i++,(byte)((v>>>24) & 0xFF));
139     }
140     
141     
142     private void gzip(ByteBuffer content, boolean complete, final Callback callback)
143     {
144         if (content.hasRemaining() || complete)
145             new GzipBufferCB(content,complete,callback).iterate();
146         else
147             callback.succeeded();
148     }
149 
150     protected void commit(ByteBuffer content, boolean complete, Callback callback)
151     {
152         // Are we excluding because of status?
153         int sc = _channel.getResponse().getStatus();
154         if (sc>0 && (sc<200 || sc==204 || sc==205 || sc>=300))
155         {
156             LOG.debug("{} exclude by status {}",this,sc);
157             noCompression();
158             _interceptor.write(content, complete, callback);
159             return;
160         }
161         
162         // Are we excluding because of mime-type?
163         String ct = _channel.getResponse().getContentType();
164         if (ct!=null)
165         {
166             ct=MimeTypes.getContentTypeWithoutCharset(ct);
167             if (!_factory.isMimeTypeGzipable(StringUtil.asciiToLowerCase(ct)))
168             {
169                 LOG.debug("{} exclude by mimeType {}",this,ct);
170                 noCompression();
171                 _interceptor.write(content, complete, callback);
172                 return;
173             }
174         }
175         
176         // Has the Content-Encoding header already been set?
177         String ce=_channel.getResponse().getHeader("Content-Encoding");
178         if (ce != null)
179         {
180             LOG.debug("{} exclude by content-encoding {}",this,ce);
181             noCompression();
182             _interceptor.write(content, complete, callback);
183             return;
184         }
185         
186         // Are we the thread that commits?
187         if (_state.compareAndSet(GZState.MIGHT_COMPRESS,GZState.COMMITTING))
188         {
189             // We are varying the response due to accept encoding header.
190             HttpFields fields = _channel.getResponse().getHttpFields();
191             fields.add(_vary);
192 
193             long content_length = _channel.getResponse().getContentLength();
194             if (content_length<0 && complete)
195                 content_length=content.remaining();
196             
197             _deflater = _factory.getDeflater(_channel.getRequest(),content_length);
198             
199             if (_deflater==null)
200             {
201                 LOG.debug("{} exclude no deflater",this);
202                 _state.set(GZState.NOT_COMPRESSING);
203                 _interceptor.write(content, complete, callback);
204                 return;
205             }
206 
207             fields.put(GzipHttpContent.CONTENT_ENCODING_GZIP);
208             _crc.reset();
209             _buffer=_channel.getByteBufferPool().acquire(_bufferSize,false);
210             BufferUtil.fill(_buffer,GZIP_HEADER,0,GZIP_HEADER.length);
211 
212             // Adjust headers
213             _channel.getResponse().setContentLength(-1);
214             String etag=fields.get(HttpHeader.ETAG);
215             if (etag!=null)
216             {
217                 int end = etag.length()-1;
218                 etag=(etag.charAt(end)=='"')?etag.substring(0,end)+GzipHttpContent.ETAG_GZIP+'"':etag+GzipHttpContent.ETAG_GZIP;
219                 fields.put(HttpHeader.ETAG,etag);
220             }
221             
222             LOG.debug("{} compressing {}",this,_deflater);
223             _state.set(GZState.COMPRESSING);
224             
225             gzip(content,complete,callback);
226         }
227         else
228             callback.failed(new WritePendingException());
229     }
230 
231     public void noCompression()
232     {
233         while (true)
234         {
235             switch (_state.get())
236             {
237                 case NOT_COMPRESSING:
238                     return;
239 
240                 case MIGHT_COMPRESS:
241                     if (_state.compareAndSet(GZState.MIGHT_COMPRESS,GZState.NOT_COMPRESSING))
242                         return;
243                     break;
244 
245                 default:
246                     throw new IllegalStateException(_state.get().toString());
247             }
248         }
249     }
250 
251     public void noCompressionIfPossible()
252     {
253         while (true)
254         {
255             switch (_state.get())
256             {
257                 case COMPRESSING:
258                 case NOT_COMPRESSING:
259                     return;
260 
261                 case MIGHT_COMPRESS:
262                     if (_state.compareAndSet(GZState.MIGHT_COMPRESS,GZState.NOT_COMPRESSING))
263                         return;
264                     break;
265 
266                 default:
267                     throw new IllegalStateException(_state.get().toString());
268             }
269         }
270     }
271     
272     public boolean mightCompress()
273     {
274         return _state.get()==GZState.MIGHT_COMPRESS;
275     }
276     
277     private class GzipBufferCB extends IteratingNestedCallback
278     {        
279         private ByteBuffer _copy;
280         private final ByteBuffer _content;
281         private final boolean _last;
282         public GzipBufferCB(ByteBuffer content, boolean complete, Callback callback)
283         {
284             super(callback);
285             _content=content;
286             _last=complete;
287         }
288 
289         @Override
290         protected Action process() throws Exception
291         {
292             if (_deflater==null)
293                 return Action.SUCCEEDED;
294                 
295             if (_deflater.needsInput())
296             {                
297                 if (BufferUtil.isEmpty(_content))
298                 {                    
299                     if (_deflater.finished())
300                     {
301                         _factory.recycle(_deflater);
302                         _deflater=null;
303                         _channel.getByteBufferPool().release(_buffer);
304                         _buffer=null;
305                         if (_copy!=null)
306                         {
307                             _channel.getByteBufferPool().release(_copy);
308                             _copy=null;
309                         }
310                         return Action.SUCCEEDED;
311                     }
312                     
313                     if (!_last)
314                     {
315                         return Action.SUCCEEDED;
316                     }
317                     
318                     _deflater.finish();
319                 }
320                 else if (_content.hasArray())
321                 {
322                     byte[] array=_content.array();
323                     int off=_content.arrayOffset()+_content.position();
324                     int len=_content.remaining();
325                     BufferUtil.clear(_content);
326                     
327                     _crc.update(array,off,len);
328                     _deflater.setInput(array,off,len);                
329                     if (_last)
330                         _deflater.finish();
331                 }
332                 else
333                 {
334                     if (_copy==null)
335                         _copy=_channel.getByteBufferPool().acquire(_bufferSize,false);
336                     BufferUtil.clearToFill(_copy);
337                     int took=BufferUtil.put(_content,_copy);
338                     BufferUtil.flipToFlush(_copy,0);
339                     if (took==0)
340                         throw new IllegalStateException();
341                    
342                     byte[] array=_copy.array();
343                     int off=_copy.arrayOffset()+_copy.position();
344                     int len=_copy.remaining();
345 
346                     _crc.update(array,off,len);
347                     _deflater.setInput(array,off,len);                
348                     if (_last && BufferUtil.isEmpty(_content))
349                         _deflater.finish();
350                 }
351             }
352 
353             BufferUtil.compact(_buffer);
354             int off=_buffer.arrayOffset()+_buffer.limit();
355             int len=_buffer.capacity()-_buffer.limit() - (_last?8:0);
356             if (len>0)
357             {
358                 int produced=_deflater.deflate(_buffer.array(),off,len,_syncFlush?Deflater.SYNC_FLUSH:Deflater.NO_FLUSH);
359                 _buffer.limit(_buffer.limit()+produced);
360             }
361             boolean finished=_deflater.finished();
362             
363             if (finished)
364                 addTrailer();
365                 
366             _interceptor.write(_buffer,finished,this);
367             return Action.SCHEDULED;
368         }
369     }
370 }