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