View Javadoc

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