1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package info.magnolia.cms.filters;
35
36 import java.io.IOException;
37 import java.io.OutputStreamWriter;
38 import java.io.PrintWriter;
39 import java.util.ArrayList;
40 import java.util.Arrays;
41 import java.util.HashMap;
42 import java.util.List;
43 import java.util.Map;
44
45 import javax.servlet.FilterChain;
46 import javax.servlet.ServletException;
47 import javax.servlet.ServletOutputStream;
48 import javax.servlet.http.HttpServletRequest;
49 import javax.servlet.http.HttpServletResponse;
50 import javax.servlet.http.HttpServletResponseWrapper;
51
52 import org.apache.commons.lang.StringUtils;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56
57
58
59
60
61
62 public class RangeSupportFilter extends AbstractMgnlFilter {
63
64 private static final Logger log = LoggerFactory.getLogger(RangeSupportFilter.class);
65
66 private boolean wrapWriter = true;
67
68 private boolean isRangedRequest = false;
69
70 private boolean isServeContent = true;
71
72 long lastModTime = -1;
73 @Override
74 public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
75
76 isRangedRequest = request.getHeader("Range") != null;
77
78 if (isRangedRequest) {
79 response = wrapResponse(request, response);
80
81
82 if ("HEAD".equalsIgnoreCase(request.getMethod())) {
83 isServeContent = false;
84 }
85 }
86 chain.doFilter(request, response);
87
88 }
89
90 public boolean isWrapWriter() {
91 return wrapWriter;
92 }
93
94
95
96
97
98
99 public void setWrapWriter(boolean wrapWriter) {
100 this.wrapWriter = wrapWriter;
101 }
102
103 private HttpServletResponse wrapResponse(final HttpServletRequest request, final HttpServletResponse response) {
104 return new HttpServletResponseWrapper(response) {
105
106
107 private int length = Integer.MAX_VALUE;
108
109 private final Map<String, Object> headers = new HashMap<String, Object>();
110
111 private String eTag;
112
113 private List<RangeInfo> ranges;
114
115 private RangeInfo full;
116
117 private ServletOutputStream stream;
118
119 private PrintWriter writer;
120
121 @Override
122 public void addDateHeader(String name, long date) {
123 super.addDateHeader(name, date);
124 this.headers.put(name, date);
125 if ("Last-Modified".equalsIgnoreCase(name)) {
126 lastModTime = date;
127 }
128 }
129
130 @Override
131 public void setDateHeader(String name, long date) {
132 super.setDateHeader(name, date);
133 this.headers.put(name, date);
134 if ("Last-Modified".equalsIgnoreCase(name)) {
135 lastModTime = date;
136 }
137 }
138
139 @Override
140 public void addHeader(String name, String value) {
141 if ("Content-Disposition".equalsIgnoreCase(name) && log.isDebugEnabled()) {
142 log.warn("content disposition enforced by underlying filter/servlet");
143 }
144 super.addHeader(name, value);
145 this.headers.put(name, value);
146 }
147
148 @Override
149 public void setHeader(String name, String value) {
150 if ("Content-Disposition".equalsIgnoreCase(name) && log.isDebugEnabled()) {
151 log.warn("content disposition enforced by underlying filter/servlet");
152 }
153 super.setHeader(name, value);
154 this.headers.put(name, value);
155 }
156
157 @Override
158 public void addIntHeader(String name, int value) {
159 super.addIntHeader(name, value);
160 this.headers.put(name, value);
161 }
162
163 @Override
164 public void setIntHeader(String name, int value) {
165 super.setIntHeader(name, value);
166 this.headers.put(name, value);
167 }
168
169 @Override
170 public void setContentLength(int len) {
171 this.length = len;
172
173 }
174
175 @Override
176 public ServletOutputStream getOutputStream() throws IOException {
177
178 if (this.stream == null) {
179 ServletOutputStream stream = super.getOutputStream();
180
181 this.stream = addRangeSupportWrapper(request, response, stream);
182
183 if (!isServeContent || this.stream == null) {
184
185 this.stream = new ServletOutputStream() {
186
187 @Override
188 public void write(int b) throws IOException {
189
190 }
191 };
192 }
193 }
194 return stream;
195 }
196
197 private ServletOutputStream addRangeSupportWrapper(final HttpServletRequest request, final HttpServletResponse response, ServletOutputStream stream) throws IOException {
198 if (!processContent(request, response)) {
199
200 return null;
201 }
202
203 if (headers.containsKey("Content-Range")) {
204
205 log.debug("Range request was handled by underlying filter/servlet.");
206 return stream;
207 }
208 if (ranges == null || ranges.isEmpty()) {
209
210 log.debug("Didn't find any range to speak of. Serving all content as usual.");
211 if (length != Integer.MAX_VALUE) {
212
213 response.setContentLength(length);
214 }
215 } else if (ranges.size() == 1) {
216 RangeInfo range = ranges.get(0);
217 log.debug("Serving range [{}].", range);
218 response.setContentLength(range.lengthOfRange);
219
220 response.setStatus(SC_PARTIAL_CONTENT);
221 stream = new RangedOutputStream(stream, range);
222 } else {
223 log.error("Requested multiple ranges [{}].", ranges.size());
224
225 response.setHeader("Content-Range", "bytes */" + length);
226 response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
227
228 }
229 return stream;
230 }
231
232 @Override
233 public PrintWriter getWriter() throws IOException {
234 if (!wrapWriter) {
235 return super.getWriter();
236 }
237 if (this.writer == null) {
238 this.writer = new PrintWriter(new OutputStreamWriter(getOutputStream()));
239 }
240 return writer;
241 }
242
243 private boolean processContent(HttpServletRequest request, HttpServletResponse response) throws IOException {
244 log.debug("Serving binary on uri {} was last modified at {}", new Object[] { request.getRequestURI(), lastModTime });
245 if (!isRequestValid(request, response)) {
246 log.debug("Skipping request {} since it doesn't require body", new Object[] { request.getRequestURI() });
247 return false;
248 }
249 if (!processRange(request)) {
250 log.debug("Could not process range of request {}", new Object[] { request.getRequestURI() });
251 return false;
252 }
253 return true;
254 }
255
256 private boolean processRange(HttpServletRequest request) throws IOException {
257 full = new RangeInfo(0, length - 1, length);
258 ranges = new ArrayList<RangeInfo>();
259
260 String range = request.getHeader("Range");
261
262
263 if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
264 response.setHeader("Content-Range", "bytes */" + length);
265 response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
266 return false;
267 }
268
269
270 String ifRange = request.getHeader("If-Range");
271 if (ifRange != null && !ifRange.equals(eTag)) {
272 try {
273 long ifRangeTime = request.getDateHeader("If-Range");
274 if (ifRangeTime != -1 && ifRangeTime + 1000 < lastModTime) {
275 ranges.add(full);
276 }
277 } catch (IllegalArgumentException ignore) {
278
279 ranges.add(full);
280 }
281 }
282
283
284 if (ranges.isEmpty()) {
285 for (String part : range.substring(6).split(",")) {
286 int start = intSubstring(StringUtils.substringBefore(part, "-"));
287 int end = intSubstring(StringUtils.substringAfter(part, "-"));
288
289 if (start == -1) {
290 start = length - end;
291 end = length - 1;
292 } else if (end == -1 || end > length - 1) {
293 end = length - 1;
294 }
295
296
297 if (start > end) {
298 response.setHeader("Content-Range", "bytes */" + length);
299 response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
300 return false;
301 }
302
303
304 ranges.add(new RangeInfo(start, end, length));
305 }
306 }
307
308 response.setHeader("ETag", eTag);
309 if (ranges.size() == 1) {
310 RangeInfo r = ranges.get(0);
311 response.setHeader("Accept-Ranges", "bytes");
312 response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.totalLengthOfServedBinary);
313 length = r.lengthOfRange;
314 }
315 return true;
316 }
317
318 private int intSubstring(String value) {
319 return (value.length() > 0) ? Integer.parseInt(value) : -1;
320 }
321
322 @Override
323 public void flushBuffer() throws IOException {
324 if (writer != null) {
325 writer.flush();
326 }
327 if (stream != null) {
328 stream.flush();
329 }
330
331 super.flushBuffer();
332 }
333
334 private boolean isRequestValid(HttpServletRequest request, HttpServletResponse response) throws IOException {
335 String fileName = StringUtils.substringAfterLast(request.getRequestURI(), "/");
336 eTag = fileName + "_" + length + "_" + lastModTime;
337
338
339 String ifNoneMatch = request.getHeader("If-None-Match");
340 if (ifNoneMatch != null && matches(ifNoneMatch, eTag)) {
341 response.setHeader("ETag", eTag);
342 log.debug("Returning {} on header If-None-Match", HttpServletResponse.SC_NOT_MODIFIED);
343 response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
344 return false;
345 }
346
347
348 long ifModifiedSince = request.getDateHeader("If-Modified-Since");
349 if (ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModTime) {
350 response.setHeader("ETag", eTag);
351
352 response.addDateHeader("Date", lastModTime);
353 log.debug("Returning {} on header If-Modified-Since", HttpServletResponse.SC_NOT_MODIFIED);
354 response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
355 return false;
356 }
357
358
359 String ifMatch = request.getHeader("If-Match");
360 if (ifMatch != null && !matches(ifMatch, eTag)) {
361 log.debug("Returning {} on header If-Match", HttpServletResponse.SC_PRECONDITION_FAILED);
362 response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
363 return false;
364 }
365
366
367 long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since");
368 if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModTime) {
369 log.debug("Returning {} on header If-Unmodified-Since", HttpServletResponse.SC_PRECONDITION_FAILED);
370 response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
371 return false;
372 }
373
374 log.debug("Passed all precondition checkes for request {}", request.getRequestURI());
375 return true;
376 }
377 };
378 }
379
380 private boolean matches(String matchHeader, String toMatch) {
381 String[] matchValues = matchHeader.split("\\s*,\\s*");
382 Arrays.sort(matchValues);
383 return Arrays.binarySearch(matchValues, toMatch) > -1
384 || Arrays.binarySearch(matchValues, "*") > -1;
385 }
386
387
388
389
390
391
392
393 protected class RangeInfo {
394 final int start;
395 final int end;
396 final int lengthOfRange;
397 final int totalLengthOfServedBinary;
398
399 public RangeInfo(int start, int end, int totalLengthOfServedBinary) {
400 this.start = start;
401 this.end = end;
402 this.lengthOfRange = end - start + 1;
403 this.totalLengthOfServedBinary = totalLengthOfServedBinary;
404 }
405
406 @Override
407 public String toString() {
408 return "Start: " + start + ", end: " + end + ", len: " + lengthOfRange + ", totalLengthOfServedBinary: " + totalLengthOfServedBinary;
409 }
410 }
411 }