blob: 11a3f2a7096f4f05d5869f57adba755f5ab5ca7d [file] [log] [blame]
Jake Slack03928ae2014-05-13 18:41:56 -07001//
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}