KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > com > caucho > servlets > FileServlet


1 /*
2  * Copyright (c) 1998-2006 Caucho Technology -- all rights reserved
3  *
4  * This file is part of Resin(R) Open Source
5  *
6  * Each copy or derived work must preserve the copyright notice and this
7  * notice unmodified.
8  *
9  * Resin Open Source is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation; either version 2 of the License, or
12  * (at your option) any later version.
13  *
14  * Resin Open Source is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
17  * of NON-INFRINGEMENT. See the GNU General Public License for more
18  * details.
19  *
20  * You should have received a copy of the GNU General Public License
21  * along with Resin Open Source; if not, write to the
22  *
23  * Free Software Foundation, Inc.
24  * 59 Temple Place, Suite 330
25  * Boston, MA 02111-1307 USA
26  *
27  * @author Scott Ferguson
28  */

29
30 package com.caucho.servlets;
31
32 import com.caucho.server.connection.CauchoRequest;
33 import com.caucho.server.connection.CauchoResponse;
34 import com.caucho.server.util.CauchoSystem;
35 import com.caucho.server.webapp.Application;
36 import com.caucho.util.Alarm;
37 import com.caucho.util.Base64;
38 import com.caucho.util.CharBuffer;
39 import com.caucho.util.LruCache;
40 import com.caucho.util.QDate;
41 import com.caucho.util.RandomUtil;
42 import com.caucho.vfs.CaseInsensitive;
43 import com.caucho.vfs.Path;
44 import com.caucho.vfs.ReadStream;
45
46 import javax.servlet.*;
47 import javax.servlet.http.HttpServletRequest JavaDoc;
48 import javax.servlet.http.HttpServletResponse JavaDoc;
49 import java.io.FileNotFoundException JavaDoc;
50 import java.io.IOException JavaDoc;
51 import java.io.OutputStream JavaDoc;
52
53 /**
54  * Serves static files. The cache headers are automatically set on these
55  * files.
56  */

