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.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;
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
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
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
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
187 if (_state.compareAndSet(GZState.MIGHT_COMPRESS,GZState.COMMITTING))
188 {
189
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
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 }