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