1// 2// ======================================================================== 3// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. 4// ------------------------------------------------------------------------ 5// All rights reserved. This program and the accompanying materials 6// are made available under the terms of the Eclipse Public License v1.0 7// and Apache License v2.0 which accompanies this distribution. 8// 9// The Eclipse Public License is available at 10// http://www.eclipse.org/legal/epl-v10.html 11// 12// The Apache License v2.0 is available at 13// http://www.opensource.org/licenses/apache2.0.php 14// 15// You may elect to redistribute this code under either of these licenses. 16// ======================================================================== 17// 18 19package org.eclipse.jetty.server.handler; 20 21import java.io.IOException; 22import java.io.OutputStream; 23import java.net.MalformedURLException; 24 25import javax.servlet.ServletException; 26import javax.servlet.http.HttpServletRequest; 27import javax.servlet.http.HttpServletResponse; 28 29import org.eclipse.jetty.http.HttpFields; 30import org.eclipse.jetty.http.HttpHeaders; 31import org.eclipse.jetty.http.HttpMethods; 32import org.eclipse.jetty.http.HttpStatus; 33import org.eclipse.jetty.http.MimeTypes; 34import org.eclipse.jetty.io.Buffer; 35import org.eclipse.jetty.io.ByteArrayBuffer; 36import org.eclipse.jetty.io.WriterOutputStream; 37import org.eclipse.jetty.server.AbstractHttpConnection; 38import org.eclipse.jetty.server.Dispatcher; 39import org.eclipse.jetty.server.Request; 40import org.eclipse.jetty.server.Response; 41import org.eclipse.jetty.server.handler.ContextHandler.Context; 42import org.eclipse.jetty.util.URIUtil; 43import org.eclipse.jetty.util.log.Log; 44import org.eclipse.jetty.util.log.Logger; 45import org.eclipse.jetty.util.resource.FileResource; 46import org.eclipse.jetty.util.resource.Resource; 47 48 49/* ------------------------------------------------------------ */ 50/** Resource Handler. 51 * 52 * This handle will serve static content and handle If-Modified-Since headers. 53 * No caching is done. 54 * Requests for resources that do not exist are let pass (Eg no 404's). 55 * 56 * 57 * @org.apache.xbean.XBean 58 */ 59public class ResourceHandler extends HandlerWrapper 60{ 61 private static final Logger LOG = Log.getLogger(ResourceHandler.class); 62 63 ContextHandler _context; 64 Resource _baseResource; 65 Resource _defaultStylesheet; 66 Resource _stylesheet; 67 String[] _welcomeFiles={"index.html"}; 68 MimeTypes _mimeTypes = new MimeTypes(); 69 ByteArrayBuffer _cacheControl; 70 boolean _aliases; 71 boolean _directory; 72 boolean _etags; 73 74 /* ------------------------------------------------------------ */ 75 public ResourceHandler() 76 { 77 78 } 79 80 /* ------------------------------------------------------------ */ 81 public MimeTypes getMimeTypes() 82 { 83 return _mimeTypes; 84 } 85 86 /* ------------------------------------------------------------ */ 87 public void setMimeTypes(MimeTypes mimeTypes) 88 { 89 _mimeTypes = mimeTypes; 90 } 91 92 /* ------------------------------------------------------------ */ 93 /** 94 * @return True if resource aliases are allowed. 95 */ 96 public boolean isAliases() 97 { 98 return _aliases; 99 } 100 101 /* ------------------------------------------------------------ */ 102 /** 103 * Set if resource aliases (eg symlink, 8.3 names, case insensitivity) are allowed. 104 * Allowing aliases can significantly increase security vulnerabilities. 105 * If this handler is deployed inside a ContextHandler, then the 106 * {@link ContextHandler#isAliases()} takes precedent. 107 * @param aliases True if aliases are supported. 108 */ 109 public void setAliases(boolean aliases) 110 { 111 _aliases = aliases; 112 } 113 114 /* ------------------------------------------------------------ */ 115 /** Get the directory option. 116 * @return true if directories are listed. 117 */ 118 public boolean isDirectoriesListed() 119 { 120 return _directory; 121 } 122 123 /* ------------------------------------------------------------ */ 124 /** Set the directory. 125 * @param directory true if directories are listed. 126 */ 127 public void setDirectoriesListed(boolean directory) 128 { 129 _directory = directory; 130 } 131 132 /* ------------------------------------------------------------ */ 133 /** 134 * @return True if ETag processing is done 135 */ 136 public boolean isEtags() 137 { 138 return _etags; 139 } 140 141 /* ------------------------------------------------------------ */ 142 /** 143 * @param etags True if ETag processing is done 144 */ 145 public void setEtags(boolean etags) 146 { 147 _etags = etags; 148 } 149 150 /* ------------------------------------------------------------ */ 151 @Override 152 public void doStart() 153 throws Exception 154 { 155 Context scontext = ContextHandler.getCurrentContext(); 156 _context = (scontext==null?null:scontext.getContextHandler()); 157 158 if (_context!=null) 159 _aliases=_context.isAliases(); 160 161 if (!_aliases && !FileResource.getCheckAliases()) 162 throw new IllegalStateException("Alias checking disabled"); 163 164 super.doStart(); 165 } 166 167 /* ------------------------------------------------------------ */ 168 /** 169 * @return Returns the resourceBase. 170 */ 171 public Resource getBaseResource() 172 { 173 if (_baseResource==null) 174 return null; 175 return _baseResource; 176 } 177 178 /* ------------------------------------------------------------ */ 179 /** 180 * @return Returns the base resource as a string. 181 */ 182 public String getResourceBase() 183 { 184 if (_baseResource==null) 185 return null; 186 return _baseResource.toString(); 187 } 188 189 190 /* ------------------------------------------------------------ */ 191 /** 192 * @param base The resourceBase to set. 193 */ 194 public void setBaseResource(Resource base) 195 { 196 _baseResource=base; 197 } 198 199 /* ------------------------------------------------------------ */ 200 /** 201 * @param resourceBase The base resource as a string. 202 */ 203 public void setResourceBase(String resourceBase) 204 { 205 try 206 { 207 setBaseResource(Resource.newResource(resourceBase)); 208 } 209 catch (Exception e) 210 { 211 LOG.warn(e.toString()); 212 LOG.debug(e); 213 throw new IllegalArgumentException(resourceBase); 214 } 215 } 216 217 /* ------------------------------------------------------------ */ 218 /** 219 * @return Returns the stylesheet as a Resource. 220 */ 221 public Resource getStylesheet() 222 { 223 if(_stylesheet != null) 224 { 225 return _stylesheet; 226 } 227 else 228 { 229 if(_defaultStylesheet == null) 230 { 231 try 232 { 233 _defaultStylesheet = Resource.newResource(this.getClass().getResource("/jetty-dir.css")); 234 } 235 catch(IOException e) 236 { 237 LOG.warn(e.toString()); 238 LOG.debug(e); 239 } 240 } 241 return _defaultStylesheet; 242 } 243 } 244 245 /* ------------------------------------------------------------ */ 246 /** 247 * @param stylesheet The location of the stylesheet to be used as a String. 248 */ 249 public void setStylesheet(String stylesheet) 250 { 251 try 252 { 253 _stylesheet = Resource.newResource(stylesheet); 254 if(!_stylesheet.exists()) 255 { 256 LOG.warn("unable to find custom stylesheet: " + stylesheet); 257 _stylesheet = null; 258 } 259 } 260 catch(Exception e) 261 { 262 LOG.warn(e.toString()); 263 LOG.debug(e); 264 throw new IllegalArgumentException(stylesheet.toString()); 265 } 266 } 267 268 /* ------------------------------------------------------------ */ 269 /** 270 * @return the cacheControl header to set on all static content. 271 */ 272 public String getCacheControl() 273 { 274 return _cacheControl.toString(); 275 } 276 277 /* ------------------------------------------------------------ */ 278 /** 279 * @param cacheControl the cacheControl header to set on all static content. 280 */ 281 public void setCacheControl(String cacheControl) 282 { 283 _cacheControl=cacheControl==null?null:new ByteArrayBuffer(cacheControl); 284 } 285 286 /* ------------------------------------------------------------ */ 287 /* 288 */ 289 public Resource getResource(String path) throws MalformedURLException 290 { 291 if (path==null || !path.startsWith("/")) 292 throw new MalformedURLException(path); 293 294 Resource base = _baseResource; 295 if (base==null) 296 { 297 if (_context==null) 298 return null; 299 base=_context.getBaseResource(); 300 if (base==null) 301 return null; 302 } 303 304 try 305 { 306 path=URIUtil.canonicalPath(path); 307 return base.addPath(path); 308 } 309 catch(Exception e) 310 { 311 LOG.ignore(e); 312 } 313 314 return null; 315 } 316 317 /* ------------------------------------------------------------ */ 318 protected Resource getResource(HttpServletRequest request) throws MalformedURLException 319 { 320 String servletPath; 321 String pathInfo; 322 Boolean included = request.getAttribute(Dispatcher.INCLUDE_REQUEST_URI) != null; 323 if (included != null && included.booleanValue()) 324 { 325 servletPath = (String)request.getAttribute(Dispatcher.INCLUDE_SERVLET_PATH); 326 pathInfo = (String)request.getAttribute(Dispatcher.INCLUDE_PATH_INFO); 327 328 if (servletPath == null && pathInfo == null) 329 { 330 servletPath = request.getServletPath(); 331 pathInfo = request.getPathInfo(); 332 } 333 } 334 else 335 { 336 servletPath = request.getServletPath(); 337 pathInfo = request.getPathInfo(); 338 } 339 340 String pathInContext=URIUtil.addPaths(servletPath,pathInfo); 341 return getResource(pathInContext); 342 } 343 344 345 /* ------------------------------------------------------------ */ 346 public String[] getWelcomeFiles() 347 { 348 return _welcomeFiles; 349 } 350 351 /* ------------------------------------------------------------ */ 352 public void setWelcomeFiles(String[] welcomeFiles) 353 { 354 _welcomeFiles=welcomeFiles; 355 } 356 357 /* ------------------------------------------------------------ */ 358 protected Resource getWelcome(Resource directory) throws MalformedURLException, IOException 359 { 360 for (int i=0;i<_welcomeFiles.length;i++) 361 { 362 Resource welcome=directory.addPath(_welcomeFiles[i]); 363 if (welcome.exists() && !welcome.isDirectory()) 364 return welcome; 365 } 366 367 return null; 368 } 369 370 /* ------------------------------------------------------------ */ 371 /* 372 * @see org.eclipse.jetty.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int) 373 */ 374 public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException 375 { 376 if (baseRequest.isHandled()) 377 return; 378 379 boolean skipContentBody = false; 380 381 if(!HttpMethods.GET.equals(request.getMethod())) 382 { 383 if(!HttpMethods.HEAD.equals(request.getMethod())) 384 { 385 //try another handler 386 super.handle(target, baseRequest, request, response); 387 return; 388 } 389 skipContentBody = true; 390 } 391 392 Resource resource = getResource(request); 393 394 if (resource==null || !resource.exists()) 395 { 396 if (target.endsWith("/jetty-dir.css")) 397 { 398 resource = getStylesheet(); 399 if (resource==null) 400 return; 401 response.setContentType("text/css"); 402 } 403 else 404 { 405 //no resource - try other handlers 406 super.handle(target, baseRequest, request, response); 407 return; 408 } 409 } 410 411 if (!_aliases && resource.getAlias()!=null) 412 { 413 LOG.info(resource+" aliased to "+resource.getAlias()); 414 return; 415 } 416 417 // We are going to serve something 418 baseRequest.setHandled(true); 419 420 if (resource.isDirectory()) 421 { 422 if (!request.getPathInfo().endsWith(URIUtil.SLASH)) 423 { 424 response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getRequestURI(),URIUtil.SLASH))); 425 return; 426 } 427 428 Resource welcome=getWelcome(resource); 429 if (welcome!=null && welcome.exists()) 430 resource=welcome; 431 else 432 { 433 doDirectory(request,response,resource); 434 baseRequest.setHandled(true); 435 return; 436 } 437 } 438 439 // set some headers 440 long last_modified=resource.lastModified(); 441 String etag=null; 442 if (_etags) 443 { 444 // simple handling of only a single etag 445 String ifnm = request.getHeader(HttpHeaders.IF_NONE_MATCH); 446 etag=resource.getWeakETag(); 447 if (ifnm!=null && resource!=null && ifnm.equals(etag)) 448 { 449 response.setStatus(HttpStatus.NOT_MODIFIED_304); 450 baseRequest.getResponse().getHttpFields().put(HttpHeaders.ETAG_BUFFER,etag); 451 return; 452 } 453 } 454 455 456 if (last_modified>0) 457 { 458 long if_modified=request.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE); 459 if (if_modified>0 && last_modified/1000<=if_modified/1000) 460 { 461 response.setStatus(HttpStatus.NOT_MODIFIED_304); 462 return; 463 } 464 } 465 466 Buffer mime=_mimeTypes.getMimeByExtension(resource.toString()); 467 if (mime==null) 468 mime=_mimeTypes.getMimeByExtension(request.getPathInfo()); 469 470 // set the headers 471 doResponseHeaders(response,resource,mime!=null?mime.toString():null); 472 response.setDateHeader(HttpHeaders.LAST_MODIFIED,last_modified); 473 if (_etags) 474 baseRequest.getResponse().getHttpFields().put(HttpHeaders.ETAG_BUFFER,etag); 475 476 if(skipContentBody) 477 return; 478 // Send the content 479 OutputStream out =null; 480 try {out = response.getOutputStream();} 481 catch(IllegalStateException e) {out = new WriterOutputStream(response.getWriter());} 482 483 // See if a short direct method can be used? 484 if (out instanceof AbstractHttpConnection.Output) 485 { 486 // TODO file mapped buffers 487 ((AbstractHttpConnection.Output)out).sendContent(resource.getInputStream()); 488 } 489 else 490 { 491 // Write content normally 492 resource.writeTo(out,0,resource.length()); 493 } 494 } 495 496 /* ------------------------------------------------------------ */ 497 protected void doDirectory(HttpServletRequest request,HttpServletResponse response, Resource resource) 498 throws IOException 499 { 500 if (_directory) 501 { 502 String listing = resource.getListHTML(request.getRequestURI(),request.getPathInfo().lastIndexOf("/") > 0); 503 response.setContentType("text/html; charset=UTF-8"); 504 response.getWriter().println(listing); 505 } 506 else 507 response.sendError(HttpStatus.FORBIDDEN_403); 508 } 509 510 /* ------------------------------------------------------------ */ 511 /** Set the response headers. 512 * This method is called to set the response headers such as content type and content length. 513 * May be extended to add additional headers. 514 * @param response 515 * @param resource 516 * @param mimeType 517 */ 518 protected void doResponseHeaders(HttpServletResponse response, Resource resource, String mimeType) 519 { 520 if (mimeType!=null) 521 response.setContentType(mimeType); 522 523 long length=resource.length(); 524 525 if (response instanceof Response) 526 { 527 HttpFields fields = ((Response)response).getHttpFields(); 528 529 if (length>0) 530 fields.putLongField(HttpHeaders.CONTENT_LENGTH_BUFFER,length); 531 532 if (_cacheControl!=null) 533 fields.put(HttpHeaders.CACHE_CONTROL_BUFFER,_cacheControl); 534 } 535 else 536 { 537 if (length>0) 538 response.setHeader(HttpHeaders.CONTENT_LENGTH,Long.toString(length)); 539 540 if (_cacheControl!=null) 541 response.setHeader(HttpHeaders.CACHE_CONTROL,_cacheControl.toString()); 542 } 543 544 } 545} 546