blob: 93776a1e80be0066e11135230c7a9f3ac646161b [file] [log] [blame]
//
// ========================================================================
// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.server.handler;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import javax.servlet.AsyncContext;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.io.WriterOutputStream;
import org.eclipse.jetty.server.HttpOutput;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.handler.ContextHandler.Context;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.PathResource;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
/* ------------------------------------------------------------ */
/** Resource Handler.
*
* This handle will serve static content and handle If-Modified-Since headers.
* No caching is done.
* Requests for resources that do not exist are let pass (Eg no 404's).
*
*
*/
public class ResourceHandler extends HandlerWrapper implements ResourceFactory
{
private static final Logger LOG = Log.getLogger(ResourceHandler.class);
ContextHandler _context;
Resource _baseResource;
Resource _defaultStylesheet;
Resource _stylesheet;
String[] _welcomeFiles={"index.html"};
MimeTypes _mimeTypes;
String _cacheControl;
boolean _directory;
boolean _gzip;
boolean _etags;
int _minMemoryMappedContentLength=0;
int _minAsyncContentLength=16*1024;
/* ------------------------------------------------------------ */
public ResourceHandler()
{
}
/* ------------------------------------------------------------ */
public MimeTypes getMimeTypes()
{
return _mimeTypes;
}
/* ------------------------------------------------------------ */
public void setMimeTypes(MimeTypes mimeTypes)
{
_mimeTypes = mimeTypes;
}
/* ------------------------------------------------------------ */
/** Get the directory option.
* @return true if directories are listed.
*/
public boolean isDirectoriesListed()
{
return _directory;
}
/* ------------------------------------------------------------ */
/** Set the directory.
* @param directory true if directories are listed.
*/
public void setDirectoriesListed(boolean directory)
{
_directory = directory;
}
/* ------------------------------------------------------------ */
/** Get minimum memory mapped file content length.
* @return the minimum size in bytes of a file resource that will
* be served using a memory mapped buffer, or -1 (default) for no memory mapped
* buffers.
*/
public int getMinMemoryMappedContentLength()
{
return _minMemoryMappedContentLength;
}
/* ------------------------------------------------------------ */
/** Set minimum memory mapped file content length.
* @param minMemoryMappedFileSize the minimum size in bytes of a file resource that will
* be served using a memory mapped buffer, or -1 for no memory mapped
* buffers.
*/
public void setMinMemoryMappedContentLength(int minMemoryMappedFileSize)
{
_minMemoryMappedContentLength = minMemoryMappedFileSize;
}
/* ------------------------------------------------------------ */
/** Get the minimum content length for async handling.
* @return The minimum size in bytes of the content before asynchronous
* handling is used, or -1 for no async handling or 0 (default) for using
* {@link HttpServletResponse#getBufferSize()} as the minimum length.
*/
public int getMinAsyncContentLength()
{
return _minAsyncContentLength;
}
/* ------------------------------------------------------------ */
/** Set the minimum content length for async handling.
* @param minAsyncContentLength The minimum size in bytes of the content before asynchronous
* handling is used, or -1 for no async handling or 0 for using
* {@link HttpServletResponse#getBufferSize()} as the minimum length.
*/
public void setMinAsyncContentLength(int minAsyncContentLength)
{
_minAsyncContentLength = minAsyncContentLength;
}
/* ------------------------------------------------------------ */
/**
* @return True if ETag processing is done
*/
public boolean isEtags()
{
return _etags;
}
/* ------------------------------------------------------------ */
/**
* @param etags True if ETag processing is done
*/
public void setEtags(boolean etags)
{
_etags = etags;
}
/* ------------------------------------------------------------ */
@Override
public void doStart()
throws Exception
{
Context scontext = ContextHandler.getCurrentContext();
_context = (scontext==null?null:scontext.getContextHandler());
_mimeTypes = _context==null?new MimeTypes():_context.getMimeTypes();
super.doStart();
}
/* ------------------------------------------------------------ */
/**
* @return Returns the resourceBase.
*/
public Resource getBaseResource()
{
if (_baseResource==null)
return null;
return _baseResource;
}
/* ------------------------------------------------------------ */
/**
* @return Returns the base resource as a string.
*/
public String getResourceBase()
{
if (_baseResource==null)
return null;
return _baseResource.toString();
}
/* ------------------------------------------------------------ */
/**
* @param base The resourceBase to set.
*/
public void setBaseResource(Resource base)
{
_baseResource=base;
}
/* ------------------------------------------------------------ */
/**
* @param resourceBase The base resource as a string.
*/
public void setResourceBase(String resourceBase)
{
try
{
setBaseResource(Resource.newResource(resourceBase));
}
catch (Exception e)
{
LOG.warn(e.toString());
LOG.debug(e);
throw new IllegalArgumentException(resourceBase);
}
}
/* ------------------------------------------------------------ */
/**
* @return Returns the stylesheet as a Resource.
*/
public Resource getStylesheet()
{
if(_stylesheet != null)
{
return _stylesheet;
}
else
{
if(_defaultStylesheet == null)
{
_defaultStylesheet = Resource.newResource(this.getClass().getResource("/jetty-dir.css"));
}
return _defaultStylesheet;
}
}
/* ------------------------------------------------------------ */
/**
* @param stylesheet The location of the stylesheet to be used as a String.
*/
public void setStylesheet(String stylesheet)
{
try
{
_stylesheet = Resource.newResource(stylesheet);
if(!_stylesheet.exists())
{
LOG.warn("unable to find custom stylesheet: " + stylesheet);
_stylesheet = null;
}
}
catch(Exception e)
{
LOG.warn(e.toString());
LOG.debug(e);
throw new IllegalArgumentException(stylesheet);
}
}
/* ------------------------------------------------------------ */
/**
* @return the cacheControl header to set on all static content.
*/
public String getCacheControl()
{
return _cacheControl;
}
/* ------------------------------------------------------------ */
/**
* @param cacheControl the cacheControl header to set on all static content.
*/
public void setCacheControl(String cacheControl)
{
_cacheControl=cacheControl;
}
/* ------------------------------------------------------------ */
/*
*/
@Override
public Resource getResource(String path)
{
if (LOG.isDebugEnabled())
LOG.debug("{} getResource({})",_context==null?_baseResource:_context,_baseResource,path);
if (path==null || !path.startsWith("/"))
return null;
try
{
Resource base = _baseResource;
if (base==null)
{
if (_context==null)
return null;
return _context.getResource(path);
}
path=URIUtil.canonicalPath(path);
Resource r = base.addPath(path);
if (r!=null && r.isAlias() && (_context==null || !_context.checkAlias(path, r)))
{
if (LOG.isDebugEnabled())
LOG.debug("resource={} alias={}",r,r.getAlias());
return null;
}
return r;
}
catch(Exception e)
{
LOG.debug(e);
}
return null;
}
/* ------------------------------------------------------------ */
protected Resource getResource(HttpServletRequest request) throws MalformedURLException
{
String servletPath;
String pathInfo;
Boolean included = request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null;
if (included != null && included.booleanValue())
{
servletPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
pathInfo = (String)request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
if (servletPath == null && pathInfo == null)
{
servletPath = request.getServletPath();
pathInfo = request.getPathInfo();
}
}
else
{
servletPath = request.getServletPath();
pathInfo = request.getPathInfo();
}
String pathInContext=URIUtil.addPaths(servletPath,pathInfo);
return getResource(pathInContext);
}
/* ------------------------------------------------------------ */
public String[] getWelcomeFiles()
{
return _welcomeFiles;
}
/* ------------------------------------------------------------ */
public void setWelcomeFiles(String[] welcomeFiles)
{
_welcomeFiles=welcomeFiles;
}
/* ------------------------------------------------------------ */
protected Resource getWelcome(Resource directory) throws MalformedURLException, IOException
{
for (int i=0;i<_welcomeFiles.length;i++)
{
Resource welcome=directory.addPath(_welcomeFiles[i]);
if (welcome.exists() && !welcome.isDirectory())
return welcome;
}
return null;
}
/* ------------------------------------------------------------ */
/*
* @see org.eclipse.jetty.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int)
*/
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
if (baseRequest.isHandled())
return;
boolean skipContentBody = false;
if(!HttpMethod.GET.is(request.getMethod()))
{
if(!HttpMethod.HEAD.is(request.getMethod()))
{
//try another handler
super.handle(target, baseRequest, request, response);
return;
}
skipContentBody = true;
}
Resource resource = getResource(request);
if (LOG.isDebugEnabled())
{
if (resource==null)
LOG.debug("resource=null");
else
LOG.debug("resource={} alias={} exists={}",resource,resource.getAlias(),resource.exists());
}
// If resource is not found
if (resource==null || !resource.exists())
{
// inject the jetty-dir.css file if it matches
if (target.endsWith("/jetty-dir.css"))
{
resource = getStylesheet();
if (resource==null)
return;
response.setContentType("text/css");
}
else
{
//no resource - try other handlers
super.handle(target, baseRequest, request, response);
return;
}
}
// We are going to serve something
baseRequest.setHandled(true);
// handle directories
if (resource.isDirectory())
{
String pathInfo = request.getPathInfo();
boolean endsWithSlash=(pathInfo==null?request.getServletPath():pathInfo).endsWith(URIUtil.SLASH);
if (!endsWithSlash)
{
response.sendRedirect(response.encodeRedirectURL(request.getRequestURI()+URIUtil.SLASH));
return;
}
Resource welcome=getWelcome(resource);
if (welcome!=null && welcome.exists())
resource=welcome;
else
{
doDirectory(request,response,resource);
baseRequest.setHandled(true);
return;
}
}
// Handle ETAGS
long last_modified=resource.lastModified();
String etag=null;
if (_etags)
{
// simple handling of only a single etag
String ifnm = request.getHeader(HttpHeader.IF_NONE_MATCH.asString());
etag=resource.getWeakETag();
if (ifnm!=null && resource!=null && ifnm.equals(etag))
{
response.setStatus(HttpStatus.NOT_MODIFIED_304);
baseRequest.getResponse().getHttpFields().put(HttpHeader.ETAG,etag);
return;
}
}
// Handle if modified since
if (last_modified>0)
{
long if_modified=request.getDateHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
if (if_modified>0 && last_modified/1000<=if_modified/1000)
{
response.setStatus(HttpStatus.NOT_MODIFIED_304);
return;
}
}
// set the headers
String mime=_mimeTypes.getMimeByExtension(resource.toString());
if (mime==null)
mime=_mimeTypes.getMimeByExtension(request.getPathInfo());
doResponseHeaders(response,resource,mime);
if (_etags)
baseRequest.getResponse().getHttpFields().put(HttpHeader.ETAG,etag);
if (last_modified>0)
response.setDateHeader(HttpHeader.LAST_MODIFIED.asString(),last_modified);
if(skipContentBody)
return;
// Send the content
OutputStream out =null;
try {out = response.getOutputStream();}
catch(IllegalStateException e) {out = new WriterOutputStream(response.getWriter());}
// Has the output been wrapped
if (!(out instanceof HttpOutput))
// Write content via wrapped output
resource.writeTo(out,0,resource.length());
else
{
// select async by size
int min_async_size=_minAsyncContentLength==0?response.getBufferSize():_minAsyncContentLength;
if (request.isAsyncSupported() &&
min_async_size>0 &&
resource.length()>=min_async_size)
{
final AsyncContext async = request.startAsync();
async.setTimeout(0);
Callback callback = new Callback()
{
@Override
public void succeeded()
{
async.complete();
}
@Override
public void failed(Throwable x)
{
LOG.warn(x.toString());
LOG.debug(x);
async.complete();
}
};
// Can we use a memory mapped file?
if (_minMemoryMappedContentLength>=0 &&
resource.length()>_minMemoryMappedContentLength &&
resource.length()<Integer.MAX_VALUE &&
resource instanceof PathResource)
{
ByteBuffer buffer = BufferUtil.toMappedBuffer(resource.getFile());
((HttpOutput)out).sendContent(buffer,callback);
}
else // Do a blocking write of a channel (if available) or input stream
{
// Close of the channel/inputstream is done by the async sendContent
ReadableByteChannel channel= resource.getReadableByteChannel();
if (channel!=null)
((HttpOutput)out).sendContent(channel,callback);
else
((HttpOutput)out).sendContent(resource.getInputStream(),callback);
}
}
else
{
// Can we use a memory mapped file?
if (_minMemoryMappedContentLength>0 &&
resource.length()>_minMemoryMappedContentLength &&
resource instanceof PathResource)
{
ByteBuffer buffer = BufferUtil.toMappedBuffer(resource.getFile());
((HttpOutput)out).sendContent(buffer);
}
else // Do a blocking write of a channel (if available) or input stream
{
ReadableByteChannel channel= resource.getReadableByteChannel();
if (channel!=null)
((HttpOutput)out).sendContent(channel);
else
((HttpOutput)out).sendContent(resource.getInputStream());
}
}
}
}
/* ------------------------------------------------------------ */
protected void doDirectory(HttpServletRequest request,HttpServletResponse response, Resource resource)
throws IOException
{
if (_directory)
{
String listing = resource.getListHTML(request.getRequestURI(),request.getPathInfo().lastIndexOf("/") > 0);
response.setContentType("text/html;charset=utf-8");
response.getWriter().println(listing);
}
else
response.sendError(HttpStatus.FORBIDDEN_403);
}
/* ------------------------------------------------------------ */
/** Set the response headers.
* This method is called to set the response headers such as content type and content length.
* May be extended to add additional headers.
* @param response the http response
* @param resource the resource
* @param mimeType the mime type
*/
protected void doResponseHeaders(HttpServletResponse response, Resource resource, String mimeType)
{
if (mimeType!=null)
response.setContentType(mimeType);
long length=resource.length();
if (response instanceof Response)
{
HttpFields fields = ((Response)response).getHttpFields();
if (length>0)
((Response)response).setLongContentLength(length);
if (_cacheControl!=null)
fields.put(HttpHeader.CACHE_CONTROL,_cacheControl);
}
else
{
if (length>Integer.MAX_VALUE)
response.setHeader(HttpHeader.CONTENT_LENGTH.asString(),Long.toString(length));
else if (length>0)
response.setContentLength((int)length);
if (_cacheControl!=null)
response.setHeader(HttpHeader.CACHE_CONTROL.asString(),_cacheControl);
}
}
}