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