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.File;
22 import java.io.IOException;
23 import java.util.HashSet;
24 import java.util.Locale;
25 import java.util.Set;
26 import java.util.StringTokenizer;
27 import java.util.regex.Pattern;
28 import java.util.zip.Deflater;
29 import java.util.zip.DeflaterOutputStream;
30
31 import javax.servlet.AsyncEvent;
32 import javax.servlet.AsyncListener;
33 import javax.servlet.FilterChain;
34 import javax.servlet.FilterConfig;
35 import javax.servlet.ServletContext;
36 import javax.servlet.ServletException;
37 import javax.servlet.ServletRequest;
38 import javax.servlet.ServletResponse;
39 import javax.servlet.http.HttpServletRequest;
40 import javax.servlet.http.HttpServletResponse;
41
42 import org.eclipse.jetty.http.HttpMethod;
43 import org.eclipse.jetty.http.MimeTypes;
44 import org.eclipse.jetty.servlets.gzip.AbstractCompressedStream;
45 import org.eclipse.jetty.servlets.gzip.CompressedResponseWrapper;
46 import org.eclipse.jetty.servlets.gzip.GzipOutputStream;
47 import org.eclipse.jetty.util.URIUtil;
48 import org.eclipse.jetty.util.log.Log;
49 import org.eclipse.jetty.util.log.Logger;
50
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
118
119
120
121
122
123
124 public class GzipFilter extends UserAgentFilter
125 {
126 private static final Logger LOG = Log.getLogger(GzipFilter.class);
127 public final static String GZIP="gzip";
128 public final static String ETAG_GZIP="--gzip\"";
129 public final static String DEFLATE="deflate";
130 public final static String ETAG_DEFLATE="--deflate\"";
131 public final static String ETAG="o.e.j.s.GzipFilter.ETag";
132
133 protected ServletContext _context;
134 protected final Set<String> _mimeTypes=new HashSet<>();
135 protected boolean _excludeMimeTypes;
136 protected int _bufferSize=8192;
137 protected int _minGzipSize=256;
138 protected int _deflateCompressionLevel=Deflater.DEFAULT_COMPRESSION;
139 protected boolean _deflateNoWrap = true;
140 protected boolean _checkGzExists = true;
141
142
143 protected final ThreadLocal<Deflater> _deflater = new ThreadLocal<Deflater>();
144
145 protected final Set<String> _methods=new HashSet<String>();
146 protected Set<String> _excludedAgents;
147 protected Set<Pattern> _excludedAgentPatterns;
148 protected Set<String> _excludedPaths;
149 protected Set<Pattern> _excludedPathPatterns;
150 protected String _vary="Accept-Encoding, User-Agent";
151
152 private static final int STATE_SEPARATOR = 0;
153 private static final int STATE_Q = 1;
154 private static final int STATE_QVALUE = 2;
155 private static final int STATE_DEFAULT = 3;
156
157
158
159
160
161
162 @Override
163 public void init(FilterConfig filterConfig) throws ServletException
164 {
165 super.init(filterConfig);
166
167 _context=filterConfig.getServletContext();
168
169 String tmp=filterConfig.getInitParameter("bufferSize");
170 if (tmp!=null)
171 _bufferSize=Integer.parseInt(tmp);
172
173 tmp=filterConfig.getInitParameter("minGzipSize");
174 if (tmp!=null)
175 _minGzipSize=Integer.parseInt(tmp);
176
177 tmp=filterConfig.getInitParameter("deflateCompressionLevel");
178 if (tmp!=null)
179 _deflateCompressionLevel=Integer.parseInt(tmp);
180
181 tmp=filterConfig.getInitParameter("deflateNoWrap");
182 if (tmp!=null)
183 _deflateNoWrap=Boolean.parseBoolean(tmp);
184
185 tmp=filterConfig.getInitParameter("checkGzExists");
186 if (tmp!=null)
187 _checkGzExists=Boolean.parseBoolean(tmp);
188
189 tmp=filterConfig.getInitParameter("methods");
190 if (tmp!=null)
191 {
192 StringTokenizer tok = new StringTokenizer(tmp,",",false);
193 while (tok.hasMoreTokens())
194 _methods.add(tok.nextToken().trim().toUpperCase());
195 }
196 else
197 _methods.add(HttpMethod.GET.asString());
198
199 tmp=filterConfig.getInitParameter("mimeTypes");
200 if (tmp==null)
201 {
202 _excludeMimeTypes=true;
203 tmp=filterConfig.getInitParameter("excludedMimeTypes");
204 if (tmp==null)
205 {
206 for (String type:MimeTypes.getKnownMimeTypes())
207 {
208 if (type.startsWith("image/")||
209 type.startsWith("audio/")||
210 type.startsWith("video/"))
211 _mimeTypes.add(type);
212 _mimeTypes.add("application/compress");
213 _mimeTypes.add("application/zip");
214 _mimeTypes.add("application/gzip");
215 }
216 }
217 else
218 {
219 StringTokenizer tok = new StringTokenizer(tmp,",",false);
220 while (tok.hasMoreTokens())
221 _mimeTypes.add(tok.nextToken());
222 }
223 }
224 else
225 {
226 StringTokenizer tok = new StringTokenizer(tmp,",",false);
227 while (tok.hasMoreTokens())
228 _mimeTypes.add(tok.nextToken());
229 }
230 tmp=filterConfig.getInitParameter("excludedAgents");
231 if (tmp!=null)
232 {
233 _excludedAgents=new HashSet<String>();
234 StringTokenizer tok = new StringTokenizer(tmp,",",false);
235 while (tok.hasMoreTokens())
236 _excludedAgents.add(tok.nextToken());
237 }
238
239 tmp=filterConfig.getInitParameter("excludeAgentPatterns");
240 if (tmp!=null)
241 {
242 _excludedAgentPatterns=new HashSet<Pattern>();
243 StringTokenizer tok = new StringTokenizer(tmp,",",false);
244 while (tok.hasMoreTokens())
245 _excludedAgentPatterns.add(Pattern.compile(tok.nextToken()));
246 }
247
248 tmp=filterConfig.getInitParameter("excludePaths");
249 if (tmp!=null)
250 {
251 _excludedPaths=new HashSet<String>();
252 StringTokenizer tok = new StringTokenizer(tmp,",",false);
253 while (tok.hasMoreTokens())
254 _excludedPaths.add(tok.nextToken());
255 }
256
257 tmp=filterConfig.getInitParameter("excludePathPatterns");
258 if (tmp!=null)
259 {
260 _excludedPathPatterns=new HashSet<Pattern>();
261 StringTokenizer tok = new StringTokenizer(tmp,",",false);
262 while (tok.hasMoreTokens())
263 _excludedPathPatterns.add(Pattern.compile(tok.nextToken()));
264 }
265
266 tmp=filterConfig.getInitParameter("vary");
267 if (tmp!=null)
268 _vary=tmp;
269 }
270
271
272
273
274
275 @Override
276 public void destroy()
277 {
278 }
279
280
281
282
283
284 @Override
285 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
286 throws IOException, ServletException
287 {
288 HttpServletRequest request=(HttpServletRequest)req;
289 HttpServletResponse response=(HttpServletResponse)res;
290
291
292 String requestURI = request.getRequestURI();
293 if (!_methods.contains(request.getMethod()) || isExcludedPath(requestURI))
294 {
295 super.doFilter(request,response,chain);
296 return;
297 }
298
299
300 if (_mimeTypes.size()>0)
301 {
302 String mimeType = _context.getMimeType(request.getRequestURI());
303
304 if (mimeType!=null && _mimeTypes.contains(mimeType)==_excludeMimeTypes)
305 {
306
307 super.doFilter(request,response,chain);
308 return;
309 }
310 }
311
312 if (_checkGzExists && request.getServletContext()!=null)
313 {
314 String path=request.getServletContext().getRealPath(URIUtil.addPaths(request.getServletPath(),request.getPathInfo()));
315 if (path!=null)
316 {
317 File gz=new File(path+".gz");
318 if (gz.exists())
319 {
320
321 super.doFilter(request,response,chain);
322 return;
323 }
324 }
325 }
326
327
328 String ua = getUserAgent(request);
329 boolean ua_excluded=ua!=null&&isExcludedAgent(ua);
330
331
332 String compressionType = ua_excluded?null:selectCompression(request.getHeader("accept-encoding"));
333
334
335 String etag = request.getHeader("If-None-Match");
336 if (etag!=null)
337 {
338 int dd=etag.indexOf("--");
339 if (dd>0)
340 request.setAttribute(ETAG,etag.substring(0,dd)+(etag.endsWith("\"")?"\"":""));
341 }
342
343 CompressedResponseWrapper wrappedResponse = createWrappedResponse(request,response,compressionType);
344
345 boolean exceptional=true;
346 try
347 {
348 super.doFilter(request,wrappedResponse,chain);
349 exceptional=false;
350 }
351 finally
352 {
353 if (request.isAsyncStarted())
354 {
355
356 request.getAsyncContext().addListener(new FinishOnCompleteListener(wrappedResponse));
357 }
358 else if (exceptional && !response.isCommitted())
359 {
360 wrappedResponse.resetBuffer();
361 wrappedResponse.noCompression();
362 }
363 else
364 wrappedResponse.finish();
365 }
366 }
367
368
369 private String selectCompression(String encodingHeader)
370 {
371
372
373 String compression = null;
374 if (encodingHeader!=null)
375 {
376
377 String[] encodings = getEncodings(encodingHeader);
378 if (encodings != null)
379 {
380 for (int i=0; i< encodings.length; i++)
381 {
382 if (encodings[i].toLowerCase(Locale.ENGLISH).contains(GZIP))
383 {
384 if (isEncodingAcceptable(encodings[i]))
385 {
386 compression = GZIP;
387 break;
388 }
389 }
390
391 if (encodings[i].toLowerCase(Locale.ENGLISH).contains(DEFLATE))
392 {
393 if (isEncodingAcceptable(encodings[i]))
394 {
395 compression = DEFLATE;
396 }
397 }
398 }
399 }
400 }
401 return compression;
402 }
403
404
405 private String[] getEncodings (String encodingHeader)
406 {
407 if (encodingHeader == null)
408 return null;
409 return encodingHeader.split(",");
410 }
411
412 private boolean isEncodingAcceptable(String encoding)
413 {
414 int state = STATE_DEFAULT;
415 int qvalueIdx = -1;
416 for (int i=0;i<encoding.length();i++)
417 {
418 char c = encoding.charAt(i);
419 switch (state)
420 {
421 case STATE_DEFAULT:
422 {
423 if (';' == c)
424 state = STATE_SEPARATOR;
425 break;
426 }
427 case STATE_SEPARATOR:
428 {
429 if ('q' == c || 'Q' == c)
430 state = STATE_Q;
431 break;
432 }
433 case STATE_Q:
434 {
435 if ('=' == c)
436 state = STATE_QVALUE;
437 break;
438 }
439 case STATE_QVALUE:
440 {
441 if (qvalueIdx < 0 && '0' == c || '1' == c)
442 qvalueIdx = i;
443 break;
444 }
445 }
446 }
447
448 if (qvalueIdx < 0)
449 return true;
450
451 if ("0".equals(encoding.substring(qvalueIdx).trim()))
452 return false;
453 return true;
454 }
455
456
457 protected CompressedResponseWrapper createWrappedResponse(HttpServletRequest request, HttpServletResponse response, final String compressionType)
458 {
459 CompressedResponseWrapper wrappedResponse = null;
460 wrappedResponse = new CompressedResponseWrapper(request,response)
461 {
462 @Override
463 protected AbstractCompressedStream newCompressedStream(HttpServletRequest request, HttpServletResponse response) throws IOException
464 {
465 return new AbstractCompressedStream(compressionType,request,this,_vary)
466 {
467 private Deflater _allocatedDeflater;
468
469 @Override
470 protected DeflaterOutputStream createStream() throws IOException
471 {
472 if (compressionType == null)
473 {
474 return null;
475 }
476
477
478 _allocatedDeflater = _deflater.get();
479 if (_allocatedDeflater==null)
480 _allocatedDeflater = new Deflater(_deflateCompressionLevel,_deflateNoWrap);
481 else
482 {
483 _deflater.remove();
484 _allocatedDeflater.reset();
485 }
486
487 switch (compressionType)
488 {
489 case GZIP:
490 return new GzipOutputStream(_response.getOutputStream(),_allocatedDeflater,_bufferSize);
491 case DEFLATE:
492 return new DeflaterOutputStream(_response.getOutputStream(),_allocatedDeflater,_bufferSize);
493 }
494 throw new IllegalStateException(compressionType + " not supported");
495 }
496
497 @Override
498 public void finish() throws IOException
499 {
500 super.finish();
501 if (_allocatedDeflater != null && _deflater.get() == null)
502 {
503 _deflater.set(_allocatedDeflater);
504 }
505 }
506 };
507 }
508 };
509 configureWrappedResponse(wrappedResponse);
510 return wrappedResponse;
511 }
512
513 protected void configureWrappedResponse(CompressedResponseWrapper wrappedResponse)
514 {
515 wrappedResponse.setMimeTypes(_mimeTypes,_excludeMimeTypes);
516 wrappedResponse.setBufferSize(_bufferSize);
517 wrappedResponse.setMinCompressSize(_minGzipSize);
518 }
519
520 private class FinishOnCompleteListener implements AsyncListener
521 {
522 private CompressedResponseWrapper wrappedResponse;
523
524 public FinishOnCompleteListener(CompressedResponseWrapper wrappedResponse)
525 {
526 this.wrappedResponse = wrappedResponse;
527 }
528
529 @Override
530 public void onComplete(AsyncEvent event) throws IOException
531 {
532 try
533 {
534 wrappedResponse.finish();
535 }
536 catch (IOException e)
537 {
538 LOG.warn(e);
539 }
540 }
541
542 @Override
543 public void onTimeout(AsyncEvent event) throws IOException
544 {
545 }
546
547 @Override
548 public void onError(AsyncEvent event) throws IOException
549 {
550 }
551
552 @Override
553 public void onStartAsync(AsyncEvent event) throws IOException
554 {
555 }
556 }
557
558
559
560
561
562
563
564
565 private boolean isExcludedAgent(String ua)
566 {
567 if (ua == null)
568 return false;
569
570 if (_excludedAgents != null)
571 {
572 if (_excludedAgents.contains(ua))
573 {
574 return true;
575 }
576 }
577 if (_excludedAgentPatterns != null)
578 {
579 for (Pattern pattern : _excludedAgentPatterns)
580 {
581 if (pattern.matcher(ua).matches())
582 {
583 return true;
584 }
585 }
586 }
587
588 return false;
589 }
590
591
592
593
594
595
596
597
598 private boolean isExcludedPath(String requestURI)
599 {
600 if (requestURI == null)
601 return false;
602 if (_excludedPaths != null)
603 {
604 for (String excludedPath : _excludedPaths)
605 {
606 if (requestURI.startsWith(excludedPath))
607 {
608 return true;
609 }
610 }
611 }
612 if (_excludedPathPatterns != null)
613 {
614 for (Pattern pattern : _excludedPathPatterns)
615 {
616 if (pattern.matcher(requestURI).matches())
617 {
618 return true;
619 }
620 }
621 }
622 return false;
623 }
624 }