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.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;
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
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
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
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
204 if (_state.compareAndSet(GZState.MIGHT_COMPRESS,GZState.COMMITTING))
205 {
206
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
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 }