blob: 22689e0d009b278dd0a27a6250c68ec45899adef [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.servlet;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.StringTokenizer;
import javax.servlet.AsyncContext;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.UnavailableException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpContent;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpGenerator.CachedHttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.PathMap.MappedEntry;
import org.eclipse.jetty.io.WriterOutputStream;
import org.eclipse.jetty.server.HttpOutput;
import org.eclipse.jetty.server.InclusiveByteRange;
import org.eclipse.jetty.server.ResourceCache;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.MultiPartOutputStream;
import org.eclipse.jetty.util.QuotedStringTokenizer;
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.Resource;
import org.eclipse.jetty.util.resource.ResourceCollection;
import org.eclipse.jetty.util.resource.ResourceFactory;
/* ------------------------------------------------------------ */
/** The default servlet.
*
* This servlet, normally mapped to /, provides the handling for static
* content, OPTION and TRACE methods for the context.
* The following initParameters are supported, these can be set either
* on the servlet itself or as ServletContext initParameters with a prefix
* of org.eclipse.jetty.servlet.Default. :
* <PRE>
* acceptRanges If true, range requests and responses are
* supported
*
* dirAllowed If true, directory listings are returned if no
* welcome file is found. Else 403 Forbidden.
*
* welcomeServlets If true, attempt to dispatch to welcome files
* that are servlets, but only after no matching static
* resources could be found. If false, then a welcome
* file must exist on disk. If "exact", then exact
* servlet matches are supported without an existing file.
* Default is true.
*
* This must be false if you want directory listings,
* but have index.jsp in your welcome file list.
*
* redirectWelcome If true, welcome files are redirected rather than
* forwarded to.
*
* gzip If set to true, then static content will be served as
* gzip content encoded if a matching resource is
* found ending with ".gz"
*
* resourceBase Set to replace the context resource base
*
* resourceCache If set, this is a context attribute name, which the servlet
* will use to look for a shared ResourceCache instance.
*
* relativeResourceBase
* Set with a pathname relative to the base of the
* servlet context root. Useful for only serving static content out
* of only specific subdirectories.
*
* pathInfoOnly If true, only the path info will be applied to the resourceBase
*
* stylesheet Set with the location of an optional stylesheet that will be used
* to decorate the directory listing html.
*
* etags If True, weak etags will be generated and handled.
*
* maxCacheSize The maximum total size of the cache or 0 for no cache.
* maxCachedFileSize The maximum size of a file to cache
* maxCachedFiles The maximum number of files to cache
*
* useFileMappedBuffer
* If set to true, it will use mapped file buffer to serve static content
* when using NIO connector. Setting this value to false means that
* a direct buffer will be used instead of a mapped file buffer.
* This is set to false by default by this class, but may be overridden
* by eg webdefault.xml
*
* cacheControl If set, all static content will have this value set as the cache-control
* header.
*
* otherGzipFileExtensions
* Other file extensions that signify that a file is gzip compressed. Eg ".svgz"
*
*
* </PRE>
*
*
*
*
*/
public class DefaultServlet extends HttpServlet implements ResourceFactory
{
private static final Logger LOG = Log.getLogger(DefaultServlet.class);
private static final long serialVersionUID = 4930458713846881193L;
private static final CachedHttpField ACCEPT_RANGES = new CachedHttpField(HttpHeader.ACCEPT_RANGES, "bytes");
private ServletContext _servletContext;
private ContextHandler _contextHandler;
private boolean _acceptRanges=true;
private boolean _dirAllowed=true;
private boolean _welcomeServlets=false;
private boolean _welcomeExactServlets=false;
private boolean _redirectWelcome=false;
private boolean _gzip=false;
private boolean _pathInfoOnly=false;
private boolean _etags=false;
private Resource _resourceBase;
private ResourceCache _cache;
private MimeTypes _mimeTypes;
private String[] _welcomes;
private Resource _stylesheet;
private boolean _useFileMappedBuffer=false;
private HttpField _cacheControl;
private String _relativeResourceBase;
private ServletHandler _servletHandler;
private ServletHolder _defaultHolder;
private List<String> _gzipEquivalentFileExtensions;
/* ------------------------------------------------------------ */
@Override
public void init()
throws UnavailableException
{
_servletContext=getServletContext();
_contextHandler = initContextHandler(_servletContext);
_mimeTypes = _contextHandler.getMimeTypes();
_welcomes = _contextHandler.getWelcomeFiles();
if (_welcomes==null)
_welcomes=new String[] {"index.html","index.jsp"};
_acceptRanges=getInitBoolean("acceptRanges",_acceptRanges);
_dirAllowed=getInitBoolean("dirAllowed",_dirAllowed);
_redirectWelcome=getInitBoolean("redirectWelcome",_redirectWelcome);
_gzip=getInitBoolean("gzip",_gzip);
_pathInfoOnly=getInitBoolean("pathInfoOnly",_pathInfoOnly);
if ("exact".equals(getInitParameter("welcomeServlets")))
{
_welcomeExactServlets=true;
_welcomeServlets=false;
}
else
_welcomeServlets=getInitBoolean("welcomeServlets", _welcomeServlets);
_useFileMappedBuffer=getInitBoolean("useFileMappedBuffer",_useFileMappedBuffer);
_relativeResourceBase = getInitParameter("relativeResourceBase");
String rb=getInitParameter("resourceBase");
if (rb!=null)
{
if (_relativeResourceBase!=null)
throw new UnavailableException("resourceBase & relativeResourceBase");
try{_resourceBase=_contextHandler.newResource(rb);}
catch (Exception e)
{
LOG.warn(Log.EXCEPTION,e);
throw new UnavailableException(e.toString());
}
}
String css=getInitParameter("stylesheet");
try
{
if(css!=null)
{
_stylesheet = Resource.newResource(css);
if(!_stylesheet.exists())
{
LOG.warn("!" + css);
_stylesheet = null;
}
}
if(_stylesheet == null)
{
_stylesheet = Resource.newResource(this.getClass().getResource("/jetty-dir.css"));
}
}
catch(Exception e)
{
LOG.warn(e.toString());
LOG.debug(e);
}
String cc=getInitParameter("cacheControl");
if (cc!=null)
_cacheControl=new CachedHttpField(HttpHeader.CACHE_CONTROL, cc);
String resourceCache = getInitParameter("resourceCache");
int max_cache_size=getInitInt("maxCacheSize", -2);
int max_cached_file_size=getInitInt("maxCachedFileSize", -2);
int max_cached_files=getInitInt("maxCachedFiles", -2);
if (resourceCache!=null)
{
if (max_cache_size!=-1 || max_cached_file_size!= -2 || max_cached_files!=-2)
LOG.debug("ignoring resource cache configuration, using resourceCache attribute");
if (_relativeResourceBase!=null || _resourceBase!=null)
throw new UnavailableException("resourceCache specified with resource bases");
_cache=(ResourceCache)_servletContext.getAttribute(resourceCache);
if (LOG.isDebugEnabled())
LOG.debug("Cache {}={}",resourceCache,_cache);
}
_etags = getInitBoolean("etags",_etags);
try
{
if (_cache==null && (max_cached_files!=-2 || max_cache_size!=-2 || max_cached_file_size!=-2))
{
_cache= new ResourceCache(null,this,_mimeTypes,_useFileMappedBuffer,_etags);
if (max_cache_size>=0)
_cache.setMaxCacheSize(max_cache_size);
if (max_cached_file_size>=-1)
_cache.setMaxCachedFileSize(max_cached_file_size);
if (max_cached_files>=-1)
_cache.setMaxCachedFiles(max_cached_files);
}
}
catch (Exception e)
{
LOG.warn(Log.EXCEPTION,e);
throw new UnavailableException(e.toString());
}
_gzipEquivalentFileExtensions = new ArrayList<String>();
String otherGzipExtensions = getInitParameter("otherGzipFileExtensions");
if (otherGzipExtensions != null)
{
//comma separated list
StringTokenizer tok = new StringTokenizer(otherGzipExtensions,",",false);
while (tok.hasMoreTokens())
{
String s = tok.nextToken().trim();
_gzipEquivalentFileExtensions.add((s.charAt(0)=='.'?s:"."+s));
}
}
else
{
//.svgz files are gzipped svg files and must be served with Content-Encoding:gzip
_gzipEquivalentFileExtensions.add(".svgz");
}
_servletHandler= _contextHandler.getChildHandlerByClass(ServletHandler.class);
for (ServletHolder h :_servletHandler.getServlets())
if (h.getServletInstance()==this)
_defaultHolder=h;
if (LOG.isDebugEnabled())
LOG.debug("resource base = "+_resourceBase);
}
/**
* Compute the field _contextHandler.<br/>
* In the case where the DefaultServlet is deployed on the HttpService it is likely that
* this method needs to be overwritten to unwrap the ServletContext facade until we reach
* the original jetty's ContextHandler.
* @param servletContext The servletContext of this servlet.
* @return the jetty's ContextHandler for this servletContext.
*/
protected ContextHandler initContextHandler(ServletContext servletContext)
{
ContextHandler.Context scontext=ContextHandler.getCurrentContext();
if (scontext==null)
{
if (servletContext instanceof ContextHandler.Context)
return ((ContextHandler.Context)servletContext).getContextHandler();
else
throw new IllegalArgumentException("The servletContext " + servletContext + " " +
servletContext.getClass().getName() + " is not " + ContextHandler.Context.class.getName());
}
else
return ContextHandler.getCurrentContext().getContextHandler();
}
/* ------------------------------------------------------------ */
@Override
public String getInitParameter(String name)
{
String value=getServletContext().getInitParameter("org.eclipse.jetty.servlet.Default."+name);
if (value==null)
value=super.getInitParameter(name);
return value;
}
/* ------------------------------------------------------------ */
private boolean getInitBoolean(String name, boolean dft)
{
String value=getInitParameter(name);
if (value==null || value.length()==0)
return dft;
return (value.startsWith("t")||
value.startsWith("T")||
value.startsWith("y")||
value.startsWith("Y")||
value.startsWith("1"));
}
/* ------------------------------------------------------------ */
private int getInitInt(String name, int dft)
{
String value=getInitParameter(name);
if (value==null)
value=getInitParameter(name);
if (value!=null && value.length()>0)
return Integer.parseInt(value);
return dft;
}
/* ------------------------------------------------------------ */
/** get Resource to serve.
* Map a path to a resource. The default implementation calls
* HttpContext.getResource but derived servlets may provide
* their own mapping.
* @param pathInContext The path to find a resource for.
* @return The resource to serve.
*/
@Override
public Resource getResource(String pathInContext)
{
Resource r=null;
if (_relativeResourceBase!=null)
pathInContext=URIUtil.addPaths(_relativeResourceBase,pathInContext);
try
{
if (_resourceBase!=null)
{
r = _resourceBase.addPath(pathInContext);
if (!_contextHandler.checkAlias(pathInContext,r))
r=null;
}
else if (_servletContext instanceof ContextHandler.Context)
{
r = _contextHandler.getResource(pathInContext);
}
else
{
URL u = _servletContext.getResource(pathInContext);
r = _contextHandler.newResource(u);
}
if (LOG.isDebugEnabled())
LOG.debug("Resource "+pathInContext+"="+r);
}
catch (IOException e)
{
LOG.ignore(e);
}
if((r==null || !r.exists()) && pathInContext.endsWith("/jetty-dir.css"))
r=_stylesheet;
return r;
}
/* ------------------------------------------------------------ */
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
String servletPath=null;
String pathInfo=null;
Enumeration<String> reqRanges = null;
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)
{
servletPath=request.getServletPath();
pathInfo=request.getPathInfo();
}
}
else
{
included = Boolean.FALSE;
servletPath = _pathInfoOnly?"/":request.getServletPath();
pathInfo = request.getPathInfo();
// Is this a Range request?
reqRanges = request.getHeaders(HttpHeader.RANGE.asString());
if (!hasDefinedRange(reqRanges))
reqRanges = null;
}
String pathInContext=URIUtil.addPaths(servletPath,pathInfo);
boolean endsWithSlash=(pathInfo==null?request.getServletPath():pathInfo).endsWith(URIUtil.SLASH);
// Find the resource and content
Resource resource=null;
HttpContent content=null;
boolean close_content=true;
try
{
// is gzip enabled?
String pathInContextGz=null;
boolean gzip=false;
if (!included.booleanValue() && _gzip && reqRanges==null && !endsWithSlash )
{
// Look for a gzip resource
pathInContextGz=pathInContext+".gz";
if (_cache==null)
resource=getResource(pathInContextGz);
else
{
content=_cache.lookup(pathInContextGz);
resource=(content==null)?null:content.getResource();
}
// Does a gzip resource exist?
if (resource!=null && resource.exists() && !resource.isDirectory())
{
// Tell caches that response may vary by accept-encoding
response.addHeader(HttpHeader.VARY.asString(),HttpHeader.ACCEPT_ENCODING.asString());
// Does the client accept gzip?
String accept=request.getHeader(HttpHeader.ACCEPT_ENCODING.asString());
if (accept!=null && accept.indexOf("gzip")>=0)
gzip=true;
}
}
// find resource
if (!gzip)
{
if (_cache==null)
resource=getResource(pathInContext);
else
{
content=_cache.lookup(pathInContext);
resource=content==null?null:content.getResource();
}
}
if (LOG.isDebugEnabled())
LOG.debug(String.format("uri=%s, resource=%s, content=%s",request.getRequestURI(),resource,content));
// Handle resource
if (resource==null || !resource.exists())
{
if (included)
throw new FileNotFoundException("!" + pathInContext);
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
else if (!resource.isDirectory())
{
if (endsWithSlash && pathInContext.length()>1)
{
String q=request.getQueryString();
pathInContext=pathInContext.substring(0,pathInContext.length()-1);
if (q!=null&&q.length()!=0)
pathInContext+="?"+q;
response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(_servletContext.getContextPath(),pathInContext)));
}
else
{
// ensure we have content
if (content==null)
content=new HttpContent.ResourceAsHttpContent(resource,_mimeTypes.getMimeByExtension(resource.toString()),response.getBufferSize(),_etags);
if (included.booleanValue() || passConditionalHeaders(request,response, resource,content))
{
if (gzip || isGzippedContent(pathInContext))
{
response.setHeader(HttpHeader.CONTENT_ENCODING.asString(),"gzip");
String mt=_servletContext.getMimeType(pathInContext);
if (mt!=null)
response.setContentType(mt);
}
close_content=sendData(request,response,included.booleanValue(),resource,content,reqRanges);
}
}
}
else
{
String welcome=null;
if (!endsWithSlash || (pathInContext.length()==1 && request.getAttribute("org.eclipse.jetty.server.nullPathInfo")!=null))
{
StringBuffer buf=request.getRequestURL();
synchronized(buf)
{
int param=buf.lastIndexOf(";");
if (param<0)
buf.append('/');
else
buf.insert(param,'/');
String q=request.getQueryString();
if (q!=null&&q.length()!=0)
{
buf.append('?');
buf.append(q);
}
response.setContentLength(0);
response.sendRedirect(response.encodeRedirectURL(buf.toString()));
}
}
// else look for a welcome file
else if (null!=(welcome=getWelcomeFile(pathInContext)))
{
if (LOG.isDebugEnabled())
LOG.debug("welcome={}",welcome);
if (_redirectWelcome)
{
// Redirect to the index
response.setContentLength(0);
String q=request.getQueryString();
if (q!=null&&q.length()!=0)
response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths( _servletContext.getContextPath(),welcome)+"?"+q));
else
response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths( _servletContext.getContextPath(),welcome)));
}
else
{
// Forward to the index
RequestDispatcher dispatcher=request.getRequestDispatcher(welcome);
if (dispatcher!=null)
{
if (included.booleanValue())
dispatcher.include(request,response);
else
{
request.setAttribute("org.eclipse.jetty.server.welcome",welcome);
dispatcher.forward(request,response);
}
}
}
}
else
{
content=new HttpContent.ResourceAsHttpContent(resource,_mimeTypes.getMimeByExtension(resource.toString()),_etags);
if (included.booleanValue() || passConditionalHeaders(request,response, resource,content))
sendDirectory(request,response,resource,pathInContext);
}
}
}
catch(IllegalArgumentException e)
{
LOG.warn(Log.EXCEPTION,e);
if(!response.isCommitted())
response.sendError(500, e.getMessage());
}
finally
{
if (close_content)
{
if (content!=null)
content.release();
else if (resource!=null)
resource.close();
}
}
}
/**
* @param resource
* @return
*/
protected boolean isGzippedContent(String path)
{
if (path == null) return false;
for (String suffix:_gzipEquivalentFileExtensions)
if (path.endsWith(suffix))
return true;
return false;
}
/* ------------------------------------------------------------ */
private boolean hasDefinedRange(Enumeration<String> reqRanges)
{
return (reqRanges!=null && reqRanges.hasMoreElements());
}
/* ------------------------------------------------------------ */
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
doGet(request,response);
}
/* ------------------------------------------------------------ */
/* (non-Javadoc)
* @see javax.servlet.http.HttpServlet#doTrace(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
@Override
protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
}
/* ------------------------------------------------------------ */
@Override
protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
resp.setHeader("Allow", "GET,HEAD,POST,OPTIONS");
}
/* ------------------------------------------------------------ */
/**
* Finds a matching welcome file for the supplied {@link Resource}. This will be the first entry in the list of
* configured {@link #_welcomes welcome files} that existing within the directory referenced by the <code>Resource</code>.
* If the resource is not a directory, or no matching file is found, then it may look for a valid servlet mapping.
* If there is none, then <code>null</code> is returned.
* The list of welcome files is read from the {@link ContextHandler} for this servlet, or
* <code>"index.jsp" , "index.html"</code> if that is <code>null</code>.
* @param resource
* @return The path of the matching welcome file in context or null.
* @throws IOException
* @throws MalformedURLException
*/
private String getWelcomeFile(String pathInContext) throws MalformedURLException, IOException
{
if (_welcomes==null)
return null;
String welcome_servlet=null;
for (int i=0;i<_welcomes.length;i++)
{
String welcome_in_context=URIUtil.addPaths(pathInContext,_welcomes[i]);
Resource welcome=getResource(welcome_in_context);
if (welcome!=null && welcome.exists())
return _welcomes[i];
if ((_welcomeServlets || _welcomeExactServlets) && welcome_servlet==null)
{
MappedEntry<?> entry=_servletHandler.getHolderEntry(welcome_in_context);
if (entry!=null && entry.getValue()!=_defaultHolder &&
(_welcomeServlets || (_welcomeExactServlets && entry.getKey().equals(welcome_in_context))))
welcome_servlet=welcome_in_context;
}
}
return welcome_servlet;
}
/* ------------------------------------------------------------ */
/* Check modification date headers.
*/
protected boolean passConditionalHeaders(HttpServletRequest request,HttpServletResponse response, Resource resource, HttpContent content)
throws IOException
{
try
{
if (!HttpMethod.HEAD.is(request.getMethod()))
{
if (_etags)
{
String ifm=request.getHeader(HttpHeader.IF_MATCH.asString());
if (ifm!=null)
{
boolean match=false;
if (content.getETag()!=null)
{
QuotedStringTokenizer quoted = new QuotedStringTokenizer(ifm,", ",false,true);
while (!match && quoted.hasMoreTokens())
{
String tag = quoted.nextToken();
if (content.getETag().equals(tag))
match=true;
}
}
if (!match)
{
response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
return false;
}
}
String if_non_match_etag=request.getHeader(HttpHeader.IF_NONE_MATCH.asString());
if (if_non_match_etag!=null && content.getETag()!=null)
{
// Look for GzipFiltered version of etag
if (content.getETag().equals(request.getAttribute("o.e.j.s.GzipFilter.ETag")))
{
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
response.setHeader(HttpHeader.ETAG.asString(),if_non_match_etag);
return false;
}
// Handle special case of exact match.
if (content.getETag().equals(if_non_match_etag))
{
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
response.setHeader(HttpHeader.ETAG.asString(),content.getETag());
return false;
}
// Handle list of tags
QuotedStringTokenizer quoted = new QuotedStringTokenizer(if_non_match_etag,", ",false,true);
while (quoted.hasMoreTokens())
{
String tag = quoted.nextToken();
if (content.getETag().equals(tag))
{
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
response.setHeader(HttpHeader.ETAG.asString(),content.getETag());
return false;
}
}
// If etag requires content to be served, then do not check if-modified-since
return true;
}
}
// Handle if modified since
String ifms=request.getHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
if (ifms!=null)
{
//Get jetty's Response impl
String mdlm=content.getLastModified();
if (mdlm!=null && ifms.equals(mdlm))
{
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
if (_etags)
response.setHeader(HttpHeader.ETAG.asString(),content.getETag());
response.flushBuffer();
return false;
}
long ifmsl=request.getDateHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
if (ifmsl!=-1 && resource.lastModified()/1000 <= ifmsl/1000)
{
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
if (_etags)
response.setHeader(HttpHeader.ETAG.asString(),content.getETag());
response.flushBuffer();
return false;
}
}
// Parse the if[un]modified dates and compare to resource
long date=request.getDateHeader(HttpHeader.IF_UNMODIFIED_SINCE.asString());
if (date!=-1 && resource.lastModified()/1000 > date/1000)
{
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
return false;
}
}
}
catch(IllegalArgumentException iae)
{
if(!response.isCommitted())
response.sendError(400, iae.getMessage());
throw iae;
}
return true;
}
/* ------------------------------------------------------------------- */
protected void sendDirectory(HttpServletRequest request,
HttpServletResponse response,
Resource resource,
String pathInContext)
throws IOException
{
if (!_dirAllowed)
{
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
byte[] data=null;
String base = URIUtil.addPaths(request.getRequestURI(),URIUtil.SLASH);
//If the DefaultServlet has a resource base set, use it
if (_resourceBase != null)
{
// handle ResourceCollection
if (_resourceBase instanceof ResourceCollection)
resource=_resourceBase.addPath(pathInContext);
}
//Otherwise, try using the resource base of its enclosing context handler
else if (_contextHandler.getBaseResource() instanceof ResourceCollection)
resource=_contextHandler.getBaseResource().addPath(pathInContext);
String dir = resource.getListHTML(base,pathInContext.length()>1);
if (dir==null)
{
response.sendError(HttpServletResponse.SC_FORBIDDEN,
"No directory");
return;
}
data=dir.getBytes("UTF-8");
response.setContentType("text/html; charset=UTF-8");
response.setContentLength(data.length);
response.getOutputStream().write(data);
}
/* ------------------------------------------------------------ */
protected boolean sendData(HttpServletRequest request,
HttpServletResponse response,
boolean include,
Resource resource,
final HttpContent content,
Enumeration<String> reqRanges)
throws IOException
{
final long content_length = (content==null)?resource.length():content.getContentLength();
// Get the output stream (or writer)
OutputStream out =null;
boolean written;
try
{
out = response.getOutputStream();
// has a filter already written to the response?
written = out instanceof HttpOutput
? ((HttpOutput)out).isWritten()
: true;
}
catch(IllegalStateException e)
{
out = new WriterOutputStream(response.getWriter());
written=true; // there may be data in writer buffer, so assume written
}
if (LOG.isDebugEnabled())
LOG.debug(String.format("sendData content=%s out=%s async=%b",content,out,request.isAsyncSupported()));
if ( reqRanges == null || !reqRanges.hasMoreElements() || content_length<0)
{
// if there were no ranges, send entire entity
if (include)
{
resource.writeTo(out,0,content_length);
}
// else if we can't do a bypass write because of wrapping
else if (content==null || written || !(out instanceof HttpOutput))
{
// write normally
writeHeaders(response,content,written?-1:content_length);
ByteBuffer buffer = (content==null)?null:content.getIndirectBuffer();
if (buffer!=null)
BufferUtil.writeTo(buffer,out);
else
resource.writeTo(out,0,content_length);
}
// else do a bypass write
else
{
// write the headers
if (response instanceof Response)
{
Response r = (Response)response;
writeOptionHeaders(r.getHttpFields());
r.setHeaders(content);
}
else
writeHeaders(response,content,content_length);
// write the content asynchronously if supported
if (request.isAsyncSupported())
{
final AsyncContext context = request.startAsync();
context.setTimeout(0);
((HttpOutput)out).sendContent(content,new Callback()
{
@Override
public void succeeded()
{
context.complete();
content.release();
}
@Override
public void failed(Throwable x)
{
if (x instanceof IOException)
LOG.debug(x);
else
LOG.warn(x);
context.complete();
content.release();
}
@Override
public String toString()
{
return String.format("DefaultServlet@%x$CB", DefaultServlet.this.hashCode());
}
});
return false;
}
// otherwise write content blocking
((HttpOutput)out).sendContent(content);
}
}
else
{
// Parse the satisfiable ranges
List<InclusiveByteRange> ranges =InclusiveByteRange.satisfiableRanges(reqRanges,content_length);
// if there are no satisfiable ranges, send 416 response
if (ranges==null || ranges.size()==0)
{
writeHeaders(response, content, content_length);
response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
response.setHeader(HttpHeader.CONTENT_RANGE.asString(),
InclusiveByteRange.to416HeaderRangeString(content_length));
resource.writeTo(out,0,content_length);
return true;
}
// if there is only a single valid range (must be satisfiable
// since were here now), send that range with a 216 response
if ( ranges.size()== 1)
{
InclusiveByteRange singleSatisfiableRange = ranges.get(0);
long singleLength = singleSatisfiableRange.getSize(content_length);
writeHeaders(response,content,singleLength );
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
if (!response.containsHeader(HttpHeader.DATE.asString()))
response.addDateHeader(HttpHeader.DATE.asString(),System.currentTimeMillis());
response.setHeader(HttpHeader.CONTENT_RANGE.asString(),
singleSatisfiableRange.toHeaderRangeString(content_length));
resource.writeTo(out,singleSatisfiableRange.getFirst(content_length),singleLength);
return true;
}
// multiple non-overlapping valid ranges cause a multipart
// 216 response which does not require an overall
// content-length header
//
writeHeaders(response,content,-1);
String mimetype=(content==null?null:content.getContentType());
if (mimetype==null)
LOG.warn("Unknown mimetype for "+request.getRequestURI());
MultiPartOutputStream multi = new MultiPartOutputStream(out);
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
if (!response.containsHeader(HttpHeader.DATE.asString()))
response.addDateHeader(HttpHeader.DATE.asString(),System.currentTimeMillis());
// If the request has a "Request-Range" header then we need to
// send an old style multipart/x-byteranges Content-Type. This
// keeps Netscape and acrobat happy. This is what Apache does.
String ctp;
if (request.getHeader(HttpHeader.REQUEST_RANGE.asString())!=null)
ctp = "multipart/x-byteranges; boundary=";
else
ctp = "multipart/byteranges; boundary=";
response.setContentType(ctp+multi.getBoundary());
InputStream in=resource.getInputStream();
long pos=0;
// calculate the content-length
int length=0;
String[] header = new String[ranges.size()];
for (int i=0;i<ranges.size();i++)
{
InclusiveByteRange ibr = ranges.get(i);
header[i]=ibr.toHeaderRangeString(content_length);
length+=
((i>0)?2:0)+
2+multi.getBoundary().length()+2+
(mimetype==null?0:HttpHeader.CONTENT_TYPE.asString().length()+2+mimetype.length())+2+
HttpHeader.CONTENT_RANGE.asString().length()+2+header[i].length()+2+
2+
(ibr.getLast(content_length)-ibr.getFirst(content_length))+1;
}
length+=2+2+multi.getBoundary().length()+2+2;
response.setContentLength(length);
for (int i=0;i<ranges.size();i++)
{
InclusiveByteRange ibr = ranges.get(i);
multi.startPart(mimetype,new String[]{HttpHeader.CONTENT_RANGE+": "+header[i]});
long start=ibr.getFirst(content_length);
long size=ibr.getSize(content_length);
if (in!=null)
{
// Handle non cached resource
if (start<pos)
{
in.close();
in=resource.getInputStream();
pos=0;
}
if (pos<start)
{
in.skip(start-pos);
pos=start;
}
IO.copy(in,multi,size);
pos+=size;
}
else
// Handle cached resource
(resource).writeTo(multi,start,size);
}
if (in!=null)
in.close();
multi.close();
}
return true;
}
/* ------------------------------------------------------------ */
protected void writeHeaders(HttpServletResponse response,HttpContent content,long count)
{
if (content == null)
{
// No content, then no headers to process
// This is possible during bypass write because of wrapping
// See .sendData() for more details.
return;
}
if (content.getContentType()!=null && response.getContentType()==null)
response.setContentType(content.getContentType().toString());
if (response instanceof Response)
{
Response r=(Response)response;
HttpFields fields = r.getHttpFields();
if (content.getLastModified()!=null)
fields.put(HttpHeader.LAST_MODIFIED,content.getLastModified());
else if (content.getResource()!=null)
{
long lml=content.getResource().lastModified();
if (lml!=-1)
fields.putDateField(HttpHeader.LAST_MODIFIED,lml);
}
if (count != -1)
r.setLongContentLength(count);
writeOptionHeaders(fields);
if (_etags)
fields.put(HttpHeader.ETAG,content.getETag());
}
else
{
long lml=content.getResource().lastModified();
if (lml>=0)
response.setDateHeader(HttpHeader.LAST_MODIFIED.asString(),lml);
if (count != -1)
{
if (count<Integer.MAX_VALUE)
response.setContentLength((int)count);
else
response.setHeader(HttpHeader.CONTENT_LENGTH.asString(),Long.toString(count));
}
writeOptionHeaders(response);
if (_etags)
response.setHeader(HttpHeader.ETAG.asString(),content.getETag());
}
}
/* ------------------------------------------------------------ */
protected void writeOptionHeaders(HttpFields fields)
{
if (_acceptRanges)
fields.put(ACCEPT_RANGES);
if (_cacheControl!=null)
fields.put(_cacheControl);
}
/* ------------------------------------------------------------ */
protected void writeOptionHeaders(HttpServletResponse response)
{
if (_acceptRanges)
response.setHeader(HttpHeader.ACCEPT_RANGES.asString(),"bytes");
if (_cacheControl!=null)
response.setHeader(HttpHeader.CACHE_CONTROL.asString(),_cacheControl.getValue());
}
/* ------------------------------------------------------------ */
/*
* @see javax.servlet.Servlet#destroy()
*/
@Override
public void destroy()
{
if (_cache!=null)
_cache.flushCache();
super.destroy();
}
}