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 addHeader(String name, String value) {
132
133 if ("Content-Disposition".equalsIgnoreCase(name)) {
134 log.error("content disposition enforced by underlying filter/servlet");
135 }
136 super.addHeader(name, value);
137 this.headers.put(name, value);
138 }
139
140 @Override
141 public void addIntHeader(String name, int value) {
142 super.addIntHeader(name, value);
143 this.headers.put(name, value);
144 }
145
146 @Override
147 public void setContentLength(int len) {
148 this.length = len;
149
150 }
151
152 @Override
153 public ServletOutputStream getOutputStream() throws IOException {
154
155 if (this.stream == null) {
156 ServletOutputStream stream = super.getOutputStream();
157
158 this.stream = addRangeSupportWrapper(request, response, stream);
159
160 if (!isServeContent) {
161
162 this.stream = new ServletOutputStream() {
163
164 @Override
165 public void write(int b) throws IOException {
166
167 }
168 };
169 }
170 }
171 return stream;
172 }
173
174 private ServletOutputStream addRangeSupportWrapper(final HttpServletRequest request, final HttpServletResponse response, ServletOutputStream stream) throws IOException {
175 if (!processContent(request, response)) {
176
177 return stream;
178 }
179
180 if (ranges == null || ranges.isEmpty()) {
181
182 log.debug("Didn't find any range to speak of. Serving all content as usual.");
183 if (length != Integer.MAX_VALUE) {
184
185 response.setContentLength(length);
186 }
187 } else if (ranges.size() == 1) {
188 RangeInfo range = ranges.get(0);
189 log.debug("Serving range [{}].", range);
190 response.setContentLength(range.lengthOfRange);
191 stream = new RangedOutputStream(stream, range);
192 } else {
193 log.error("Requested multiple ranges [{}].", ranges.size());
194
195 response.setHeader("Content-Range", "bytes */" + length);
196 response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
197
198 }
199 return stream;
200 }
201
202 @Override
203 public PrintWriter getWriter() throws IOException {
204 if (!wrapWriter) {
205 return super.getWriter();
206 }
207 if (this.writer == null) {
208 this.writer = new PrintWriter(new OutputStreamWriter(getOutputStream()));
209 }
210 return writer;
211 }
212
213 private boolean processContent(HttpServletRequest request, HttpServletResponse response) throws IOException {
214 log.debug("Serving binary on uri {} was last modified at {}", new Object[] { request.getRequestURI(), lastModTime });
215 if (!isRequestValid(request, response)) {
216 log.debug("Skipping request {} as invalid", new Object[] { request.getRequestURI() });
217 return false;
218 }
219 if (!processRange(request)) {
220 log.debug("Could not process range of request {}", new Object[] { request.getRequestURI() });
221 return false;
222 }
223 return true;
224 }
225
226 private boolean processRange(HttpServletRequest request) throws IOException {
227 full = new RangeInfo(0, length - 1, length);
228 ranges = new ArrayList<RangeInfo>();
229
230 String range = request.getHeader("Range");
231
232
233 if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
234 response.setHeader("Content-Range", "bytes */" + length);
235 response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
236 return false;
237 }
238
239
240 String ifRange = request.getHeader("If-Range");
241 if (ifRange != null && !ifRange.equals(eTag)) {
242 try {
243 long ifRangeTime = request.getDateHeader("If-Range");
244 if (ifRangeTime != -1 && ifRangeTime + 1000 < lastModTime) {
245 ranges.add(full);
246 }
247 } catch (IllegalArgumentException ignore) {
248
249 ranges.add(full);
250 }
251 }
252
253
254 if (ranges.isEmpty()) {
255 for (String part : range.substring(6).split(",")) {
256 int start = intSubstring(StringUtils.substringBefore(part, "-"));
257 int end = intSubstring(StringUtils.substringAfter(part, "-"));
258
259 if (start == -1) {
260 start = length - end;
261 end = length - 1;
262 } else if (end == -1 || end > length - 1) {
263 end = length - 1;
264 }
265
266
267 if (start > end) {
268 response.setHeader("Content-Range", "bytes */" + length);
269 response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
270 return false;
271 }
272
273
274 ranges.add(new RangeInfo(start, end, length));
275 }
276 }
277
278 response.setHeader("ETag", eTag);
279 if (ranges.size() == 1) {
280 RangeInfo r = ranges.get(0);
281 response.setHeader("Accept-Ranges", "bytes");
282 response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.totalLengthOfServedBinary);
283 length = r.lengthOfRange;
284 }
285 return true;
286 }
287
288 private int intSubstring(String value) {
289 return (value.length() > 0) ? Integer.parseInt(value) : -1;
290 }
291
292 @Override
293 public void flushBuffer() throws IOException {
294 if (writer != null) {
295 writer.flush();
296 }
297 if (stream != null) {
298 stream.flush();
299 }
300
301 super.flushBuffer();
302 }
303
304 private boolean isRequestValid(HttpServletRequest request, HttpServletResponse response) throws IOException {
305 String fileName = StringUtils.substringAfterLast(request.getRequestURI(), "/");
306 eTag = fileName + "_" + length + "_" + lastModTime;
307
308
309 String ifNoneMatch = request.getHeader("If-None-Match");
310 if (ifNoneMatch != null && matches(ifNoneMatch, eTag)) {
311 response.setHeader("ETag", eTag);
312 log.debug("Returning {} on header If-None-Match", HttpServletResponse.SC_NOT_MODIFIED);
313 response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
314 return false;
315 }
316
317
318 long ifModifiedSince = request.getDateHeader("If-Modified-Since");
319 if (ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModTime) {
320 response.setHeader("ETag", eTag);
321 log.debug("Returning {} on header If-Modified-Since", HttpServletResponse.SC_NOT_MODIFIED);
322 response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
323 return false;
324 }
325
326
327 String ifMatch = request.getHeader("If-Match");
328 if (ifMatch != null && !matches(ifMatch, eTag)) {
329 log.debug("Returning {} on header If-Match", HttpServletResponse.SC_PRECONDITION_FAILED);
330 response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
331 return false;
332 }
333
334
335 long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since");
336 if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModTime) {
337 log.debug("Returning {} on header If-Unmodified-Since", HttpServletResponse.SC_PRECONDITION_FAILED);
338 response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
339 return false;
340 }
341
342 log.debug("Passed all precondition checkes for request {}", request.getRequestURI());
343 return true;
344 }
345 };
346 }
347
348 private boolean matches(String matchHeader, String toMatch) {
349 String[] matchValues = matchHeader.split("\\s*,\\s*");
350 Arrays.sort(matchValues);
351 return Arrays.binarySearch(matchValues, toMatch) > -1
352 || Arrays.binarySearch(matchValues, "*") > -1;
353 }
354
355
356
357
358
359
360
361 protected class RangeInfo {
362 final int start;
363 final int end;
364 final int lengthOfRange;
365 final int totalLengthOfServedBinary;
366
367 public RangeInfo(int start, int end, int totalLengthOfServedBinary) {
368 this.start = start;
369 this.end = end;
370 this.lengthOfRange = end - start + 1;
371 this.totalLengthOfServedBinary = totalLengthOfServedBinary;
372 }
373
374 @Override
375 public String toString() {
376 return "Start: " + start + ", end: " + end + ", len: " + lengthOfRange + ", totalLengthOfServedBinary: " + totalLengthOfServedBinary;
377 }
378 }
379 }