1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.jetty.servlets;
20
21 import java.io.IOException;
22 import java.util.HashSet;
23 import java.util.Locale;
24 import java.util.Set;
25 import java.util.StringTokenizer;
26 import java.util.regex.Pattern;
27 import java.util.zip.Deflater;
28 import java.util.zip.DeflaterOutputStream;
29 import java.util.zip.GZIPOutputStream;
30
31 import javax.servlet.FilterChain;
32 import javax.servlet.FilterConfig;
33 import javax.servlet.ServletContext;
34 import javax.servlet.ServletException;
35 import javax.servlet.ServletRequest;
36 import javax.servlet.ServletResponse;
37 import javax.servlet.ServletResponseWrapper;
38 import javax.servlet.http.HttpServletRequest;
39 import javax.servlet.http.HttpServletResponse;
40 import javax.servlet.http.HttpServletResponseWrapper;
41
42 import org.eclipse.jetty.continuation.Continuation;
43 import org.eclipse.jetty.continuation.ContinuationListener;
44 import org.eclipse.jetty.continuation.ContinuationSupport;
45 import org.eclipse.jetty.http.HttpMethods;
46 import org.eclipse.jetty.http.gzip.CompressedResponseWrapper;
47 import org.eclipse.jetty.http.gzip.AbstractCompressedStream;
48 import org.eclipse.jetty.util.StringUtil;
49 import org.eclipse.jetty.util.log.Log;
50 import org.eclipse.jetty.util.log.Logger;
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117 public class GzipFilter extends UserAgentFilter
118 {
119 private static final Logger LOG = Log.getLogger(GzipFilter.class);
120 public final static String GZIP="gzip";
121 public final static String ETAG_GZIP="--gzip\"";
122 public final static String DEFLATE="deflate";
123 public final static String ETAG_DEFLATE="--deflate\"";
124 public final static String ETAG="o.e.j.s.GzipFilter.ETag";
125
126 protected ServletContext _context;
127 protected Set<String> _mimeTypes;
128 protected int _bufferSize=8192;
129 protected int _minGzipSize=256;
130 protected int _deflateCompressionLevel=Deflater.DEFAULT_COMPRESSION;
131 protected boolean _deflateNoWrap = true;
132
133 protected final Set<String> _methods=new HashSet<String>();
134 protected Set<String> _excludedAgents;
135 protected Set<Pattern> _excludedAgentPatterns;
136 protected Set<String> _excludedPaths;
137 protected Set<Pattern> _excludedPathPatterns;
138 protected String _vary="Accept-Encoding, User-Agent";
139
140 private static final int STATE_SEPARATOR = 0;
141 private static final int STATE_Q = 1;
142 private static final int STATE_QVALUE = 2;
143 private static final int STATE_DEFAULT = 3;
144
145
146
147
148
149
150 @Override
151 public void init(FilterConfig filterConfig) throws ServletException
152 {
153 super.init(filterConfig);
154
155 _context=filterConfig.getServletContext();
156
157 String tmp=filterConfig.getInitParameter("bufferSize");
158 if (tmp!=null)
159 _bufferSize=Integer.parseInt(tmp);
160
161 tmp=filterConfig.getInitParameter("minGzipSize");
162 if (tmp!=null)
163 _minGzipSize=Integer.parseInt(tmp);
164
165 tmp=filterConfig.getInitParameter("deflateCompressionLevel");
166 if (tmp!=null)
167 _deflateCompressionLevel=Integer.parseInt(tmp);
168
169 tmp=filterConfig.getInitParameter("deflateNoWrap");
170 if (tmp!=null)
171 _deflateNoWrap=Boolean.parseBoolean(tmp);
172
173 tmp=filterConfig.getInitParameter("methods");
174 if (tmp!=null)
175 {
176 StringTokenizer tok = new StringTokenizer(tmp,",",false);
177 while (tok.hasMoreTokens())
178 _methods.add(tok.nextToken().trim().toUpperCase());
179 }
180 else
181 _methods.add(HttpMethods.GET);
182
183 tmp=filterConfig.getInitParameter("mimeTypes");
184 if (tmp!=null)
185 {
186 _mimeTypes=new HashSet<String>();
187 StringTokenizer tok = new StringTokenizer(tmp,",",false);
188 while (tok.hasMoreTokens())
189 _mimeTypes.add(tok.nextToken());
190 }
191 tmp=filterConfig.getInitParameter("excludedAgents");
192 if (tmp!=null)
193 {
194 _excludedAgents=new HashSet<String>();
195 StringTokenizer tok = new StringTokenizer(tmp,",",false);
196 while (tok.hasMoreTokens())
197 _excludedAgents.add(tok.nextToken());
198 }
199
200 tmp=filterConfig.getInitParameter("excludeAgentPatterns");
201 if (tmp!=null)
202 {
203 _excludedAgentPatterns=new HashSet<Pattern>();
204 StringTokenizer tok = new StringTokenizer(tmp,",",false);
205 while (tok.hasMoreTokens())
206 _excludedAgentPatterns.add(Pattern.compile(tok.nextToken()));
207 }
208
209 tmp=filterConfig.getInitParameter("excludePaths");
210 if (tmp!=null)
211 {
212 _excludedPaths=new HashSet<String>();
213 StringTokenizer tok = new StringTokenizer(tmp,",",false);
214 while (tok.hasMoreTokens())
215 _excludedPaths.add(tok.nextToken());
216 }
217
218 tmp=filterConfig.getInitParameter("excludePathPatterns");
219 if (tmp!=null)
220 {
221 _excludedPathPatterns=new HashSet<Pattern>();
222 StringTokenizer tok = new StringTokenizer(tmp,",",false);
223 while (tok.hasMoreTokens())
224 _excludedPathPatterns.add(Pattern.compile(tok.nextToken()));
225 }
226
227 tmp=filterConfig.getInitParameter("vary");
228 if (tmp!=null)
229 _vary=tmp;
230 }
231
232
233
234
235
236 @Override
237 public void destroy()
238 {
239 }
240
241
242
243
244
245 @Override
246 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
247 throws IOException, ServletException
248 {
249 HttpServletRequest request=(HttpServletRequest)req;
250 HttpServletResponse response=(HttpServletResponse)res;
251
252
253 String requestURI = request.getRequestURI();
254 if (!_methods.contains(request.getMethod()) || isExcludedPath(requestURI))
255 {
256 super.doFilter(request,response,chain);
257 return;
258 }
259
260
261 if (_mimeTypes!=null && _mimeTypes.size()>0)
262 {
263 String mimeType = _context.getMimeType(request.getRequestURI());
264
265 if (mimeType!=null && !_mimeTypes.contains(mimeType))
266 {
267
268 super.doFilter(request,response,chain);
269 return;
270 }
271 }
272
273
274 String ua = getUserAgent(request);
275 boolean ua_excluded=ua!=null&&isExcludedAgent(ua);
276
277
278 String compressionType = ua_excluded?null:selectCompression(request.getHeader("accept-encoding"));
279
280
281 String etag = request.getHeader("If-None-Match");
282 if (etag!=null)
283 {
284 int dd=etag.indexOf("--");
285 if (dd>0)
286 request.setAttribute(ETAG,etag.substring(0,dd)+(etag.endsWith("\"")?"\"":""));
287 }
288
289 CompressedResponseWrapper wrappedResponse = createWrappedResponse(request,response,compressionType);
290
291 boolean exceptional=true;
292 try
293 {
294 super.doFilter(request,wrappedResponse,chain);
295 exceptional=false;
296 }
297 finally
298 {
299 Continuation continuation = ContinuationSupport.getContinuation(request);
300 if (continuation.isSuspended() && continuation.isResponseWrapped())
301 {
302 continuation.addContinuationListener(new ContinuationListenerWaitingForWrappedResponseToFinish(wrappedResponse));
303 }
304 else if (exceptional && !response.isCommitted())
305 {
306 wrappedResponse.resetBuffer();
307 wrappedResponse.noCompression();
308 }
309 else
310 wrappedResponse.finish();
311 }
312 }
313
314
315 private String selectCompression(String encodingHeader)
316 {
317
318
319 String compression = null;
320 if (encodingHeader!=null)
321 {
322
323 String[] encodings = getEncodings(encodingHeader);
324 if (encodings != null)
325 {
326 for (int i=0; i< encodings.length; i++)
327 {
328 if (encodings[i].toLowerCase(Locale.ENGLISH).contains(GZIP))
329 {
330 if (isEncodingAcceptable(encodings[i]))
331 {
332 compression = GZIP;
333 break;
334 }
335 }
336
337 if (encodings[i].toLowerCase(Locale.ENGLISH).contains(DEFLATE))
338 {
339 if (isEncodingAcceptable(encodings[i]))
340 {
341 compression = DEFLATE;
342 }
343 }
344 }
345 }
346 }
347 return compression;
348 }
349
350
351 private String[] getEncodings (String encodingHeader)
352 {
353 if (encodingHeader == null)
354 return null;
355 return encodingHeader.split(",");
356 }
357
358 private boolean isEncodingAcceptable(String encoding)
359 {
360 int state = STATE_DEFAULT;
361 int qvalueIdx = -1;
362 for (int i=0;i<encoding.length();i++)
363 {
364 char c = encoding.charAt(i);
365 switch (state)
366 {
367 case STATE_DEFAULT:
368 {
369 if (';' == c)
370 state = STATE_SEPARATOR;
371 break;
372 }
373 case STATE_SEPARATOR:
374 {
375 if ('q' == c || 'Q' == c)
376 state = STATE_Q;
377 break;
378 }
379 case STATE_Q:
380 {
381 if ('=' == c)
382 state = STATE_QVALUE;
383 break;
384 }
385 case STATE_QVALUE:
386 {
387 if (qvalueIdx < 0 && '0' == c || '1' == c)
388 qvalueIdx = i;
389 break;
390 }
391 }
392 }
393
394 if (qvalueIdx < 0)
395 return true;
396
397 if ("0".equals(encoding.substring(qvalueIdx).trim()))
398 return false;
399 return true;
400 }
401
402
403 protected CompressedResponseWrapper createWrappedResponse(HttpServletRequest request, HttpServletResponse response, final String compressionType)
404 {
405 CompressedResponseWrapper wrappedResponse = null;
406 if (compressionType==null)
407 {
408 wrappedResponse = new CompressedResponseWrapper(request,response)
409 {
410 @Override
411 protected AbstractCompressedStream newCompressedStream(HttpServletRequest request,HttpServletResponse response) throws IOException
412 {
413 return new AbstractCompressedStream(null,request,this,_vary)
414 {
415 @Override
416 protected DeflaterOutputStream createStream() throws IOException
417 {
418 return null;
419 }
420 };
421 }
422 };
423 }
424 else if (compressionType.equals(GZIP))
425 {
426 wrappedResponse = new CompressedResponseWrapper(request,response)
427 {
428 @Override
429 protected AbstractCompressedStream newCompressedStream(HttpServletRequest request,HttpServletResponse response) throws IOException
430 {
431 return new AbstractCompressedStream(compressionType,request,this,_vary)
432 {
433 @Override
434 protected DeflaterOutputStream createStream() throws IOException
435 {
436 return new GZIPOutputStream(_response.getOutputStream(),_bufferSize);
437 }
438 };
439 }
440 };
441 }
442 else if (compressionType.equals(DEFLATE))
443 {
444 wrappedResponse = new CompressedResponseWrapper(request,response)
445 {
446 @Override
447 protected AbstractCompressedStream newCompressedStream(HttpServletRequest request,HttpServletResponse response) throws IOException
448 {
449 return new AbstractCompressedStream(compressionType,request,this,_vary)
450 {
451 @Override
452 protected DeflaterOutputStream createStream() throws IOException
453 {
454 return new DeflaterOutputStream(_response.getOutputStream(),new Deflater(_deflateCompressionLevel,_deflateNoWrap));
455 }
456 };
457 }
458 };
459 }
460 else
461 {
462 throw new IllegalStateException(compressionType + " not supported");
463 }
464 configureWrappedResponse(wrappedResponse);
465 return wrappedResponse;
466 }
467
468 protected void configureWrappedResponse(CompressedResponseWrapper wrappedResponse)
469 {
470 wrappedResponse.setMimeTypes(_mimeTypes);
471 wrappedResponse.setBufferSize(_bufferSize);
472 wrappedResponse.setMinCompressSize(_minGzipSize);
473 }
474
475 private class ContinuationListenerWaitingForWrappedResponseToFinish implements ContinuationListener
476 {
477 private CompressedResponseWrapper wrappedResponse;
478
479 public ContinuationListenerWaitingForWrappedResponseToFinish(CompressedResponseWrapper wrappedResponse)
480 {
481 this.wrappedResponse = wrappedResponse;
482 }
483
484 public void onComplete(Continuation continuation)
485 {
486 try
487 {
488 wrappedResponse.finish();
489 }
490 catch (IOException e)
491 {
492 LOG.warn(e);
493 }
494 }
495
496 public void onTimeout(Continuation continuation)
497 {
498 }
499 }
500
501
502
503
504
505
506
507
508 private boolean isExcludedAgent(String ua)
509 {
510 if (ua == null)
511 return false;
512
513 if (_excludedAgents != null)
514 {
515 if (_excludedAgents.contains(ua))
516 {
517 return true;
518 }
519 }
520 if (_excludedAgentPatterns != null)
521 {
522 for (Pattern pattern : _excludedAgentPatterns)
523 {
524 if (pattern.matcher(ua).matches())
525 {
526 return true;
527 }
528 }
529 }
530
531 return false;
532 }
533
534
535
536
537
538
539
540
541 private boolean isExcludedPath(String requestURI)
542 {
543 if (requestURI == null)
544 return false;
545 if (_excludedPaths != null)
546 {
547 for (String excludedPath : _excludedPaths)
548 {
549 if (requestURI.startsWith(excludedPath))
550 {
551 return true;
552 }
553 }
554 }
555 if (_excludedPathPatterns != null)
556 {
557 for (Pattern pattern : _excludedPathPatterns)
558 {
559 if (pattern.matcher(requestURI).matches())
560 {
561 return true;
562 }
563 }
564 }
565 return false;
566 }
567 }