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