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 request.getAsyncContext().addListener(new FinishOnCompleteListener(wrappedResponse));
356 }
357 else if (exceptional && !response.isCommitted())
358 {
359 wrappedResponse.resetBuffer();
360 wrappedResponse.noCompression();
361 }
362 else
363 wrappedResponse.finish();
364 }
365 }
366
367
368 private String selectCompression(String encodingHeader)
369 {
370
371
372 String compression = null;
373 if (encodingHeader!=null)
374 {
375
376 String[] encodings = getEncodings(encodingHeader);
377 if (encodings != null)
378 {
379 for (int i=0; i< encodings.length; i++)
380 {
381 if (encodings[i].toLowerCase(Locale.ENGLISH).contains(GZIP))
382 {
383 if (isEncodingAcceptable(encodings[i]))
384 {
385 compression = GZIP;
386 break;
387 }
388 }
389
390 if (encodings[i].toLowerCase(Locale.ENGLISH).contains(DEFLATE))
391 {
392 if (isEncodingAcceptable(encodings[i]))
393 {
394 compression = DEFLATE;
395 }
396 }
397 }
398 }
399 }
400 return compression;
401 }
402
403
404 private String[] getEncodings (String encodingHeader)
405 {
406 if (encodingHeader == null)
407 return null;
408 return encodingHeader.split(",");
409 }
410
411 private boolean isEncodingAcceptable(String encoding)
412 {
413 int state = STATE_DEFAULT;
414 int qvalueIdx = -1;
415 for (int i=0;i<encoding.length();i++)
416 {
417 char c = encoding.charAt(i);
418 switch (state)
419 {
420 case STATE_DEFAULT:
421 {
422 if (';' == c)
423 state = STATE_SEPARATOR;
424 break;
425 }
426 case STATE_SEPARATOR:
427 {
428 if ('q' == c || 'Q' == c)
429 state = STATE_Q;
430 break;
431 }
432 case STATE_Q:
433 {
434 if ('=' == c)
435 state = STATE_QVALUE;
436 break;
437 }
438 case STATE_QVALUE:
439 {
440 if (qvalueIdx < 0 && '0' == c || '1' == c)
441 qvalueIdx = i;
442 break;
443 }
444 }
445 }
446
447 if (qvalueIdx < 0)
448 return true;
449
450 if ("0".equals(encoding.substring(qvalueIdx).trim()))
451 return false;
452 return true;
453 }
454
455
456 protected CompressedResponseWrapper createWrappedResponse(HttpServletRequest request, HttpServletResponse response, final String compressionType)
457 {
458 CompressedResponseWrapper wrappedResponse = null;
459 wrappedResponse = new CompressedResponseWrapper(request,response)
460 {
461 @Override
462 protected AbstractCompressedStream newCompressedStream(HttpServletRequest request, HttpServletResponse response) throws IOException
463 {
464 return new AbstractCompressedStream(compressionType,request,this,_vary)
465 {
466 private Deflater _allocatedDeflater;
467
468 @Override
469 protected DeflaterOutputStream createStream() throws IOException
470 {
471 if (compressionType == null)
472 {
473 return null;
474 }
475
476
477 _allocatedDeflater = _deflater.get();
478 if (_allocatedDeflater==null)
479 _allocatedDeflater = new Deflater(_deflateCompressionLevel,_deflateNoWrap);
480 else
481 {
482 _deflater.remove();
483 _allocatedDeflater.reset();
484 }
485
486 switch (compressionType)
487 {
488 case GZIP:
489 return new GzipOutputStream(_response.getOutputStream(),_allocatedDeflater,_bufferSize);
490 case DEFLATE:
491 return new DeflaterOutputStream(_response.getOutputStream(),_allocatedDeflater,_bufferSize);
492 }
493 throw new IllegalStateException(compressionType + " not supported");
494 }
495
496 @Override
497 public void finish() throws IOException
498 {
499 super.finish();
500 if (_allocatedDeflater != null && _deflater.get() == null)
501 {
502 _deflater.set(_allocatedDeflater);
503 }
504 }
505 };
506 }
507 };
508 configureWrappedResponse(wrappedResponse);
509 return wrappedResponse;
510 }
511
512 protected void configureWrappedResponse(CompressedResponseWrapper wrappedResponse)
513 {
514 wrappedResponse.setMimeTypes(_mimeTypes,_excludeMimeTypes);
515 wrappedResponse.setBufferSize(_bufferSize);
516 wrappedResponse.setMinCompressSize(_minGzipSize);
517 }
518
519 private class FinishOnCompleteListener implements AsyncListener
520 {
521 private CompressedResponseWrapper wrappedResponse;
522
523 public FinishOnCompleteListener(CompressedResponseWrapper wrappedResponse)
524 {
525 this.wrappedResponse = wrappedResponse;
526 }
527
528 @Override
529 public void onComplete(AsyncEvent event) throws IOException
530 {
531 try
532 {
533 wrappedResponse.finish();
534 }
535 catch (IOException e)
536 {
537 LOG.warn(e);
538 }
539 }
540
541 @Override
542 public void onTimeout(AsyncEvent event) throws IOException
543 {
544 }
545
546 @Override
547 public void onError(AsyncEvent event) throws IOException
548 {
549 }
550
551 @Override
552 public void onStartAsync(AsyncEvent event) throws IOException
553 {
554 }
555 }
556
557
558
559
560
561
562
563
564 private boolean isExcludedAgent(String ua)
565 {
566 if (ua == null)
567 return false;
568
569 if (_excludedAgents != null)
570 {
571 if (_excludedAgents.contains(ua))
572 {
573 return true;
574 }
575 }
576 if (_excludedAgentPatterns != null)
577 {
578 for (Pattern pattern : _excludedAgentPatterns)
579 {
580 if (pattern.matcher(ua).matches())
581 {
582 return true;
583 }
584 }
585 }
586
587 return false;
588 }
589
590
591
592
593
594
595
596
597 private boolean isExcludedPath(String requestURI)
598 {
599 if (requestURI == null)
600 return false;
601 if (_excludedPaths != null)
602 {
603 for (String excludedPath : _excludedPaths)
604 {
605 if (requestURI.startsWith(excludedPath))
606 {
607 return true;
608 }
609 }
610 }
611 if (_excludedPathPatterns != null)
612 {
613 for (Pattern pattern : _excludedPathPatterns)
614 {
615 if (pattern.matcher(requestURI).matches())
616 {
617 return true;
618 }
619 }
620 }
621 return false;
622 }
623 }