View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2015 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  
19  package org.eclipse.jetty.server.handler;
20  
21  import java.io.IOException;
22  import java.io.OutputStream;
23  import java.net.MalformedURLException;
24  import java.nio.ByteBuffer;
25  import java.nio.channels.ReadableByteChannel;
26  
27  import javax.servlet.AsyncContext;
28  import javax.servlet.RequestDispatcher;
29  import javax.servlet.ServletException;
30  import javax.servlet.http.HttpServletRequest;
31  import javax.servlet.http.HttpServletResponse;
32  
33  import org.eclipse.jetty.http.HttpFields;
34  import org.eclipse.jetty.http.HttpHeader;
35  import org.eclipse.jetty.http.HttpMethod;
36  import org.eclipse.jetty.http.HttpStatus;
37  import org.eclipse.jetty.http.MimeTypes;
38  import org.eclipse.jetty.io.WriterOutputStream;
39  import org.eclipse.jetty.server.HttpOutput;
40  import org.eclipse.jetty.server.Request;
41  import org.eclipse.jetty.server.Response;
42  import org.eclipse.jetty.server.handler.ContextHandler.Context;
43  import org.eclipse.jetty.util.BufferUtil;
44  import org.eclipse.jetty.util.Callback;
45  import org.eclipse.jetty.util.URIUtil;
46  import org.eclipse.jetty.util.log.Log;
47  import org.eclipse.jetty.util.log.Logger;
48  import org.eclipse.jetty.util.resource.PathResource;
49  import org.eclipse.jetty.util.resource.Resource;
50  
51  
52  /* ------------------------------------------------------------ */
53  /** Resource Handler.
54   *
55   * This handle will serve static content and handle If-Modified-Since headers.
56   * No caching is done.
57   * Requests for resources that do not exist are let pass (Eg no 404's).
58   *
59   *
60   */
61  public class ResourceHandler extends HandlerWrapper
62  {
63      private static final Logger LOG = Log.getLogger(ResourceHandler.class);
64  
65      ContextHandler _context;
66      Resource _baseResource;
67      Resource _defaultStylesheet;
68      Resource _stylesheet;
69      String[] _welcomeFiles={"index.html"};
70      MimeTypes _mimeTypes = new MimeTypes();
71      String _cacheControl;
72      boolean _directory;
73      boolean _etags;
74      int _minMemoryMappedContentLength=1024;
75      int _minAsyncContentLength=0;
76  
77      /* ------------------------------------------------------------ */
78      public ResourceHandler()
79      {
80  
81      }
82  
83      /* ------------------------------------------------------------ */
84      public MimeTypes getMimeTypes()
85      {
86          return _mimeTypes;
87      }
88  
89      /* ------------------------------------------------------------ */
90      public void setMimeTypes(MimeTypes mimeTypes)
91      {
92          _mimeTypes = mimeTypes;
93      }
94  
95      /* ------------------------------------------------------------ */
96      /** Get the directory option.
97       * @return true if directories are listed.
98       */
99      public boolean isDirectoriesListed()
100     {
101         return _directory;
102     }
103 
104     /* ------------------------------------------------------------ */
105     /** Set the directory.
106      * @param directory true if directories are listed.
107      */
108     public void setDirectoriesListed(boolean directory)
109     {
110         _directory = directory;
111     }
112 
113     /* ------------------------------------------------------------ */
114     /** Get minimum memory mapped file content length.
115      * @return the minimum size in bytes of a file resource that will
116      * be served using a memory mapped buffer, or -1 (default) for no memory mapped
117      * buffers.
118      */
119     public int getMinMemoryMappedContentLength()
120     {
121         return _minMemoryMappedContentLength;
122     }
123 
124     /* ------------------------------------------------------------ */
125     /** Set minimum memory mapped file content length.
126      * @param minMemoryMappedFileSize the minimum size in bytes of a file resource that will
127      * be served using a memory mapped buffer, or -1 for no memory mapped
128      * buffers.
129      */
130     public void setMinMemoryMappedContentLength(int minMemoryMappedFileSize)
131     {
132         _minMemoryMappedContentLength = minMemoryMappedFileSize;
133     }
134 
135     /* ------------------------------------------------------------ */
136     /** Get the minimum content length for async handling.
137      * @return The minimum size in bytes of the content before asynchronous 
138      * handling is used, or -1 for no async handling or 0 (default) for using
139      * {@link HttpServletResponse#getBufferSize()} as the minimum length.
140      */
141     public int getMinAsyncContentLength()
142     {
143         return _minAsyncContentLength;
144     }
145 
146     /* ------------------------------------------------------------ */
147     /** Set the minimum content length for async handling.
148      * @param minAsyncContentLength The minimum size in bytes of the content before asynchronous 
149      * handling is used, or -1 for no async handling or 0 for using
150      * {@link HttpServletResponse#getBufferSize()} as the minimum length.
151      */
152     public void setMinAsyncContentLength(int minAsyncContentLength)
153     {
154         _minAsyncContentLength = minAsyncContentLength;
155     }
156 
157     /* ------------------------------------------------------------ */
158     /**
159      * @return True if ETag processing is done
160      */
161     public boolean isEtags()
162     {
163         return _etags;
164     }
165 
166     /* ------------------------------------------------------------ */
167     /**
168      * @param etags True if ETag processing is done
169      */
170     public void setEtags(boolean etags)
171     {
172         _etags = etags;
173     }
174 
175     /* ------------------------------------------------------------ */
176     @Override
177     public void doStart()
178     throws Exception
179     {
180         Context scontext = ContextHandler.getCurrentContext();
181         _context = (scontext==null?null:scontext.getContextHandler());
182 
183         super.doStart();
184     }
185 
186     /* ------------------------------------------------------------ */
187     /**
188      * @return Returns the resourceBase.
189      */
190     public Resource getBaseResource()
191     {
192         if (_baseResource==null)
193             return null;
194         return _baseResource;
195     }
196 
197     /* ------------------------------------------------------------ */
198     /**
199      * @return Returns the base resource as a string.
200      */
201     public String getResourceBase()
202     {
203         if (_baseResource==null)
204             return null;
205         return _baseResource.toString();
206     }
207 
208 
209     /* ------------------------------------------------------------ */
210     /**
211      * @param base The resourceBase to set.
212      */
213     public void setBaseResource(Resource base)
214     {
215         _baseResource=base;
216     }
217 
218     /* ------------------------------------------------------------ */
219     /**
220      * @param resourceBase The base resource as a string.
221      */
222     public void setResourceBase(String resourceBase)
223     {
224         try
225         {
226             setBaseResource(Resource.newResource(resourceBase));
227         }
228         catch (Exception e)
229         {
230             LOG.warn(e.toString());
231             LOG.debug(e);
232             throw new IllegalArgumentException(resourceBase);
233         }
234     }
235 
236     /* ------------------------------------------------------------ */
237     /**
238      * @return Returns the stylesheet as a Resource.
239      */
240     public Resource getStylesheet()
241     {
242         if(_stylesheet != null)
243         {
244             return _stylesheet;
245         }
246         else
247         {
248             if(_defaultStylesheet == null)
249             {
250                 _defaultStylesheet =  Resource.newResource(this.getClass().getResource("/jetty-dir.css"));
251             }
252             return _defaultStylesheet;
253         }
254     }
255 
256     /* ------------------------------------------------------------ */
257     /**
258      * @param stylesheet The location of the stylesheet to be used as a String.
259      */
260     public void setStylesheet(String stylesheet)
261     {
262         try
263         {
264             _stylesheet = Resource.newResource(stylesheet);
265             if(!_stylesheet.exists())
266             {
267                 LOG.warn("unable to find custom stylesheet: " + stylesheet);
268                 _stylesheet = null;
269             }
270         }
271         catch(Exception e)
272         {
273             LOG.warn(e.toString());
274             LOG.debug(e);
275             throw new IllegalArgumentException(stylesheet);
276         }
277     }
278 
279     /* ------------------------------------------------------------ */
280     /**
281      * @return the cacheControl header to set on all static content.
282      */
283     public String getCacheControl()
284     {
285         return _cacheControl;
286     }
287 
288     /* ------------------------------------------------------------ */
289     /**
290      * @param cacheControl the cacheControl header to set on all static content.
291      */
292     public void setCacheControl(String cacheControl)
293     {
294         _cacheControl=cacheControl;
295     }
296 
297     /* ------------------------------------------------------------ */
298     /*
299      */
300     public Resource getResource(String path) throws MalformedURLException
301     {
302         if (path==null || !path.startsWith("/"))
303             throw new MalformedURLException(path);
304 
305         if (LOG.isDebugEnabled())
306             LOG.debug("{} getResource({})",_context==null?_baseResource:_context,_baseResource,path);
307         
308         Resource base = _baseResource;
309         if (base==null)
310         {
311             if (_context==null)
312                 return null;
313             return _context.getResource(path);
314         }
315 
316         try
317         {
318             path=URIUtil.canonicalPath(path);
319             Resource r = base.addPath(path);
320             if (r!=null && r.isAlias() && (_context==null || !_context.checkAlias(path, r)))
321             {
322                 if (LOG.isDebugEnabled())
323                     LOG.debug("resource={} alias={}",r,r.getAlias());
324                 return null;
325             }
326             return r;
327         }
328         catch(Exception e)
329         {
330             LOG.ignore(e);
331         }
332 
333         return null;
334     }
335 
336     /* ------------------------------------------------------------ */
337     protected Resource getResource(HttpServletRequest request) throws MalformedURLException
338     {
339         String servletPath;
340         String pathInfo;
341         Boolean included = request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null;
342         if (included != null && included.booleanValue())
343         {
344             servletPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
345             pathInfo = (String)request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
346 
347             if (servletPath == null && pathInfo == null)
348             {
349                 servletPath = request.getServletPath();
350                 pathInfo = request.getPathInfo();
351             }
352         }
353         else
354         {
355             servletPath = request.getServletPath();
356             pathInfo = request.getPathInfo();
357         }
358 
359         String pathInContext=URIUtil.addPaths(servletPath,pathInfo);
360         return getResource(pathInContext);
361     }
362 
363 
364     /* ------------------------------------------------------------ */
365     public String[] getWelcomeFiles()
366     {
367         return _welcomeFiles;
368     }
369 
370     /* ------------------------------------------------------------ */
371     public void setWelcomeFiles(String[] welcomeFiles)
372     {
373         _welcomeFiles=welcomeFiles;
374     }
375 
376     /* ------------------------------------------------------------ */
377     protected Resource getWelcome(Resource directory) throws MalformedURLException, IOException
378     {
379         for (int i=0;i<_welcomeFiles.length;i++)
380         {
381             Resource welcome=directory.addPath(_welcomeFiles[i]);
382             if (welcome.exists() && !welcome.isDirectory())
383                 return welcome;
384         }
385 
386         return null;
387     }
388 
389     /* ------------------------------------------------------------ */
390     /*
391      * @see org.eclipse.jetty.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int)
392      */
393     @Override
394     public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
395     {
396         if (baseRequest.isHandled())
397             return;
398 
399         boolean skipContentBody = false;
400 
401         if(!HttpMethod.GET.is(request.getMethod()))
402         {
403             if(!HttpMethod.HEAD.is(request.getMethod()))
404             {
405                 //try another handler
406                 super.handle(target, baseRequest, request, response);
407                 return;
408             }
409             skipContentBody = true;
410         }
411 
412         Resource resource = getResource(request);
413         
414         if (LOG.isDebugEnabled())
415         { 
416             if (resource==null)
417                 LOG.debug("resource=null");
418             else
419                 LOG.debug("resource={} alias={} exists={}",resource,resource.getAlias(),resource.exists());
420         }
421         
422         
423         // If resource is not found
424         if (resource==null || !resource.exists())
425         {
426             // inject the jetty-dir.css file if it matches
427             if (target.endsWith("/jetty-dir.css"))
428             {
429                 resource = getStylesheet();
430                 if (resource==null)
431                     return;
432                 response.setContentType("text/css");
433             }
434             else
435             {
436                 //no resource - try other handlers
437                 super.handle(target, baseRequest, request, response);
438                 return;
439             }
440         }
441 
442         // We are going to serve something
443         baseRequest.setHandled(true);
444 
445         // handle directories
446         if (resource.isDirectory())
447         {
448             String pathInfo = request.getPathInfo();
449             boolean endsWithSlash=(pathInfo==null?request.getServletPath():pathInfo).endsWith(URIUtil.SLASH);
450             if (!endsWithSlash)
451             {
452                 response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getRequestURI(),URIUtil.SLASH)));
453                 return;
454             }
455 
456             Resource welcome=getWelcome(resource);
457             if (welcome!=null && welcome.exists())
458                 resource=welcome;
459             else
460             {
461                 doDirectory(request,response,resource);
462                 baseRequest.setHandled(true);
463                 return;
464             }
465         }
466 
467         // Handle ETAGS
468         long last_modified=resource.lastModified();
469         String etag=null;
470         if (_etags)
471         {
472             // simple handling of only a single etag
473             String ifnm = request.getHeader(HttpHeader.IF_NONE_MATCH.asString());
474             etag=resource.getWeakETag();
475             if (ifnm!=null && resource!=null && ifnm.equals(etag))
476             {
477                 response.setStatus(HttpStatus.NOT_MODIFIED_304);
478                 baseRequest.getResponse().getHttpFields().put(HttpHeader.ETAG,etag);
479                 return;
480             }
481         }
482         
483         // Handle if modified since 
484         if (last_modified>0)
485         {
486             long if_modified=request.getDateHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
487             if (if_modified>0 && last_modified/1000<=if_modified/1000)
488             {
489                 response.setStatus(HttpStatus.NOT_MODIFIED_304);
490                 return;
491             }
492         }
493 
494         // set the headers
495         String mime=_mimeTypes.getMimeByExtension(resource.toString());
496         if (mime==null)
497             mime=_mimeTypes.getMimeByExtension(request.getPathInfo());
498         doResponseHeaders(response,resource,mime);
499         if (_etags)
500             baseRequest.getResponse().getHttpFields().put(HttpHeader.ETAG,etag);
501         
502         if(skipContentBody)
503             return;
504         
505         
506         // Send the content
507         OutputStream out =null;
508         try {out = response.getOutputStream();}
509         catch(IllegalStateException e) {out = new WriterOutputStream(response.getWriter());}
510 
511         // Has the output been wrapped
512         if (!(out instanceof HttpOutput))
513             // Write content via wrapped output
514             resource.writeTo(out,0,resource.length());
515         else
516         {
517             // select async by size
518             int min_async_size=_minAsyncContentLength==0?response.getBufferSize():_minAsyncContentLength;
519             
520             if (request.isAsyncSupported() && 
521                 min_async_size>0 &&
522                 resource.length()>=min_async_size)
523             {
524                 final AsyncContext async = request.startAsync();
525                 async.setTimeout(0);
526                 Callback callback = new Callback()
527                 {
528                     @Override
529                     public void succeeded()
530                     {
531                         async.complete();
532                     }
533 
534                     @Override
535                     public void failed(Throwable x)
536                     {
537                         LOG.warn(x.toString());
538                         LOG.debug(x);
539                         async.complete();
540                     }   
541                 };
542 
543                 // Can we use a memory mapped file?
544                 if (_minMemoryMappedContentLength>0 && 
545                     resource.length()>_minMemoryMappedContentLength &&
546                     resource.length()<Integer.MAX_VALUE &&
547                     resource instanceof PathResource)
548                 {
549                     ByteBuffer buffer = BufferUtil.toMappedBuffer(resource.getFile());
550                     ((HttpOutput)out).sendContent(buffer,callback);
551                 }
552                 else  // Do a blocking write of a channel (if available) or input stream
553                 {
554                     // Close of the channel/inputstream is done by the async sendContent
555                     ReadableByteChannel channel= resource.getReadableByteChannel();
556                     if (channel!=null)
557                         ((HttpOutput)out).sendContent(channel,callback);
558                     else
559                         ((HttpOutput)out).sendContent(resource.getInputStream(),callback);
560                 }
561             }
562             else
563             {
564                 // Can we use a memory mapped file?
565                 if (_minMemoryMappedContentLength>0 && 
566                     resource.length()>_minMemoryMappedContentLength &&
567                     resource instanceof PathResource)
568                 {
569                     ByteBuffer buffer = BufferUtil.toMappedBuffer(resource.getFile());
570                     ((HttpOutput)out).sendContent(buffer);
571                 }
572                 else  // Do a blocking write of a channel (if available) or input stream
573                 {
574                     ReadableByteChannel channel= resource.getReadableByteChannel();
575                     if (channel!=null)
576                         ((HttpOutput)out).sendContent(channel);
577                     else
578                         ((HttpOutput)out).sendContent(resource.getInputStream());
579                 }
580             }
581         }
582     }
583 
584     /* ------------------------------------------------------------ */
585     protected void doDirectory(HttpServletRequest request,HttpServletResponse response, Resource resource)
586         throws IOException
587     {
588         if (_directory)
589         {
590             String listing = resource.getListHTML(request.getRequestURI(),request.getPathInfo().lastIndexOf("/") > 0);
591             response.setContentType("text/html;charset=utf-8");
592             response.getWriter().println(listing);
593         }
594         else
595             response.sendError(HttpStatus.FORBIDDEN_403);
596     }
597 
598     /* ------------------------------------------------------------ */
599     /** Set the response headers.
600      * This method is called to set the response headers such as content type and content length.
601      * May be extended to add additional headers.
602      * @param response the http response
603      * @param resource the resource
604      * @param mimeType the mime type
605      */
606     protected void doResponseHeaders(HttpServletResponse response, Resource resource, String mimeType)
607     {
608         if (mimeType!=null)
609             response.setContentType(mimeType);
610 
611         long length=resource.length();
612 
613         if (response instanceof Response)
614         {
615             HttpFields fields = ((Response)response).getHttpFields();
616 
617             if (length>0)
618                 ((Response)response).setLongContentLength(length);
619 
620             if (_cacheControl!=null)
621                 fields.put(HttpHeader.CACHE_CONTROL,_cacheControl);
622         }
623         else
624         {
625             if (length>Integer.MAX_VALUE)
626                 response.setHeader(HttpHeader.CONTENT_LENGTH.asString(),Long.toString(length));
627             else if (length>0)
628                 response.setContentLength((int)length);
629 
630             if (_cacheControl!=null)
631                 response.setHeader(HttpHeader.CACHE_CONTROL.asString(),_cacheControl);
632         }
633     }
634 }