1 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 ; 48 import javax.servlet.http.HttpServletResponse ; 49 import java.io.FileNotFoundException ; 50 import java.io.IOException ; 51 import java.io.OutputStream ; 52 53 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 ,Cache> _pathCache; 63 private QDate _calendar = new QDate(); 64 private boolean _isCaseInsensitive; 65 private boolean _isEnableRange = true; 66 private String _characterEncoding; 67 68 public FileServlet() 69 { 70 _isCaseInsensitive = CaseInsensitive.isCaseInsensitive(); 71 } 72 73 76 public void setEnableRange(boolean isEnable) 77 { 78 _isEnableRange = isEnable; 79 } 80 81 84 public void setCharacterEncoding(String 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 e) { 100 } 101 102 _pathCache = new LruCache<String ,Cache>(1024); 103 104 String enable = getInitParameter("enable-range"); 105 if (enable != null && enable.equals("false")) 106 _isEnableRange = false; 107 108 String 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 request, ServletResponse response) 122 throws ServletException, IOException 123 { 124 CauchoRequest cauchoReq = null; 125 HttpServletRequest req; 126 HttpServletResponse res; 127 128 if (request instanceof CauchoRequest) { 129 cauchoReq = (CauchoRequest) request; 130 req = cauchoReq; 131 } 132 else 133 req = (HttpServletRequest ) request; 134 135 res = (HttpServletResponse ) response; 136 137 String 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 uri; 147 148 uri = (String ) 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 filename = null; 157 158 if (cache == null) { 159 CharBuffer cb = new CharBuffer(); 160 String servletPath; 161 162 if (cauchoReq != null) 163 servletPath = cauchoReq.getPageServletPath(); 164 else if (isInclude) 165 servletPath = (String ) 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 pathInfo; 173 if (cauchoReq != null) 174 pathInfo = cauchoReq.getPagePathInfo(); 175 else if (isInclude) 176 pathInfo = (String ) 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 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 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 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 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 res.sendError(res.SC_NOT_FOUND); 230 return; 231 } 232 } 233 234 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 (uri); 264 else 265 res.sendError(res.SC_NOT_FOUND); 266 return; 267 } 268 269 String ifMatch = req.getHeader("If-None-Match"); 270 String 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 lastModified = cache.getLastModifiedString(); 278 279 if (ifMatch == null) { 280 String 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 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 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 range = req.getHeader("Range"); 330 331 if (range != null) { 332 String 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 os = res.getOutputStream(); 350 cache.getPath().writeToStream(os); 351 } 352 } 353 354 private boolean handleRange(HttpServletRequest req, 355 HttpServletResponse res, 356 Cache cache, 357 String range, 358 String mime) 359 throws IOException 360 { 361 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 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 for (; off < length && (ch = range.charAt(off)) == ' '; off++) { 387 } 388 389 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 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 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 if (! hasFirst) { 427 first = cacheLength - last; 428 last = cacheLength - 1; 429 } 430 431 if (last < first) 432 break; 433 434 if (cacheLength <= last) { 435 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 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 _relPath; 529 String _etag; 530 String _lastModifiedString; 531 String _mimeType; 532 533 Cache(QDate calendar, Path path, String relPath, String 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 getRelPath() 564 { 565 return _relPath; 566 } 567 568 String getEtag() 569 { 570 return _etag; 571 } 572 573 long getLastModified() 574 { 575 return _lastModified; 576 } 577 578 String getLastModifiedString() 579 { 580 return _lastModifiedString; 581 } 582 583 String 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 |