57 public class FileServlet extends GenericServlet {
58   private Path _context;
59   private byte []_buffer = new byte[1024];
60   private Application _app;
61   private RequestDispatcher _dir;
62   private LruCache<String JavaDoc,Cache> _pathCache;
63   private QDate _calendar = new QDate();
64   private boolean _isCaseInsensitive;
65   private boolean _isEnableRange = true;
66   private String JavaDoc _characterEncoding;
67
68   public FileServlet()
69   {
70     _isCaseInsensitive = CaseInsensitive.isCaseInsensitive();
71   }
72
73   /**
74    * Flag to disable the "Range" header.
75    */

76   public void setEnableRange(boolean isEnable)
77   {
78     _isEnableRange = isEnable;
79   }
80
81   /**
82    * Sets the character encoding.
83    */

84   public void setCharacterEncoding(String JavaDoc encoding)
85   {
86     _characterEncoding = encoding;
87   }
88   
89   public void init(ServletConfig conf)
90     throws ServletException
91   {
92     super.init(conf);
93
94     _app = (Application) getServletContext();
95     _context = _app.getAppDir();
96
97     try {
98       _dir = _app.getNamedDispatcher("directory");
99     } catch (Throwable JavaDoc e) {
100     }
101       
102     _pathCache = new LruCache<String JavaDoc,Cache>(1024);
103
104     String JavaDoc enable = getInitParameter("enable-range");
105     if (enable != null && enable.equals("false"))
106       _isEnableRange = false;
107
108     String JavaDoc encoding = getInitParameter("character-encoding");
109     if (encoding != null && ! "".equals(encoding))
110       _characterEncoding = encoding;
111   }
112
113   private RequestDispatcher getDirectoryServlet()
114   {
115     if (_dir == null)
116       _dir = _app.getNamedDispatcher("directory");
117
118     return _dir;
119   }
120
121   public void service(ServletRequest JavaDoc request, ServletResponse JavaDoc response)
122     throws ServletException, IOException JavaDoc
123   {
124     CauchoRequest cauchoReq = null;
125     HttpServletRequest JavaDoc req;
126     HttpServletResponse JavaDoc res;
127
128     if (request instanceof CauchoRequest) {
129       cauchoReq = (CauchoRequest) request;
130       req = cauchoReq;
131     }
132     else
133       req = (HttpServletRequest JavaDoc) request;
134     
135     res = (HttpServletResponse JavaDoc) response;
136     
137     String JavaDoc method = req.getMethod();
138     if (! method.equalsIgnoreCase("GET") &&
139     ! method.equalsIgnoreCase("HEAD") &&
140     ! method.equalsIgnoreCase("POST")) {
141       res.sendError(res.SC_NOT_IMPLEMENTED, "Method not implemented");
142       return;
143     }
144
145     boolean isInclude = false;
146     String JavaDoc uri;
147
148     uri = (String JavaDoc) req.getAttribute("javax.servlet.include.request_uri");
149     if (uri != null)
150       isInclude = true;
151     else
152       uri = req.getRequestURI();
153
154     Cache cache = _pathCache.get(uri);
155
156     String JavaDoc filename = null;
157
158     if (cache == null) {
159       CharBuffer cb = new CharBuffer();
160       String JavaDoc servletPath;
161
162       if (cauchoReq != null)
163         servletPath = cauchoReq.getPageServletPath();
164       else if (isInclude)
165         servletPath = (String JavaDoc) req.getAttribute("javax.servlet.include.servlet_path");
166       else
167         servletPath = req.getServletPath();
168         
169       if (servletPath != null)
170         cb.append(servletPath);
171       
172       String JavaDoc pathInfo;
173       if (cauchoReq != null)
174         pathInfo = cauchoReq.getPagePathInfo();
175       else if (isInclude)
176         pathInfo = (String JavaDoc) req.getAttribute("javax.servlet.include.path_info");
177       else
178         pathInfo = req.getPathInfo();
179         
180       if (pathInfo != null)
181         cb.append(pathInfo);
182
183       String JavaDoc relPath = cb.toString();
184
185       if (_isCaseInsensitive)
186         relPath = relPath.toLowerCase();
187
188       filename = getServletContext().getRealPath(relPath);
189       Path path = _context.lookupNative(filename);
190       int lastCh;
191
192       // only top-level requests are checked
193
if (cauchoReq == null || cauchoReq.getRequestDepth(0) != 0) {
194       }
195       else if (relPath.regionMatches(true, 0, "/web-inf", 0, 8) &&
196                (relPath.length() == 8 ||
197                 ! Character.isLetterOrDigit(relPath.charAt(8)))) {
198         res.sendError(res.SC_NOT_FOUND);
199         return;
200       }
201       else if (relPath.regionMatches(true, 0, "/meta-inf", 0, 9) &&
202                (relPath.length() == 9 ||
203                 ! Character.isLetterOrDigit(relPath.charAt(9)))) {
204         res.sendError(res.SC_NOT_FOUND);
205         return;
206       }
207
208       if (relPath.endsWith(".DS_store")) {
209         // MacOS-X security hole with trailing '.'
210
res.sendError(res.SC_NOT_FOUND);
211         return;
212       }
213       else if (! CauchoSystem.isWindows() || relPath.length() == 0) {
214       }
215       else if (path.isDirectory()) {
216       }
217       else {
218         String JavaDoc lower = path.getPath().toLowerCase();
219         
220         if ((lastCh = relPath.charAt(relPath.length() - 1)) == '.' ||
221             lastCh == ' ' || lastCh == '*' || lastCh == '?' ||
222             lastCh == '/' || lastCh == '\\' ||
223             lower.endsWith("::$data") ||
224             lower.endsWith("/con") || lower.endsWith("/con/") ||
225             lower.endsWith("/aux") || lower.endsWith("/aux/") ||
226             lower.endsWith("/prn") || lower.endsWith("/prn/") ||
227             lower.endsWith("/nul") || lower.endsWith("/nul/")) {
228           // Windows security hole with trailing '.'
229
res.sendError(res.SC_NOT_FOUND);
230           return;
231         }
232       }
233
234       // A null will cause problems.
235
for (int i = relPath.length() - 1; i >= 0; i--) {
236         char ch = relPath.charAt(i);
237           
238         if (ch == 0) {
239           res.sendError(res.SC_NOT_FOUND);
240           return;
241         }
242       }
243
244       ServletContext app = getServletContext();
245
246       cache = new Cache(_calendar, path, relPath, app.getMimeType(relPath));
247
248       _pathCache.put(uri, cache);
249     }
250   
251     cache.update();
252
253     if (cache.isDirectory()) {
254       if (_dir != null)
255     _dir.forward(req, res);
256       else
257     res.sendError(res.SC_NOT_FOUND);
258       return;
259     }
260
261     if (! cache.canRead()) {
262       if (isInclude)
263         throw new FileNotFoundException JavaDoc(uri);
264       else
265         res.sendError(res.SC_NOT_FOUND);
266       return;
267     }
268
269     String JavaDoc ifMatch = req.getHeader("If-None-Match");
270     String JavaDoc etag = cache.getEtag();
271     if (ifMatch != null && ifMatch.equals(etag)) {
272       res.addHeader("ETag", etag);
273       res.sendError(res.SC_NOT_MODIFIED);
274       return;
275     }
276
277     String JavaDoc lastModified = cache.getLastModifiedString();
278
279     if (ifMatch == null) {
280       String JavaDoc ifModified = req.getHeader("If-Modified-Since");
281
282       boolean isModified = true;
283
284       if (ifModified == null) {
285       }
286       else if (ifModified.equals(lastModified)) {
287     isModified = false;
288       }
289       else {
290     long ifModifiedTime;
291
292     synchronized (_calendar) {
293       try {
294         ifModifiedTime = _calendar.parseDate(ifModified);
295       } catch (Throwable JavaDoc e) {
296         ifModifiedTime = 0;
297       }
298     }
299
300     isModified = ifModifiedTime != cache.getLastModified();
301       }
302
303       if (! isModified) {
304     if (etag != null)
305       res.addHeader("ETag", etag);
306     res.sendError(res.SC_NOT_MODIFIED);
307     return;
308       }
309     }
310
311     res.addHeader("ETag", etag);
312     res.addHeader("Last-Modified", lastModified);
313     if (_isEnableRange && cauchoReq != null && cauchoReq.isTop())
314       res.addHeader("Accept-Ranges", "bytes");
315     
316     if (_characterEncoding != null)
317       res.setCharacterEncoding(_characterEncoding);
318     
319     String JavaDoc mime = cache.getMimeType();
320     if (mime != null)
321       res.setContentType(mime);
322
323     if (method.equalsIgnoreCase("HEAD")) {
324       res.setContentLength((int) cache.getLength());
325       return;
326     }
327
328     if (_isEnableRange) {
329       String JavaDoc range = req.getHeader("Range");
330
331       if (range != null) {
332     String JavaDoc ifRange = req.getHeader("If-Range");
333
334     if (ifRange != null && ! ifRange.equals(etag)) {
335     }
336     else if (handleRange(req, res, cache, range, mime))
337       return;
338       }
339     }
340
341     res.setContentLength((int) cache.getLength());
342
343     if (res instanceof CauchoResponse) {
344       CauchoResponse cRes = (CauchoResponse) res;
345
346       cRes.getResponseStream().sendFile(cache.getPath(), cache.getLength());
347     }
348     else {
349       OutputStream JavaDoc os = res.getOutputStream();
350       cache.getPath().writeToStream(os);
351     }
352   }
353
354   private boolean handleRange(HttpServletRequest JavaDoc req,
355                               HttpServletResponse JavaDoc res,
356                               Cache cache,
357                   String JavaDoc range,
358                   String JavaDoc mime)
359     throws IOException JavaDoc
360   {
361     // This is duplicated in CacheInvocation. Possibly, it should be
362
// completely removed although it's useful even without caching.
363
int length = range.length();
364
365     boolean hasMore = range.indexOf(',') > 0;
366
367     int head = 0;
368     ServletOutputStream os = res.getOutputStream();
369     boolean isFirstChunk = true;
370     String JavaDoc boundary = null;
371     int off = range.indexOf("bytes=", head);
372
373     if (off < 0)
374       return false;
375
376     off += 6;
377
378     while (off > 0 && off < length) {
379       boolean hasFirst = false;
380       long first = 0;
381       boolean hasLast = false;
382       long last = 0;
383       int ch = -1;;
384
385       // Skip whitespace
386
for (; off < length && (ch = range.charAt(off)) == ' '; off++) {
387       }
388
389       // read range start (before '-')
390
for (;
391        off < length && (ch = range.charAt(off)) >= '0' && ch <= '9';
392        off++) {
393     first = 10 * first + ch - '0';
394     hasFirst = true;
395       }
396
397       if (length <= off && ! isFirstChunk)
398     break;
399       else if (ch != '-')
400     return false;
401
402       // read range end (before '-')
403
for (off++;
404        off < length && (ch = range.charAt(off)) >= '0' && ch <= '9';
405        off++) {
406     last = 10 * last + ch - '0';
407     hasLast = true;
408       }
409
410       // Skip whitespace
411
for (; off < length && (ch = range.charAt(off)) == ' '; off++) {
412       }
413
414       head = off;
415
416       long cacheLength = cache.getLength();
417
418       if (! hasLast) {
419     if (first == 0)
420       return false;
421     
422     last = cacheLength - 1;
423       }
424
425       // suffix
426
if (! hasFirst) {
427     first = cacheLength - last;
428     last = cacheLength - 1;
429       }
430
431       if (last < first)
432     break;
433     
434       if (cacheLength <= last) {
435     // XXX: actually, an error
436
break;
437       }
438     
439       res.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
440
441       CharBuffer cb = new CharBuffer();
442       cb.append("bytes ");
443       cb.append(first);
444       cb.append('-');
445       cb.append(last);
446       cb.append('/');
447       cb.append(cacheLength);
448       String JavaDoc chunkRange = cb.toString();
449
450       if (hasMore) {
451     if (isFirstChunk) {
452       CharBuffer cb1 = new CharBuffer();
453
454       cb1.append("--");
455       Base64.encode(cb1, RandomUtil.getRandomLong());
456       boundary = cb1.toString();
457
458       res.setContentType("multipart/byteranges; boundary=" + boundary);
459     }
460     else {
461       os.write('\r');
462       os.write('\n');
463     }
464
465     isFirstChunk = false;
466     
467     os.write('-');
468     os.write('-');
469     os.print(boundary);
470     os.print("\r\nContent-Type: ");
471     os.print(mime);
472     os.print("\r\nContent-Range: ");
473     os.print(chunkRange);
474     os.write('\r');
475     os.write('\n');
476     os.write('\r');
477     os.write('\n');
478       }
479       else {
480     res.setContentLength((int) (last - first + 1));
481       
482     res.addHeader("Content-Range", chunkRange);
483       }
484
485       ReadStream is = null;
486       try {
487     is = cache.getPath().openRead();
488     is.skip(first);
489
490     os = res.getOutputStream();
491     is.writeToStream(os, (int) (last - first + 1));
492       } finally {
493     if (is != null)
494       is.close();
495       }
496
497       for (off--; off < length && range.charAt(off) != ','; off++) {
498       }
499
500       off++;
501     }
502
503     if (hasMore) {
504       os.write('\r');
505       os.write('\n');
506       os.write('-');
507       os.write('-');
508       os.print(boundary);
509       os.write('-');
510       os.write('-');
511       os.write('\r');
512       os.write('\n');
513     }
514
515     return true;
516   }
517
518   static class Cache {
519     private final static long UPDATE_INTERVAL = 2000L;
520     
521     QDate _calendar;
522     Path _path;
523     boolean _isDirectory;
524     boolean _canRead;
525     long _length;
526     long _lastCheck;
527     long _lastModified = 0xdeadbabe1ee7d00dL;
528     String JavaDoc _relPath;
529     String JavaDoc _etag;
530     String JavaDoc _lastModifiedString;
531     String JavaDoc _mimeType;
532     
533     Cache(QDate calendar, Path path, String JavaDoc relPath, String JavaDoc mimeType)
534     {
535       _calendar = calendar;
536       _path = path;
537       _relPath = relPath;
538       _mimeType = mimeType;
539
540       update();
541     }
542
543     Path getPath()
544     {
545       return _path;
546     }
547
548     boolean canRead()
549     {
550       return _canRead;
551     }
552
553     boolean isDirectory()
554     {
555       return _isDirectory;
556     }
557
558     long getLength()
559     {
560       return _length;
561     }
562
563     String JavaDoc getRelPath()
564     {
565       return _relPath;
566     }
567
568     String JavaDoc getEtag()
569     {
570       return _etag;
571     }
572
573     long getLastModified()
574     {
575       return _lastModified;
576     }
577
578     String JavaDoc getLastModifiedString()
579     {
580       return _lastModifiedString;
581     }
582
583     String JavaDoc getMimeType()
584     {
585       return _mimeType;
586     }
587
588     void update()
589     {
590       long now = Alarm.getCurrentTime();
591       if (_lastCheck + UPDATE_INTERVAL < now) {
592         synchronized (this) {
593       if (now <= _lastCheck + UPDATE_INTERVAL)
594         return;
595
596       if (_lastCheck == 0) {
597         updateData();
598         _lastCheck = now;
599         return;
600       }
601
602       _lastCheck = now;
603     }
604
605     updateData();
606       }
607     }
608
609     private void updateData()
610     {
611       long lastModified = _path.getLastModified();
612       long length = _path.getLength();
613
614       if (lastModified != _lastModified || length != _length) {
615     _lastModified = lastModified;
616     _length = length;
617     _canRead = _path.canRead();
618     _isDirectory = _path.isDirectory();
619         
620     CharBuffer cb = new CharBuffer();
621     cb.append('"');
622     long hash = lastModified;
623     hash = hash * 0x5deece66dl + 0xbl + (hash >>> 32) * 137;
624     hash += length;
625     Base64.encode(cb, hash);
626     cb.append('"');
627     _etag = cb.close();
628
629     synchronized (_calendar) {
630       _calendar.setGMTTime(lastModified);
631       _lastModifiedString = _calendar.printDate();
632     }
633       }
634       
635       if (lastModified == 0) {
636     _canRead = false;
637     _isDirectory = false;
638       }
639     }
640   }
641 }
642
Popular Tags