1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
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
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
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
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
189 if (_state.compareAndSet(GZState.MIGHT_COMPRESS,GZState.COMMITTING))
190 {
191
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
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 }