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