3126 lines
111 KiB
Java
3126 lines
111 KiB
Java
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
* contributor license agreements. See the NOTICE file distributed with
|
|
* this work for additional information regarding copyright ownership.
|
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
|
* (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package org.apache.catalina.servlets;
|
|
|
|
import java.io.BufferedInputStream;
|
|
import java.io.ByteArrayInputStream;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileNotFoundException;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.InputStreamReader;
|
|
import java.io.OutputStreamWriter;
|
|
import java.io.PrintWriter;
|
|
import java.io.RandomAccessFile;
|
|
import java.io.Reader;
|
|
import java.io.Serializable;
|
|
import java.io.StringReader;
|
|
import java.io.StringWriter;
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.nio.charset.Charset;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.security.AccessController;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collections;
|
|
import java.util.Comparator;
|
|
import java.util.Enumeration;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.StringTokenizer;
|
|
|
|
import javax.servlet.DispatcherType;
|
|
import javax.servlet.RequestDispatcher;
|
|
import javax.servlet.ServletContext;
|
|
import javax.servlet.ServletException;
|
|
import javax.servlet.ServletOutputStream;
|
|
import javax.servlet.ServletResponse;
|
|
import javax.servlet.ServletResponseWrapper;
|
|
import javax.servlet.UnavailableException;
|
|
import javax.servlet.http.HttpServlet;
|
|
import javax.servlet.http.HttpServletRequest;
|
|
import javax.servlet.http.HttpServletResponse;
|
|
import javax.xml.parsers.DocumentBuilder;
|
|
import javax.xml.parsers.DocumentBuilderFactory;
|
|
import javax.xml.parsers.ParserConfigurationException;
|
|
import javax.xml.transform.Source;
|
|
import javax.xml.transform.Transformer;
|
|
import javax.xml.transform.TransformerException;
|
|
import javax.xml.transform.TransformerFactory;
|
|
import javax.xml.transform.dom.DOMSource;
|
|
import javax.xml.transform.stream.StreamResult;
|
|
import javax.xml.transform.stream.StreamSource;
|
|
|
|
import org.apache.catalina.Context;
|
|
import org.apache.catalina.Globals;
|
|
import org.apache.catalina.WebResource;
|
|
import org.apache.catalina.WebResourceRoot;
|
|
import org.apache.catalina.connector.RequestFacade;
|
|
import org.apache.catalina.connector.ResponseFacade;
|
|
import org.apache.catalina.util.IOTools;
|
|
import org.apache.catalina.util.ServerInfo;
|
|
import org.apache.catalina.util.URLEncoder;
|
|
import org.apache.catalina.webresources.CachedResource;
|
|
import org.apache.tomcat.util.buf.B2CConverter;
|
|
import org.apache.tomcat.util.http.ResponseUtil;
|
|
import org.apache.tomcat.util.http.parser.ContentRange;
|
|
import org.apache.tomcat.util.http.parser.Ranges;
|
|
import org.apache.tomcat.util.res.StringManager;
|
|
import org.apache.tomcat.util.security.Escape;
|
|
import org.apache.tomcat.util.security.PrivilegedGetTccl;
|
|
import org.apache.tomcat.util.security.PrivilegedSetTccl;
|
|
import org.w3c.dom.Document;
|
|
import org.xml.sax.InputSource;
|
|
import org.xml.sax.SAXException;
|
|
import org.xml.sax.ext.EntityResolver2;
|
|
|
|
|
|
/**
|
|
* <p>The default resource-serving servlet for most web applications,
|
|
* used to serve static resources such as HTML pages and images.
|
|
* </p>
|
|
* <p>
|
|
* This servlet is intended to be mapped to <em>/</em> e.g.:
|
|
* </p>
|
|
* <pre>
|
|
* <servlet-mapping>
|
|
* <servlet-name>default</servlet-name>
|
|
* <url-pattern>/</url-pattern>
|
|
* </servlet-mapping>
|
|
* </pre>
|
|
* <p>It can be mapped to sub-paths, however in all cases resources are served
|
|
* from the web application resource root using the full path from the root
|
|
* of the web application context.
|
|
* <br>e.g. given a web application structure:
|
|
*</p>
|
|
* <pre>
|
|
* /context
|
|
* /images
|
|
* tomcat2.jpg
|
|
* /static
|
|
* /images
|
|
* tomcat.jpg
|
|
* </pre>
|
|
* <p>
|
|
* ... and a servlet mapping that maps only <code>/static/*</code> to the default servlet:
|
|
* </p>
|
|
* <pre>
|
|
* <servlet-mapping>
|
|
* <servlet-name>default</servlet-name>
|
|
* <url-pattern>/static/*</url-pattern>
|
|
* </servlet-mapping>
|
|
* </pre>
|
|
* <p>
|
|
* Then a request to <code>/context/static/images/tomcat.jpg</code> will succeed
|
|
* while a request to <code>/context/images/tomcat2.jpg</code> will fail.
|
|
* </p>
|
|
* @author Craig R. McClanahan
|
|
* @author Remy Maucherat
|
|
*/
|
|
public class DefaultServlet extends HttpServlet {
|
|
|
|
private static final long serialVersionUID = 1L;
|
|
|
|
/**
|
|
* The string manager for this package.
|
|
*/
|
|
protected static final StringManager sm = StringManager.getManager(Constants.Package);
|
|
|
|
private static final DocumentBuilderFactory factory;
|
|
|
|
private static final SecureEntityResolver secureEntityResolver;
|
|
|
|
/**
|
|
* Full range marker.
|
|
*/
|
|
protected static final ArrayList<Range> FULL = new ArrayList<>();
|
|
|
|
private static final Range IGNORE = new Range();
|
|
|
|
/**
|
|
* MIME multipart separation string
|
|
*/
|
|
protected static final String mimeSeparation = "CATALINA_MIME_BOUNDARY";
|
|
|
|
/**
|
|
* JNDI resources name.
|
|
* @deprecated Unused. Will be removed in Tomcat 9.
|
|
*/
|
|
@Deprecated
|
|
protected static final String RESOURCES_JNDI_NAME = "java:/comp/Resources";
|
|
|
|
/**
|
|
* Size of file transfer buffer in bytes.
|
|
*/
|
|
protected static final int BUFFER_SIZE = 4096;
|
|
|
|
|
|
// ----------------------------------------------------- Static Initializer
|
|
|
|
static {
|
|
if (Globals.IS_SECURITY_ENABLED) {
|
|
factory = DocumentBuilderFactory.newInstance();
|
|
factory.setNamespaceAware(true);
|
|
factory.setValidating(false);
|
|
secureEntityResolver = new SecureEntityResolver();
|
|
} else {
|
|
factory = null;
|
|
secureEntityResolver = null;
|
|
}
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------- Instance Variables
|
|
|
|
/**
|
|
* The debugging detail level for this servlet.
|
|
*/
|
|
protected int debug = 0;
|
|
|
|
/**
|
|
* The input buffer size to use when serving resources.
|
|
*/
|
|
protected int input = 2048;
|
|
|
|
/**
|
|
* Should we generate directory listings?
|
|
*/
|
|
protected boolean listings = false;
|
|
|
|
/**
|
|
* Read only flag. By default, it's set to true.
|
|
*/
|
|
protected boolean readOnly = true;
|
|
|
|
/**
|
|
* List of compression formats to serve and their preference order.
|
|
*/
|
|
protected CompressionFormat[] compressionFormats;
|
|
|
|
/**
|
|
* The output buffer size to use when serving resources.
|
|
*/
|
|
protected int output = 2048;
|
|
|
|
/**
|
|
* Allow customized directory listing per directory.
|
|
*/
|
|
protected String localXsltFile = null;
|
|
|
|
/**
|
|
* Allow customized directory listing per context.
|
|
*/
|
|
protected String contextXsltFile = null;
|
|
|
|
/**
|
|
* Allow customized directory listing per instance.
|
|
*/
|
|
protected String globalXsltFile = null;
|
|
|
|
/**
|
|
* Allow a readme file to be included.
|
|
*/
|
|
protected String readmeFile = null;
|
|
|
|
/**
|
|
* The complete set of web application resources
|
|
*/
|
|
protected transient WebResourceRoot resources = null;
|
|
|
|
/**
|
|
* File encoding to be used when reading static files. If none is specified
|
|
* the platform default is used.
|
|
*/
|
|
protected String fileEncoding = null;
|
|
private transient Charset fileEncodingCharset = null;
|
|
|
|
/**
|
|
* If a file has a BOM, should that be used in preference to fileEncoding?
|
|
*/
|
|
private boolean useBomIfPresent = true;
|
|
|
|
/**
|
|
* Minimum size for sendfile usage in bytes.
|
|
*/
|
|
protected int sendfileSize = 48 * 1024;
|
|
|
|
/**
|
|
* Should the Accept-Ranges: bytes header be send with static resources?
|
|
*/
|
|
protected boolean useAcceptRanges = true;
|
|
|
|
/**
|
|
* Flag to determine if server information is presented.
|
|
*/
|
|
protected boolean showServerInfo = true;
|
|
|
|
/**
|
|
* Flag to determine if resources should be sorted.
|
|
*/
|
|
protected boolean sortListings = false;
|
|
|
|
/**
|
|
* The sorting manager for sorting files and directories.
|
|
*/
|
|
protected transient SortManager sortManager;
|
|
|
|
/**
|
|
* Flag that indicates whether partial PUTs are permitted.
|
|
*/
|
|
private boolean allowPartialPut = true;
|
|
|
|
|
|
// --------------------------------------------------------- Public Methods
|
|
|
|
/**
|
|
* Finalize this servlet.
|
|
*/
|
|
@Override
|
|
public void destroy() {
|
|
// NOOP
|
|
}
|
|
|
|
|
|
/**
|
|
* Initialize this servlet.
|
|
*/
|
|
@Override
|
|
public void init() throws ServletException {
|
|
|
|
if (getServletConfig().getInitParameter("debug") != null)
|
|
debug = Integer.parseInt(getServletConfig().getInitParameter("debug"));
|
|
|
|
if (getServletConfig().getInitParameter("input") != null)
|
|
input = Integer.parseInt(getServletConfig().getInitParameter("input"));
|
|
|
|
if (getServletConfig().getInitParameter("output") != null)
|
|
output = Integer.parseInt(getServletConfig().getInitParameter("output"));
|
|
|
|
listings = Boolean.parseBoolean(getServletConfig().getInitParameter("listings"));
|
|
|
|
if (getServletConfig().getInitParameter("readonly") != null)
|
|
readOnly = Boolean.parseBoolean(getServletConfig().getInitParameter("readonly"));
|
|
|
|
compressionFormats = parseCompressionFormats(
|
|
getServletConfig().getInitParameter("precompressed"),
|
|
getServletConfig().getInitParameter("gzip"));
|
|
|
|
if (getServletConfig().getInitParameter("sendfileSize") != null)
|
|
sendfileSize =
|
|
Integer.parseInt(getServletConfig().getInitParameter("sendfileSize")) * 1024;
|
|
|
|
fileEncoding = getServletConfig().getInitParameter("fileEncoding");
|
|
if (fileEncoding == null) {
|
|
fileEncodingCharset = Charset.defaultCharset();
|
|
fileEncoding = fileEncodingCharset.name();
|
|
} else {
|
|
try {
|
|
fileEncodingCharset = B2CConverter.getCharset(fileEncoding);
|
|
} catch (UnsupportedEncodingException e) {
|
|
throw new ServletException(e);
|
|
}
|
|
}
|
|
|
|
if (getServletConfig().getInitParameter("useBomIfPresent") != null)
|
|
useBomIfPresent = Boolean.parseBoolean(
|
|
getServletConfig().getInitParameter("useBomIfPresent"));
|
|
|
|
globalXsltFile = getServletConfig().getInitParameter("globalXsltFile");
|
|
contextXsltFile = getServletConfig().getInitParameter("contextXsltFile");
|
|
localXsltFile = getServletConfig().getInitParameter("localXsltFile");
|
|
readmeFile = getServletConfig().getInitParameter("readmeFile");
|
|
|
|
if (getServletConfig().getInitParameter("useAcceptRanges") != null)
|
|
useAcceptRanges = Boolean.parseBoolean(getServletConfig().getInitParameter("useAcceptRanges"));
|
|
|
|
// Sanity check on the specified buffer sizes
|
|
if (input < 256)
|
|
input = 256;
|
|
if (output < 256)
|
|
output = 256;
|
|
|
|
if (debug > 0) {
|
|
log("DefaultServlet.init: input buffer size=" + input +
|
|
", output buffer size=" + output);
|
|
}
|
|
|
|
// Load the web resources
|
|
resources = (WebResourceRoot) getServletContext().getAttribute(
|
|
Globals.RESOURCES_ATTR);
|
|
|
|
if (resources == null) {
|
|
throw new UnavailableException(sm.getString("defaultServlet.noResources"));
|
|
}
|
|
|
|
if (getServletConfig().getInitParameter("showServerInfo") != null) {
|
|
showServerInfo = Boolean.parseBoolean(getServletConfig().getInitParameter("showServerInfo"));
|
|
}
|
|
|
|
if (getServletConfig().getInitParameter("sortListings") != null) {
|
|
sortListings = Boolean.parseBoolean(getServletConfig().getInitParameter("sortListings"));
|
|
|
|
if(sortListings) {
|
|
boolean sortDirectoriesFirst;
|
|
if (getServletConfig().getInitParameter("sortDirectoriesFirst") != null) {
|
|
sortDirectoriesFirst = Boolean.parseBoolean(getServletConfig().getInitParameter("sortDirectoriesFirst"));
|
|
} else {
|
|
sortDirectoriesFirst = false;
|
|
}
|
|
|
|
sortManager = new SortManager(sortDirectoriesFirst);
|
|
}
|
|
}
|
|
|
|
if (getServletConfig().getInitParameter("allowPartialPut") != null) {
|
|
allowPartialPut = Boolean.parseBoolean(getServletConfig().getInitParameter("allowPartialPut"));
|
|
}
|
|
}
|
|
|
|
private CompressionFormat[] parseCompressionFormats(String precompressed, String gzip) {
|
|
List<CompressionFormat> ret = new ArrayList<>();
|
|
if (precompressed != null && precompressed.indexOf('=') > 0) {
|
|
for (String pair : precompressed.split(",")) {
|
|
String[] setting = pair.split("=");
|
|
String encoding = setting[0];
|
|
String extension = setting[1];
|
|
ret.add(new CompressionFormat(extension, encoding));
|
|
}
|
|
} else if (precompressed != null) {
|
|
if (Boolean.parseBoolean(precompressed)) {
|
|
ret.add(new CompressionFormat(".br", "br"));
|
|
ret.add(new CompressionFormat(".gz", "gzip"));
|
|
}
|
|
} else if (Boolean.parseBoolean(gzip)) {
|
|
// gzip handling is for backwards compatibility with Tomcat 8.x
|
|
ret.add(new CompressionFormat(".gz", "gzip"));
|
|
}
|
|
return ret.toArray(new CompressionFormat[ret.size()]);
|
|
}
|
|
|
|
|
|
// ------------------------------------------------------ Protected Methods
|
|
|
|
|
|
/**
|
|
* Return the relative path associated with this servlet.
|
|
*
|
|
* @param request The servlet request we are processing
|
|
* @return the relative path
|
|
*/
|
|
protected String getRelativePath(HttpServletRequest request) {
|
|
return getRelativePath(request, false);
|
|
}
|
|
|
|
protected String getRelativePath(HttpServletRequest request, boolean allowEmptyPath) {
|
|
// IMPORTANT: DefaultServlet can be mapped to '/' or '/path/*' but always
|
|
// serves resources from the web app root with context rooted paths.
|
|
// i.e. it cannot be used to mount the web app root under a sub-path
|
|
// This method must construct a complete context rooted path, although
|
|
// subclasses can change this behaviour.
|
|
|
|
String servletPath;
|
|
String pathInfo;
|
|
|
|
if (request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null) {
|
|
// For includes, get the info from the attributes
|
|
pathInfo = (String) request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
|
|
servletPath = (String) request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
|
|
} else {
|
|
pathInfo = request.getPathInfo();
|
|
servletPath = request.getServletPath();
|
|
}
|
|
|
|
StringBuilder result = new StringBuilder();
|
|
if (servletPath.length() > 0) {
|
|
result.append(servletPath);
|
|
}
|
|
if (pathInfo != null) {
|
|
result.append(pathInfo);
|
|
}
|
|
if (result.length() == 0 && !allowEmptyPath) {
|
|
result.append('/');
|
|
}
|
|
|
|
return result.toString();
|
|
}
|
|
|
|
|
|
/**
|
|
* Determines the appropriate path to prepend resources with
|
|
* when generating directory listings. Depending on the behaviour of
|
|
* {@link #getRelativePath(HttpServletRequest)} this will change.
|
|
* @param request the request to determine the path for
|
|
* @return the prefix to apply to all resources in the listing.
|
|
*/
|
|
protected String getPathPrefix(final HttpServletRequest request) {
|
|
return request.getContextPath();
|
|
}
|
|
|
|
|
|
@Override
|
|
protected void service(HttpServletRequest req, HttpServletResponse resp)
|
|
throws ServletException, IOException {
|
|
|
|
if (req.getDispatcherType() == DispatcherType.ERROR) {
|
|
doGet(req, resp);
|
|
} else {
|
|
super.service(req, resp);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Process a GET request for the specified resource.
|
|
*
|
|
* @param request The servlet request we are processing
|
|
* @param response The servlet response we are creating
|
|
*
|
|
* @exception IOException if an input/output error occurs
|
|
* @exception ServletException if a servlet-specified error occurs
|
|
*/
|
|
@Override
|
|
protected void doGet(HttpServletRequest request,
|
|
HttpServletResponse response)
|
|
throws IOException, ServletException {
|
|
|
|
// Serve the requested resource, including the data content
|
|
serveResource(request, response, true, fileEncoding);
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Process a HEAD request for the specified resource.
|
|
*
|
|
* @param request The servlet request we are processing
|
|
* @param response The servlet response we are creating
|
|
*
|
|
* @exception IOException if an input/output error occurs
|
|
* @exception ServletException if a servlet-specified error occurs
|
|
*/
|
|
@Override
|
|
protected void doHead(HttpServletRequest request, HttpServletResponse response)
|
|
throws IOException, ServletException {
|
|
// Serve the requested resource, without the data content unless we are
|
|
// being included since in that case the content needs to be provided so
|
|
// the correct content length is reported for the including resource
|
|
boolean serveContent = DispatcherType.INCLUDE.equals(request.getDispatcherType());
|
|
serveResource(request, response, serveContent, fileEncoding);
|
|
}
|
|
|
|
|
|
/**
|
|
* Override default implementation to ensure that TRACE is correctly
|
|
* handled.
|
|
*
|
|
* @param req the {@link HttpServletRequest} object that
|
|
* contains the request the client made of
|
|
* the servlet
|
|
*
|
|
* @param resp the {@link HttpServletResponse} object that
|
|
* contains the response the servlet returns
|
|
* to the client
|
|
*
|
|
* @exception IOException if an input or output error occurs
|
|
* while the servlet is handling the
|
|
* OPTIONS request
|
|
*
|
|
* @exception ServletException if the request for the
|
|
* OPTIONS cannot be handled
|
|
*/
|
|
@Override
|
|
protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
|
|
throws ServletException, IOException {
|
|
|
|
resp.setHeader("Allow", determineMethodsAllowed(req));
|
|
}
|
|
|
|
|
|
protected String determineMethodsAllowed(HttpServletRequest req) {
|
|
StringBuilder allow = new StringBuilder();
|
|
|
|
// Start with methods that are always allowed
|
|
allow.append("OPTIONS, GET, HEAD, POST");
|
|
|
|
// PUT and DELETE depend on readonly
|
|
if (!readOnly) {
|
|
allow.append(", PUT, DELETE");
|
|
}
|
|
|
|
// Trace - assume disabled unless we can prove otherwise
|
|
if (req instanceof RequestFacade &&
|
|
((RequestFacade) req).getAllowTrace()) {
|
|
allow.append(", TRACE");
|
|
}
|
|
|
|
return allow.toString();
|
|
}
|
|
|
|
|
|
protected void sendNotAllowed(HttpServletRequest req, HttpServletResponse resp)
|
|
throws IOException {
|
|
resp.addHeader("Allow", determineMethodsAllowed(req));
|
|
resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED);
|
|
}
|
|
|
|
|
|
/**
|
|
* Process a POST request for the specified resource.
|
|
*
|
|
* @param request The servlet request we are processing
|
|
* @param response The servlet response we are creating
|
|
*
|
|
* @exception IOException if an input/output error occurs
|
|
* @exception ServletException if a servlet-specified error occurs
|
|
*/
|
|
@Override
|
|
protected void doPost(HttpServletRequest request,
|
|
HttpServletResponse response)
|
|
throws IOException, ServletException {
|
|
doGet(request, response);
|
|
}
|
|
|
|
|
|
/**
|
|
* Process a PUT request for the specified resource.
|
|
*
|
|
* @param req The servlet request we are processing
|
|
* @param resp The servlet response we are creating
|
|
*
|
|
* @exception IOException if an input/output error occurs
|
|
* @exception ServletException if a servlet-specified error occurs
|
|
*/
|
|
@Override
|
|
protected void doPut(HttpServletRequest req, HttpServletResponse resp)
|
|
throws ServletException, IOException {
|
|
|
|
if (readOnly) {
|
|
sendNotAllowed(req, resp);
|
|
return;
|
|
}
|
|
|
|
String path = getRelativePath(req);
|
|
|
|
WebResource resource = resources.getResource(path);
|
|
|
|
Range range = parseContentRange(req, resp);
|
|
|
|
if (range == null) {
|
|
// Processing error. parseContentRange() set the error code
|
|
return;
|
|
}
|
|
|
|
InputStream resourceInputStream = null;
|
|
|
|
try {
|
|
// Append data specified in ranges to existing content for this
|
|
// resource - create a temp. file on the local filesystem to
|
|
// perform this operation
|
|
// Assume just one range is specified for now
|
|
if (range == IGNORE) {
|
|
resourceInputStream = req.getInputStream();
|
|
} else {
|
|
File contentFile = executePartialPut(req, range, path);
|
|
resourceInputStream = new FileInputStream(contentFile);
|
|
}
|
|
|
|
if (resources.write(path, resourceInputStream, true)) {
|
|
if (resource.exists()) {
|
|
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
|
|
} else {
|
|
resp.setStatus(HttpServletResponse.SC_CREATED);
|
|
}
|
|
} else {
|
|
resp.sendError(HttpServletResponse.SC_CONFLICT);
|
|
}
|
|
} finally {
|
|
if (resourceInputStream != null) {
|
|
try {
|
|
resourceInputStream.close();
|
|
} catch (IOException ioe) {
|
|
// Ignore
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Handle a partial PUT. New content specified in request is appended to
|
|
* existing content in oldRevisionContent (if present). This code does
|
|
* not support simultaneous partial updates to the same resource.
|
|
* @param req The Servlet request
|
|
* @param range The range that will be written
|
|
* @param path The path
|
|
* @return the associated file object
|
|
* @throws IOException an IO error occurred
|
|
*/
|
|
protected File executePartialPut(HttpServletRequest req, Range range,
|
|
String path)
|
|
throws IOException {
|
|
|
|
// Append data specified in ranges to existing content for this
|
|
// resource - create a temp. file on the local filesystem to
|
|
// perform this operation
|
|
File tempDir = (File) getServletContext().getAttribute
|
|
(ServletContext.TEMPDIR);
|
|
// Convert all '/' characters to '.' in resourcePath
|
|
String convertedResourcePath = path.replace('/', '.');
|
|
File contentFile = new File(tempDir, convertedResourcePath);
|
|
if (contentFile.createNewFile()) {
|
|
// Clean up contentFile when Tomcat is terminated
|
|
contentFile.deleteOnExit();
|
|
}
|
|
|
|
try (RandomAccessFile randAccessContentFile =
|
|
new RandomAccessFile(contentFile, "rw")) {
|
|
|
|
WebResource oldResource = resources.getResource(path);
|
|
|
|
// Copy data in oldRevisionContent to contentFile
|
|
if (oldResource.isFile()) {
|
|
try (BufferedInputStream bufOldRevStream =
|
|
new BufferedInputStream(oldResource.getInputStream(),
|
|
BUFFER_SIZE)) {
|
|
|
|
int numBytesRead;
|
|
byte[] copyBuffer = new byte[BUFFER_SIZE];
|
|
while ((numBytesRead = bufOldRevStream.read(copyBuffer)) != -1) {
|
|
randAccessContentFile.write(copyBuffer, 0, numBytesRead);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
randAccessContentFile.setLength(range.length);
|
|
|
|
// Append data in request input stream to contentFile
|
|
randAccessContentFile.seek(range.start);
|
|
int numBytesRead;
|
|
byte[] transferBuffer = new byte[BUFFER_SIZE];
|
|
try (BufferedInputStream requestBufInStream =
|
|
new BufferedInputStream(req.getInputStream(), BUFFER_SIZE)) {
|
|
while ((numBytesRead = requestBufInStream.read(transferBuffer)) != -1) {
|
|
randAccessContentFile.write(transferBuffer, 0, numBytesRead);
|
|
}
|
|
}
|
|
}
|
|
|
|
return contentFile;
|
|
}
|
|
|
|
|
|
/**
|
|
* Process a DELETE request for the specified resource.
|
|
*
|
|
* @param req The servlet request we are processing
|
|
* @param resp The servlet response we are creating
|
|
*
|
|
* @exception IOException if an input/output error occurs
|
|
* @exception ServletException if a servlet-specified error occurs
|
|
*/
|
|
@Override
|
|
protected void doDelete(HttpServletRequest req, HttpServletResponse resp)
|
|
throws ServletException, IOException {
|
|
|
|
if (readOnly) {
|
|
sendNotAllowed(req, resp);
|
|
return;
|
|
}
|
|
|
|
String path = getRelativePath(req);
|
|
|
|
WebResource resource = resources.getResource(path);
|
|
|
|
if (resource.exists()) {
|
|
if (resource.delete()) {
|
|
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
|
|
} else {
|
|
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
|
|
}
|
|
} else {
|
|
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Check if the conditions specified in the optional If headers are
|
|
* satisfied.
|
|
*
|
|
* @param request The servlet request we are processing
|
|
* @param response The servlet response we are creating
|
|
* @param resource The resource
|
|
* @return <code>true</code> if the resource meets all the specified
|
|
* conditions, and <code>false</code> if any of the conditions is not
|
|
* satisfied, in which case request processing is stopped
|
|
* @throws IOException an IO error occurred
|
|
*/
|
|
protected boolean checkIfHeaders(HttpServletRequest request,
|
|
HttpServletResponse response,
|
|
WebResource resource)
|
|
throws IOException {
|
|
|
|
return checkIfMatch(request, response, resource)
|
|
&& checkIfModifiedSince(request, response, resource)
|
|
&& checkIfNoneMatch(request, response, resource)
|
|
&& checkIfUnmodifiedSince(request, response, resource);
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* URL rewriter.
|
|
*
|
|
* @param path Path which has to be rewritten
|
|
* @return the rewritten path
|
|
*/
|
|
protected String rewriteUrl(String path) {
|
|
return URLEncoder.DEFAULT.encode(path, StandardCharsets.UTF_8);
|
|
}
|
|
|
|
|
|
/**
|
|
* Serve the specified resource, optionally including the data content.
|
|
*
|
|
* @param request The servlet request we are processing
|
|
* @param response The servlet response we are creating
|
|
* @param content Should the content be included?
|
|
* @param inputEncoding The encoding to use if it is necessary to access the
|
|
* source as characters rather than as bytes
|
|
*
|
|
* @exception IOException if an input/output error occurs
|
|
* @exception ServletException if a servlet-specified error occurs
|
|
*/
|
|
protected void serveResource(HttpServletRequest request,
|
|
HttpServletResponse response,
|
|
boolean content,
|
|
String inputEncoding)
|
|
throws IOException, ServletException {
|
|
|
|
boolean serveContent = content;
|
|
|
|
// Identify the requested resource path
|
|
String path = getRelativePath(request, true);
|
|
|
|
if (debug > 0) {
|
|
if (serveContent)
|
|
log("DefaultServlet.serveResource: Serving resource '" +
|
|
path + "' headers and data");
|
|
else
|
|
log("DefaultServlet.serveResource: Serving resource '" +
|
|
path + "' headers only");
|
|
}
|
|
|
|
if (path.length() == 0) {
|
|
// Context root redirect
|
|
doDirectoryRedirect(request, response);
|
|
return;
|
|
}
|
|
|
|
WebResource resource = resources.getResource(path);
|
|
boolean isError = DispatcherType.ERROR == request.getDispatcherType();
|
|
|
|
if (!resource.exists()) {
|
|
// Check if we're included so we can return the appropriate
|
|
// missing resource name in the error
|
|
String requestUri = (String) request.getAttribute(
|
|
RequestDispatcher.INCLUDE_REQUEST_URI);
|
|
if (requestUri == null) {
|
|
requestUri = request.getRequestURI();
|
|
} else {
|
|
// We're included
|
|
// SRV.9.3 says we must throw a FNFE
|
|
throw new FileNotFoundException(sm.getString(
|
|
"defaultServlet.missingResource", requestUri));
|
|
}
|
|
|
|
if (isError) {
|
|
response.sendError(((Integer) request.getAttribute(
|
|
RequestDispatcher.ERROR_STATUS_CODE)).intValue());
|
|
} else {
|
|
response.sendError(HttpServletResponse.SC_NOT_FOUND, requestUri);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!resource.canRead()) {
|
|
// Check if we're included so we can return the appropriate
|
|
// missing resource name in the error
|
|
String requestUri = (String) request.getAttribute(
|
|
RequestDispatcher.INCLUDE_REQUEST_URI);
|
|
if (requestUri == null) {
|
|
requestUri = request.getRequestURI();
|
|
} else {
|
|
// We're included
|
|
// Spec doesn't say what to do in this case but a FNFE seems
|
|
// reasonable
|
|
throw new FileNotFoundException(sm.getString(
|
|
"defaultServlet.missingResource", requestUri));
|
|
}
|
|
|
|
if (isError) {
|
|
response.sendError(((Integer) request.getAttribute(
|
|
RequestDispatcher.ERROR_STATUS_CODE)).intValue());
|
|
} else {
|
|
response.sendError(HttpServletResponse.SC_FORBIDDEN, requestUri);
|
|
}
|
|
return;
|
|
}
|
|
|
|
boolean included = false;
|
|
// Check if the conditions specified in the optional If headers are
|
|
// satisfied.
|
|
if (resource.isFile()) {
|
|
// Checking If headers
|
|
included = (request.getAttribute(
|
|
RequestDispatcher.INCLUDE_CONTEXT_PATH) != null);
|
|
if (!included && !isError && !checkIfHeaders(request, response, resource)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Find content type.
|
|
String contentType = resource.getMimeType();
|
|
if (contentType == null) {
|
|
contentType = getServletContext().getMimeType(resource.getName());
|
|
resource.setMimeType(contentType);
|
|
}
|
|
|
|
// These need to reflect the original resource, not the potentially
|
|
// precompressed version of the resource so get them now if they are going to
|
|
// be needed later
|
|
String eTag = null;
|
|
String lastModifiedHttp = null;
|
|
if (resource.isFile() && !isError) {
|
|
eTag = resource.getETag();
|
|
lastModifiedHttp = resource.getLastModifiedHttp();
|
|
}
|
|
|
|
|
|
// Serve a precompressed version of the file if present
|
|
boolean usingPrecompressedVersion = false;
|
|
if (compressionFormats.length > 0 && !included && resource.isFile() &&
|
|
!pathEndsWithCompressedExtension(path)) {
|
|
List<PrecompressedResource> precompressedResources =
|
|
getAvailablePrecompressedResources(path);
|
|
if (!precompressedResources.isEmpty()) {
|
|
ResponseUtil.addVaryFieldName(response, "accept-encoding");
|
|
PrecompressedResource bestResource =
|
|
getBestPrecompressedResource(request, precompressedResources);
|
|
if (bestResource != null) {
|
|
response.addHeader("Content-Encoding", bestResource.format.encoding);
|
|
resource = bestResource.resource;
|
|
usingPrecompressedVersion = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
ArrayList<Range> ranges = FULL;
|
|
long contentLength = -1L;
|
|
|
|
if (resource.isDirectory()) {
|
|
if (!path.endsWith("/")) {
|
|
doDirectoryRedirect(request, response);
|
|
return;
|
|
}
|
|
|
|
// Skip directory listings if we have been configured to
|
|
// suppress them
|
|
if (!listings) {
|
|
response.sendError(HttpServletResponse.SC_NOT_FOUND,
|
|
request.getRequestURI());
|
|
return;
|
|
}
|
|
contentType = "text/html;charset=UTF-8";
|
|
} else {
|
|
if (!isError) {
|
|
if (useAcceptRanges) {
|
|
// Accept ranges header
|
|
response.setHeader("Accept-Ranges", "bytes");
|
|
}
|
|
|
|
// Parse range specifier
|
|
ranges = parseRange(request, response, resource);
|
|
if (ranges == null) {
|
|
return;
|
|
}
|
|
|
|
// ETag header
|
|
response.setHeader("ETag", eTag);
|
|
|
|
// Last-Modified header
|
|
response.setHeader("Last-Modified", lastModifiedHttp);
|
|
}
|
|
|
|
// Get content length
|
|
contentLength = resource.getContentLength();
|
|
// Special case for zero length files, which would cause a
|
|
// (silent) ISE when setting the output buffer size
|
|
if (contentLength == 0L) {
|
|
serveContent = false;
|
|
}
|
|
}
|
|
|
|
ServletOutputStream ostream = null;
|
|
PrintWriter writer = null;
|
|
|
|
if (serveContent) {
|
|
// Trying to retrieve the servlet output stream
|
|
try {
|
|
ostream = response.getOutputStream();
|
|
} catch (IllegalStateException e) {
|
|
// If it fails, we try to get a Writer instead if we're
|
|
// trying to serve a text file
|
|
if (!usingPrecompressedVersion && isText(contentType)) {
|
|
writer = response.getWriter();
|
|
// Cannot reliably serve partial content with a Writer
|
|
ranges = FULL;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check to see if a Filter, Valve or wrapper has written some content.
|
|
// If it has, disable range requests and setting of a content length
|
|
// since neither can be done reliably.
|
|
ServletResponse r = response;
|
|
long contentWritten = 0;
|
|
while (r instanceof ServletResponseWrapper) {
|
|
r = ((ServletResponseWrapper) r).getResponse();
|
|
}
|
|
if (r instanceof ResponseFacade) {
|
|
contentWritten = ((ResponseFacade) r).getContentWritten();
|
|
}
|
|
if (contentWritten > 0) {
|
|
ranges = FULL;
|
|
}
|
|
|
|
String outputEncoding = response.getCharacterEncoding();
|
|
Charset charset = B2CConverter.getCharset(outputEncoding);
|
|
boolean conversionRequired;
|
|
/*
|
|
* The test below deliberately uses != to compare two Strings. This is
|
|
* because the code is looking to see if the default character encoding
|
|
* has been returned because no explicit character encoding has been
|
|
* defined. There is no clean way of doing this via the Servlet API. It
|
|
* would be possible to add a Tomcat specific API but that would require
|
|
* quite a bit of code to get to the Tomcat specific request object that
|
|
* may have been wrapped. The != test is a (slightly hacky) quick way of
|
|
* doing this.
|
|
*/
|
|
boolean outputEncodingSpecified =
|
|
outputEncoding != org.apache.coyote.Constants.DEFAULT_BODY_CHARSET.name() &&
|
|
outputEncoding != resources.getContext().getResponseCharacterEncoding();
|
|
if (!usingPrecompressedVersion && isText(contentType) && outputEncodingSpecified &&
|
|
!charset.equals(fileEncodingCharset)) {
|
|
conversionRequired = true;
|
|
// Conversion often results fewer/more/different bytes.
|
|
// That does not play nicely with range requests.
|
|
ranges = FULL;
|
|
} else {
|
|
conversionRequired = false;
|
|
}
|
|
|
|
if (resource.isDirectory() || isError || ranges == FULL ) {
|
|
// Set the appropriate output headers
|
|
if (contentType != null) {
|
|
if (debug > 0)
|
|
log("DefaultServlet.serveFile: contentType='" +
|
|
contentType + "'");
|
|
// Don't override a previously set content type
|
|
if (response.getContentType() == null) {
|
|
response.setContentType(contentType);
|
|
}
|
|
}
|
|
if (resource.isFile() && contentLength >= 0 &&
|
|
(!serveContent || ostream != null)) {
|
|
if (debug > 0)
|
|
log("DefaultServlet.serveFile: contentLength=" +
|
|
contentLength);
|
|
// Don't set a content length if something else has already
|
|
// written to the response or if conversion will be taking place
|
|
if (contentWritten == 0 && !conversionRequired) {
|
|
response.setContentLengthLong(contentLength);
|
|
}
|
|
}
|
|
|
|
if (serveContent) {
|
|
try {
|
|
response.setBufferSize(output);
|
|
} catch (IllegalStateException e) {
|
|
// Silent catch
|
|
}
|
|
InputStream renderResult = null;
|
|
if (ostream == null) {
|
|
// Output via a writer so can't use sendfile or write
|
|
// content directly.
|
|
if (resource.isDirectory()) {
|
|
renderResult = render(request, getPathPrefix(request), resource, inputEncoding);
|
|
} else {
|
|
renderResult = resource.getInputStream();
|
|
if (included) {
|
|
// Need to make sure any BOM is removed
|
|
if (!renderResult.markSupported()) {
|
|
renderResult = new BufferedInputStream(renderResult);
|
|
}
|
|
Charset bomCharset = processBom(renderResult);
|
|
if (bomCharset != null && useBomIfPresent) {
|
|
inputEncoding = bomCharset.name();
|
|
}
|
|
}
|
|
}
|
|
copy(renderResult, writer, inputEncoding);
|
|
} else {
|
|
// Output is via an OutputStream
|
|
if (resource.isDirectory()) {
|
|
renderResult = render(request, getPathPrefix(request), resource, inputEncoding);
|
|
} else {
|
|
// Output is content of resource
|
|
// Check to see if conversion is required
|
|
if (conversionRequired || included) {
|
|
// When including a file, we need to check for a BOM
|
|
// to determine if a conversion is required, so we
|
|
// might as well always convert
|
|
InputStream source = resource.getInputStream();
|
|
if (!source.markSupported()) {
|
|
source = new BufferedInputStream(source);
|
|
}
|
|
Charset bomCharset = processBom(source);
|
|
if (bomCharset != null && useBomIfPresent) {
|
|
inputEncoding = bomCharset.name();
|
|
}
|
|
// Following test also ensures included resources
|
|
// are converted if an explicit output encoding was
|
|
// specified
|
|
if (outputEncodingSpecified) {
|
|
OutputStreamWriter osw = new OutputStreamWriter(ostream, charset);
|
|
PrintWriter pw = new PrintWriter(osw);
|
|
copy(source, pw, inputEncoding);
|
|
pw.flush();
|
|
} else {
|
|
// Just included but no conversion
|
|
renderResult = source;
|
|
}
|
|
} else {
|
|
if (!checkSendfile(request, response, resource, contentLength, null)) {
|
|
// sendfile not possible so check if resource
|
|
// content is available directly via
|
|
// CachedResource. Do not want to call
|
|
// getContent() on other resource
|
|
// implementations as that could trigger loading
|
|
// the contents of a very large file into memory
|
|
byte[] resourceBody = null;
|
|
if (resource instanceof CachedResource) {
|
|
resourceBody = resource.getContent();
|
|
}
|
|
if (resourceBody == null) {
|
|
// Resource content not directly available,
|
|
// use InputStream
|
|
renderResult = resource.getInputStream();
|
|
} else {
|
|
// Use the resource content directly
|
|
ostream.write(resourceBody);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// If a stream was configured, it needs to be copied to
|
|
// the output (this method closes the stream)
|
|
if (renderResult != null) {
|
|
copy(renderResult, ostream);
|
|
}
|
|
}
|
|
}
|
|
|
|
} else {
|
|
|
|
if ((ranges == null) || (ranges.isEmpty()))
|
|
return;
|
|
|
|
// Partial content response.
|
|
|
|
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
|
|
|
if (ranges.size() == 1) {
|
|
|
|
Range range = ranges.get(0);
|
|
response.addHeader("Content-Range", "bytes "
|
|
+ range.start
|
|
+ "-" + range.end + "/"
|
|
+ range.length);
|
|
long length = range.end - range.start + 1;
|
|
response.setContentLengthLong(length);
|
|
|
|
if (contentType != null) {
|
|
if (debug > 0)
|
|
log("DefaultServlet.serveFile: contentType='" +
|
|
contentType + "'");
|
|
response.setContentType(contentType);
|
|
}
|
|
|
|
if (serveContent) {
|
|
try {
|
|
response.setBufferSize(output);
|
|
} catch (IllegalStateException e) {
|
|
// Silent catch
|
|
}
|
|
if (ostream != null) {
|
|
if (!checkSendfile(request, response, resource,
|
|
range.end - range.start + 1, range))
|
|
copy(resource, ostream, range);
|
|
} else {
|
|
// we should not get here
|
|
throw new IllegalStateException();
|
|
}
|
|
}
|
|
} else {
|
|
response.setContentType("multipart/byteranges; boundary="
|
|
+ mimeSeparation);
|
|
if (serveContent) {
|
|
try {
|
|
response.setBufferSize(output);
|
|
} catch (IllegalStateException e) {
|
|
// Silent catch
|
|
}
|
|
if (ostream != null) {
|
|
copy(resource, ostream, ranges.iterator(), contentType);
|
|
} else {
|
|
// we should not get here
|
|
throw new IllegalStateException();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Code borrowed heavily from Jasper's EncodingDetector
|
|
*/
|
|
private static Charset processBom(InputStream is) throws IOException {
|
|
// Java supported character sets do not use BOMs longer than 4 bytes
|
|
byte[] bom = new byte[4];
|
|
is.mark(bom.length);
|
|
|
|
int count = is.read(bom);
|
|
|
|
// BOMs are at least 2 bytes
|
|
if (count < 2) {
|
|
skip(is, 0);
|
|
return null;
|
|
}
|
|
|
|
// Look for two byte BOMs
|
|
int b0 = bom[0] & 0xFF;
|
|
int b1 = bom[1] & 0xFF;
|
|
if (b0 == 0xFE && b1 == 0xFF) {
|
|
skip(is, 2);
|
|
return StandardCharsets.UTF_16BE;
|
|
}
|
|
// Delay the UTF_16LE check if there are more that 2 bytes since it
|
|
// overlaps with UTF-32LE.
|
|
if (count == 2 && b0 == 0xFF && b1 == 0xFE) {
|
|
skip(is, 2);
|
|
return StandardCharsets.UTF_16LE;
|
|
}
|
|
|
|
// Remaining BOMs are at least 3 bytes
|
|
if (count < 3) {
|
|
skip(is, 0);
|
|
return null;
|
|
}
|
|
|
|
// UTF-8 is only 3-byte BOM
|
|
int b2 = bom[2] & 0xFF;
|
|
if (b0 == 0xEF && b1 == 0xBB && b2 == 0xBF) {
|
|
skip(is, 3);
|
|
return StandardCharsets.UTF_8;
|
|
}
|
|
|
|
if (count < 4) {
|
|
skip(is, 0);
|
|
return null;
|
|
}
|
|
|
|
// Look for 4-byte BOMs
|
|
int b3 = bom[3] & 0xFF;
|
|
if (b0 == 0x00 && b1 == 0x00 && b2 == 0xFE && b3 == 0xFF) {
|
|
return Charset.forName("UTF-32BE");
|
|
}
|
|
if (b0 == 0xFF && b1 == 0xFE && b2 == 0x00 && b3 == 0x00) {
|
|
return Charset.forName("UTF-32LE");
|
|
}
|
|
|
|
// Now we can check for UTF16-LE. There is an assumption here that we
|
|
// won't see a UTF16-LE file with a BOM where the first real data is
|
|
// 0x00 0x00
|
|
if (b0 == 0xFF && b1 == 0xFE) {
|
|
skip(is, 2);
|
|
return StandardCharsets.UTF_16LE;
|
|
}
|
|
|
|
skip(is, 0);
|
|
return null;
|
|
}
|
|
|
|
|
|
private static void skip(InputStream is, int skip) throws IOException {
|
|
is.reset();
|
|
while (skip-- > 0) {
|
|
is.read();
|
|
}
|
|
}
|
|
|
|
|
|
private static boolean isText(String contentType) {
|
|
return contentType == null || contentType.startsWith("text") ||
|
|
contentType.endsWith("xml") || contentType.contains("/javascript");
|
|
}
|
|
|
|
|
|
private boolean pathEndsWithCompressedExtension(String path) {
|
|
for (CompressionFormat format : compressionFormats) {
|
|
if (path.endsWith(format.extension)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private List<PrecompressedResource> getAvailablePrecompressedResources(String path) {
|
|
List<PrecompressedResource> ret = new ArrayList<>(compressionFormats.length);
|
|
for (CompressionFormat format : compressionFormats) {
|
|
WebResource precompressedResource = resources.getResource(path + format.extension);
|
|
if (precompressedResource.exists() && precompressedResource.isFile()) {
|
|
ret.add(new PrecompressedResource(precompressedResource, format));
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Match the client preferred encoding formats to the available precompressed resources.
|
|
*
|
|
* @param request The servlet request we are processing
|
|
* @param precompressedResources List of available precompressed resources.
|
|
* @return The best matching precompressed resource or null if no match was found.
|
|
*/
|
|
private PrecompressedResource getBestPrecompressedResource(HttpServletRequest request,
|
|
List<PrecompressedResource> precompressedResources) {
|
|
Enumeration<String> headers = request.getHeaders("Accept-Encoding");
|
|
PrecompressedResource bestResource = null;
|
|
double bestResourceQuality = 0;
|
|
int bestResourcePreference = Integer.MAX_VALUE;
|
|
while (headers.hasMoreElements()) {
|
|
String header = headers.nextElement();
|
|
for (String preference : header.split(",")) {
|
|
double quality = 1;
|
|
int qualityIdx = preference.indexOf(';');
|
|
if (qualityIdx > 0) {
|
|
int equalsIdx = preference.indexOf('=', qualityIdx + 1);
|
|
if (equalsIdx == -1) {
|
|
continue;
|
|
}
|
|
quality = Double.parseDouble(preference.substring(equalsIdx + 1).trim());
|
|
}
|
|
if (quality >= bestResourceQuality) {
|
|
String encoding = preference;
|
|
if (qualityIdx > 0) {
|
|
encoding = encoding.substring(0, qualityIdx);
|
|
}
|
|
encoding = encoding.trim();
|
|
if ("identity".equals(encoding)) {
|
|
bestResource = null;
|
|
bestResourceQuality = quality;
|
|
bestResourcePreference = Integer.MAX_VALUE;
|
|
continue;
|
|
}
|
|
if ("*".equals(encoding)) {
|
|
bestResource = precompressedResources.get(0);
|
|
bestResourceQuality = quality;
|
|
bestResourcePreference = 0;
|
|
continue;
|
|
}
|
|
for (int i = 0; i < precompressedResources.size(); ++i) {
|
|
PrecompressedResource resource = precompressedResources.get(i);
|
|
if (encoding.equals(resource.format.encoding)) {
|
|
if (quality > bestResourceQuality || i < bestResourcePreference) {
|
|
bestResource = resource;
|
|
bestResourceQuality = quality;
|
|
bestResourcePreference = i;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return bestResource;
|
|
}
|
|
|
|
private void doDirectoryRedirect(HttpServletRequest request, HttpServletResponse response)
|
|
throws IOException {
|
|
StringBuilder location = new StringBuilder(request.getRequestURI());
|
|
location.append('/');
|
|
if (request.getQueryString() != null) {
|
|
location.append('?');
|
|
location.append(request.getQueryString());
|
|
}
|
|
// Avoid protocol relative redirects
|
|
while (location.length() > 1 && location.charAt(1) == '/') {
|
|
location.deleteCharAt(0);
|
|
}
|
|
response.sendRedirect(response.encodeRedirectURL(location.toString()));
|
|
}
|
|
|
|
/**
|
|
* Parse the content-range header.
|
|
*
|
|
* @param request The servlet request we are processing
|
|
* @param response The servlet response we are creating
|
|
* @return the partial content-range, {@code null} if the content-range
|
|
* header was invalid or {@code #IGNORE} if there is no header to
|
|
* process
|
|
* @throws IOException an IO error occurred
|
|
*/
|
|
protected Range parseContentRange(HttpServletRequest request,
|
|
HttpServletResponse response)
|
|
throws IOException {
|
|
|
|
// Retrieving the content-range header (if any is specified
|
|
String contentRangeHeader = request.getHeader("Content-Range");
|
|
|
|
if (contentRangeHeader == null) {
|
|
return IGNORE;
|
|
}
|
|
|
|
if (!allowPartialPut) {
|
|
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
|
|
return null;
|
|
}
|
|
|
|
ContentRange contentRange = ContentRange.parse(new StringReader(contentRangeHeader));
|
|
|
|
if (contentRange == null) {
|
|
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
|
|
return null;
|
|
}
|
|
|
|
|
|
// bytes is the only range unit supported
|
|
if (!contentRange.getUnits().equals("bytes")) {
|
|
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
|
|
return null;
|
|
}
|
|
|
|
// TODO: Remove the internal representation and use Ranges
|
|
// Convert to internal representation
|
|
Range range = new Range();
|
|
range.start = contentRange.getStart();
|
|
range.end = contentRange.getEnd();
|
|
range.length = contentRange.getLength();
|
|
|
|
if (!range.validate()) {
|
|
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
|
|
return null;
|
|
}
|
|
|
|
return range;
|
|
}
|
|
|
|
|
|
/**
|
|
* Parse the range header.
|
|
*
|
|
* @param request The servlet request we are processing
|
|
* @param response The servlet response we are creating
|
|
* @param resource The resource
|
|
* @return a list of ranges, {@code null} if the range header was invalid or
|
|
* {@code #FULL} if the Range header should be ignored.
|
|
* @throws IOException an IO error occurred
|
|
*/
|
|
protected ArrayList<Range> parseRange(HttpServletRequest request,
|
|
HttpServletResponse response,
|
|
WebResource resource) throws IOException {
|
|
|
|
// Range headers are only valid on GET requests. That implies they are
|
|
// also valid on HEAD requests. This method is only called by doGet()
|
|
// and doHead() so no need to check the request method.
|
|
|
|
// Checking If-Range
|
|
String headerValue = request.getHeader("If-Range");
|
|
|
|
if (headerValue != null) {
|
|
|
|
long headerValueTime = (-1L);
|
|
try {
|
|
headerValueTime = request.getDateHeader("If-Range");
|
|
} catch (IllegalArgumentException e) {
|
|
// Ignore
|
|
}
|
|
|
|
String eTag = resource.getETag();
|
|
long lastModified = resource.getLastModified();
|
|
|
|
if (headerValueTime == (-1L)) {
|
|
// If the ETag the client gave does not match the entity
|
|
// etag, then the entire entity is returned.
|
|
if (!eTag.equals(headerValue.trim())) {
|
|
return FULL;
|
|
}
|
|
} else {
|
|
// If the timestamp of the entity the client got differs from
|
|
// the last modification date of the entity, the entire entity
|
|
// is returned.
|
|
if (Math.abs(lastModified -headerValueTime) > 1000) {
|
|
return FULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
long fileLength = resource.getContentLength();
|
|
|
|
if (fileLength == 0) {
|
|
// Range header makes no sense for a zero length resource. Tomcat
|
|
// therefore opts to ignore it.
|
|
return FULL;
|
|
}
|
|
|
|
// Retrieving the range header (if any is specified
|
|
String rangeHeader = request.getHeader("Range");
|
|
|
|
if (rangeHeader == null) {
|
|
// No Range header is the same as ignoring any Range header
|
|
return FULL;
|
|
}
|
|
|
|
Ranges ranges = Ranges.parse(new StringReader(rangeHeader));
|
|
|
|
if (ranges == null) {
|
|
// The Range header is present but not formatted correctly.
|
|
// Could argue for a 400 response but 416 is more specific.
|
|
// There is also the option to ignore the (invalid) Range header.
|
|
// RFC7233#4.4 notes that many servers do ignore the Range header in
|
|
// these circumstances but Tomcat has always returned a 416.
|
|
response.addHeader("Content-Range", "bytes */" + fileLength);
|
|
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
|
return null;
|
|
}
|
|
|
|
// bytes is the only range unit supported (and I don't see the point
|
|
// of adding new ones).
|
|
if (!ranges.getUnits().equals("bytes")) {
|
|
// RFC7233#3.1 Servers must ignore range units they don't understand
|
|
return FULL;
|
|
}
|
|
|
|
// TODO: Remove the internal representation and use Ranges
|
|
// Convert to internal representation
|
|
ArrayList<Range> result = new ArrayList<>();
|
|
|
|
for (Ranges.Entry entry : ranges.getEntries()) {
|
|
Range currentRange = new Range();
|
|
if (entry.getStart() == -1) {
|
|
currentRange.start = fileLength - entry.getEnd();
|
|
if (currentRange.start < 0) {
|
|
currentRange.start = 0;
|
|
}
|
|
currentRange.end = fileLength - 1;
|
|
} else if (entry.getEnd() == -1) {
|
|
currentRange.start = entry.getStart();
|
|
currentRange.end = fileLength - 1;
|
|
} else {
|
|
currentRange.start = entry.getStart();
|
|
currentRange.end = entry.getEnd();
|
|
}
|
|
currentRange.length = fileLength;
|
|
|
|
if (!currentRange.validate()) {
|
|
response.addHeader("Content-Range", "bytes */" + fileLength);
|
|
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
|
return null;
|
|
}
|
|
|
|
result.add(currentRange);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Decide which way to render. HTML or XML.
|
|
*
|
|
* @param contextPath The path
|
|
* @param resource The resource
|
|
*
|
|
* @return the input stream with the rendered output
|
|
*
|
|
* @throws IOException an IO error occurred
|
|
* @throws ServletException rendering error
|
|
*
|
|
* @deprecated Unused. Will be removed in Tomcat 9
|
|
*/
|
|
@Deprecated
|
|
protected InputStream render(String contextPath, WebResource resource)
|
|
throws IOException, ServletException {
|
|
return render(contextPath, resource, null);
|
|
}
|
|
|
|
/**
|
|
* Decide which way to render. HTML or XML.
|
|
*
|
|
* @param contextPath The path
|
|
* @param resource The resource
|
|
* @param encoding The encoding to use to process the readme (if any)
|
|
*
|
|
* @return the input stream with the rendered output
|
|
*
|
|
* @throws IOException an IO error occurred
|
|
* @throws ServletException rendering error
|
|
*
|
|
* @deprecated Use {@link #render(HttpServletRequest, String, WebResource, String)} instead
|
|
*/
|
|
@Deprecated
|
|
protected InputStream render(String contextPath, WebResource resource, String encoding)
|
|
throws IOException, ServletException {
|
|
|
|
return render(null, contextPath, resource, encoding);
|
|
}
|
|
|
|
/**
|
|
* Decide which way to render. HTML or XML.
|
|
*
|
|
* @param request The HttpServletRequest being served
|
|
* @param contextPath The path
|
|
* @param resource The resource
|
|
* @param encoding The encoding to use to process the readme (if any)
|
|
*
|
|
* @return the input stream with the rendered output
|
|
*
|
|
* @throws IOException an IO error occurred
|
|
* @throws ServletException rendering error
|
|
*/
|
|
protected InputStream render(HttpServletRequest request, String contextPath, WebResource resource, String encoding)
|
|
throws IOException, ServletException {
|
|
|
|
Source xsltSource = findXsltSource(resource);
|
|
|
|
if (xsltSource == null) {
|
|
return renderHtml(request, contextPath, resource, encoding);
|
|
}
|
|
return renderXml(request, contextPath, resource, xsltSource, encoding);
|
|
}
|
|
|
|
|
|
/**
|
|
* Return an InputStream to an XML representation of the contents this
|
|
* directory.
|
|
*
|
|
* @param contextPath Context path to which our internal paths are relative
|
|
* @param resource The associated resource
|
|
* @param xsltSource The XSL stylesheet
|
|
*
|
|
* @return the XML data
|
|
*
|
|
* @throws IOException an IO error occurred
|
|
* @throws ServletException rendering error
|
|
*
|
|
* @deprecated Unused. Will be removed in Tomcat 9
|
|
* @deprecated Use {@link #render(HttpServletRequest, String, WebResource, String)} instead
|
|
*/
|
|
@Deprecated
|
|
protected InputStream renderXml(String contextPath, WebResource resource, Source xsltSource)
|
|
throws IOException, ServletException {
|
|
return renderXml(contextPath, resource, xsltSource, null);
|
|
}
|
|
|
|
/**
|
|
* Return an InputStream to an XML representation of the contents this
|
|
* directory.
|
|
*
|
|
* @param contextPath Context path to which our internal paths are relative
|
|
* @param resource The associated resource
|
|
* @param xsltSource The XSL stylesheet
|
|
* @param encoding The encoding to use to process the readme (if any)
|
|
*
|
|
* @return the XML data
|
|
*
|
|
* @throws IOException an IO error occurred
|
|
* @throws ServletException rendering error
|
|
* @deprecated Unused. Will be removed in Tomcat 10
|
|
* @deprecated Use {@link #render(HttpServletRequest, String, WebResource, String)} instead
|
|
*/
|
|
@Deprecated
|
|
protected InputStream renderXml(String contextPath, WebResource resource, Source xsltSource,
|
|
String encoding)
|
|
throws ServletException, IOException
|
|
{
|
|
return renderXml(null, contextPath, resource, xsltSource, encoding);
|
|
}
|
|
|
|
/**
|
|
* Return an InputStream to an XML representation of the contents this
|
|
* directory.
|
|
*
|
|
* @param request The HttpServletRequest being served
|
|
* @param contextPath Context path to which our internal paths are relative
|
|
* @param resource The associated resource
|
|
* @param xsltSource The XSL stylesheet
|
|
* @param encoding The encoding to use to process the readme (if any)
|
|
*
|
|
* @return the XML data
|
|
*
|
|
* @throws IOException an IO error occurred
|
|
* @throws ServletException rendering error
|
|
*/
|
|
protected InputStream renderXml(HttpServletRequest request, String contextPath, WebResource resource, Source xsltSource,
|
|
String encoding)
|
|
throws IOException, ServletException {
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
sb.append("<?xml version=\"1.0\"?>");
|
|
sb.append("<listing ");
|
|
sb.append(" contextPath='");
|
|
sb.append(contextPath);
|
|
sb.append("'");
|
|
sb.append(" directory='");
|
|
sb.append(resource.getName());
|
|
sb.append("' ");
|
|
sb.append(" hasParent='").append(!resource.getName().equals("/"));
|
|
sb.append("'>");
|
|
|
|
sb.append("<entries>");
|
|
|
|
String[] entries = resources.list(resource.getWebappPath());
|
|
|
|
// rewriteUrl(contextPath) is expensive. cache result for later reuse
|
|
String rewrittenContextPath = rewriteUrl(contextPath);
|
|
String directoryWebappPath = resource.getWebappPath();
|
|
|
|
for (String entry : entries) {
|
|
|
|
if (entry.equalsIgnoreCase("WEB-INF") ||
|
|
entry.equalsIgnoreCase("META-INF") ||
|
|
entry.equalsIgnoreCase(localXsltFile))
|
|
continue;
|
|
|
|
if ((directoryWebappPath + entry).equals(contextXsltFile))
|
|
continue;
|
|
|
|
WebResource childResource =
|
|
resources.getResource(directoryWebappPath + entry);
|
|
if (!childResource.exists()) {
|
|
continue;
|
|
}
|
|
|
|
sb.append("<entry");
|
|
sb.append(" type='")
|
|
.append(childResource.isDirectory()?"dir":"file")
|
|
.append("'");
|
|
sb.append(" urlPath='")
|
|
.append(rewrittenContextPath)
|
|
.append(rewriteUrl(directoryWebappPath + entry))
|
|
.append(childResource.isDirectory()?"/":"")
|
|
.append("'");
|
|
if (childResource.isFile()) {
|
|
sb.append(" size='")
|
|
.append(renderSize(childResource.getContentLength()))
|
|
.append("'");
|
|
}
|
|
sb.append(" date='")
|
|
.append(childResource.getLastModifiedHttp())
|
|
.append("'");
|
|
|
|
sb.append(">");
|
|
sb.append(Escape.htmlElementContent(entry));
|
|
if (childResource.isDirectory())
|
|
sb.append("/");
|
|
sb.append("</entry>");
|
|
}
|
|
sb.append("</entries>");
|
|
|
|
String readme = getReadme(resource, encoding);
|
|
|
|
if (readme!=null) {
|
|
sb.append("<readme><![CDATA[");
|
|
sb.append(readme);
|
|
sb.append("]]></readme>");
|
|
}
|
|
|
|
sb.append("</listing>");
|
|
|
|
// Prevent possible memory leak. Ensure Transformer and
|
|
// TransformerFactory are not loaded from the web application.
|
|
ClassLoader original;
|
|
if (Globals.IS_SECURITY_ENABLED) {
|
|
PrivilegedGetTccl pa = new PrivilegedGetTccl();
|
|
original = AccessController.doPrivileged(pa);
|
|
} else {
|
|
original = Thread.currentThread().getContextClassLoader();
|
|
}
|
|
try {
|
|
if (Globals.IS_SECURITY_ENABLED) {
|
|
PrivilegedSetTccl pa =
|
|
new PrivilegedSetTccl(DefaultServlet.class.getClassLoader());
|
|
AccessController.doPrivileged(pa);
|
|
} else {
|
|
Thread.currentThread().setContextClassLoader(
|
|
DefaultServlet.class.getClassLoader());
|
|
}
|
|
|
|
TransformerFactory tFactory = TransformerFactory.newInstance();
|
|
Source xmlSource = new StreamSource(new StringReader(sb.toString()));
|
|
Transformer transformer = tFactory.newTransformer(xsltSource);
|
|
|
|
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
|
OutputStreamWriter osWriter = new OutputStreamWriter(stream, StandardCharsets.UTF_8);
|
|
StreamResult out = new StreamResult(osWriter);
|
|
transformer.transform(xmlSource, out);
|
|
osWriter.flush();
|
|
return new ByteArrayInputStream(stream.toByteArray());
|
|
} catch (TransformerException e) {
|
|
throw new ServletException(sm.getString("defaultServlet.xslError"), e);
|
|
} finally {
|
|
if (Globals.IS_SECURITY_ENABLED) {
|
|
PrivilegedSetTccl pa = new PrivilegedSetTccl(original);
|
|
AccessController.doPrivileged(pa);
|
|
} else {
|
|
Thread.currentThread().setContextClassLoader(original);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return an InputStream to an HTML representation of the contents of this
|
|
* directory.
|
|
*
|
|
* @param contextPath Context path to which our internal paths are relative
|
|
* @param resource The associated resource
|
|
*
|
|
* @return the HTML data
|
|
*
|
|
* @throws IOException an IO error occurred
|
|
*
|
|
* @deprecated Unused. Will be removed in Tomcat 9
|
|
*/
|
|
@Deprecated
|
|
protected InputStream renderHtml(String contextPath, WebResource resource)
|
|
throws IOException {
|
|
return renderHtml(contextPath, resource, null);
|
|
}
|
|
|
|
/**
|
|
* Return an InputStream to an HTML representation of the contents of this
|
|
* directory.
|
|
*
|
|
* @param contextPath Context path to which our internal paths are relative
|
|
* @param resource The associated resource
|
|
* @param encoding The encoding to use to process the readme (if any)
|
|
*
|
|
* @return the HTML data
|
|
*
|
|
* @throws IOException an IO error occurred
|
|
*
|
|
* @deprecated Unused. Will be removed in Tomcat 10
|
|
* @deprecated Use {@link #renderHtml(HttpServletRequest, String, WebResource, String)} instead
|
|
*/
|
|
@Deprecated
|
|
protected InputStream renderHtml(String contextPath, WebResource resource, String encoding)
|
|
throws IOException {
|
|
return renderHtml(null, contextPath, resource, encoding);
|
|
}
|
|
|
|
/**
|
|
* Return an InputStream to an HTML representation of the contents of this
|
|
* directory.
|
|
*
|
|
* @param request The HttpServletRequest being served
|
|
* @param contextPath Context path to which our internal paths are relative
|
|
* @param resource The associated resource
|
|
* @param encoding The encoding to use to process the readme (if any)
|
|
*
|
|
* @return the HTML data
|
|
*
|
|
* @throws IOException an IO error occurred
|
|
*/
|
|
protected InputStream renderHtml(HttpServletRequest request, String contextPath, WebResource resource, String encoding)
|
|
throws IOException {
|
|
|
|
// Prepare a writer to a buffered area
|
|
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
|
OutputStreamWriter osWriter = new OutputStreamWriter(stream, StandardCharsets.UTF_8);
|
|
PrintWriter writer = new PrintWriter(osWriter);
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
String directoryWebappPath = resource.getWebappPath();
|
|
WebResource[] entries = resources.listResources(directoryWebappPath);
|
|
|
|
// rewriteUrl(contextPath) is expensive. cache result for later reuse
|
|
String rewrittenContextPath = rewriteUrl(contextPath);
|
|
|
|
// Render the page header
|
|
sb.append("<!doctype html><html>\r\n");
|
|
/* TODO Activate this as soon as we use smClient with the request locales
|
|
sb.append("<!doctype html><html lang=\"");
|
|
sb.append(smClient.getLocale().getLanguage()).append("\">\r\n");
|
|
*/
|
|
sb.append("<head>\r\n");
|
|
sb.append("<title>");
|
|
sb.append(sm.getString("directory.title", directoryWebappPath));
|
|
sb.append("</title>\r\n");
|
|
sb.append("<style>");
|
|
sb.append(org.apache.catalina.util.TomcatCSS.TOMCAT_CSS);
|
|
sb.append("</style> ");
|
|
sb.append("</head>\r\n");
|
|
sb.append("<body>");
|
|
sb.append("<h1>");
|
|
sb.append(sm.getString("directory.title", directoryWebappPath));
|
|
|
|
// Render the link to our parent (if required)
|
|
String parentDirectory = directoryWebappPath;
|
|
if (parentDirectory.endsWith("/")) {
|
|
parentDirectory =
|
|
parentDirectory.substring(0, parentDirectory.length() - 1);
|
|
}
|
|
int slash = parentDirectory.lastIndexOf('/');
|
|
if (slash >= 0) {
|
|
String parent = directoryWebappPath.substring(0, slash);
|
|
sb.append(" - <a href=\"");
|
|
sb.append(rewrittenContextPath);
|
|
if (parent.equals(""))
|
|
parent = "/";
|
|
sb.append(rewriteUrl(parent));
|
|
if (!parent.endsWith("/"))
|
|
sb.append("/");
|
|
sb.append("\">");
|
|
sb.append("<b>");
|
|
sb.append(sm.getString("directory.parent", parent));
|
|
sb.append("</b>");
|
|
sb.append("</a>");
|
|
}
|
|
|
|
sb.append("</h1>");
|
|
sb.append("<hr class=\"line\">");
|
|
|
|
sb.append("<table width=\"100%\" cellspacing=\"0\"" +
|
|
" cellpadding=\"5\" align=\"center\">\r\n");
|
|
|
|
SortManager.Order order;
|
|
if(sortListings && null != request)
|
|
order = sortManager.getOrder(request.getQueryString());
|
|
else
|
|
order = null;
|
|
// Render the column headings
|
|
sb.append("<tr>\r\n");
|
|
sb.append("<td align=\"left\"><font size=\"+1\"><strong>");
|
|
if(sortListings && null != request) {
|
|
sb.append("<a href=\"?C=N;O=");
|
|
sb.append(getOrderChar(order, 'N'));
|
|
sb.append("\">");
|
|
sb.append(sm.getString("directory.filename"));
|
|
sb.append("</a>");
|
|
} else {
|
|
sb.append(sm.getString("directory.filename"));
|
|
}
|
|
sb.append("</strong></font></td>\r\n");
|
|
sb.append("<td align=\"center\"><font size=\"+1\"><strong>");
|
|
if(sortListings && null != request) {
|
|
sb.append("<a href=\"?C=S;O=");
|
|
sb.append(getOrderChar(order, 'S'));
|
|
sb.append("\">");
|
|
sb.append(sm.getString("directory.size"));
|
|
sb.append("</a>");
|
|
} else {
|
|
sb.append(sm.getString("directory.size"));
|
|
}
|
|
sb.append("</strong></font></td>\r\n");
|
|
sb.append("<td align=\"right\"><font size=\"+1\"><strong>");
|
|
if(sortListings && null != request) {
|
|
sb.append("<a href=\"?C=M;O=");
|
|
sb.append(getOrderChar(order, 'M'));
|
|
sb.append("\">");
|
|
sb.append(sm.getString("directory.lastModified"));
|
|
sb.append("</a>");
|
|
} else {
|
|
sb.append(sm.getString("directory.lastModified"));
|
|
}
|
|
sb.append("</strong></font></td>\r\n");
|
|
sb.append("</tr>");
|
|
|
|
if(null != sortManager && null != request) {
|
|
sortManager.sort(entries, request.getQueryString());
|
|
}
|
|
|
|
boolean shade = false;
|
|
for (WebResource childResource : entries) {
|
|
String filename = childResource.getName();
|
|
if (filename.equalsIgnoreCase("WEB-INF") ||
|
|
filename.equalsIgnoreCase("META-INF"))
|
|
continue;
|
|
|
|
if (!childResource.exists()) {
|
|
continue;
|
|
}
|
|
|
|
sb.append("<tr");
|
|
if (shade)
|
|
sb.append(" bgcolor=\"#eeeeee\"");
|
|
sb.append(">\r\n");
|
|
shade = !shade;
|
|
|
|
sb.append("<td align=\"left\"> \r\n");
|
|
sb.append("<a href=\"");
|
|
sb.append(rewrittenContextPath);
|
|
sb.append(rewriteUrl(childResource.getWebappPath()));
|
|
if (childResource.isDirectory())
|
|
sb.append("/");
|
|
sb.append("\"><tt>");
|
|
sb.append(Escape.htmlElementContent(filename));
|
|
if (childResource.isDirectory())
|
|
sb.append("/");
|
|
sb.append("</tt></a></td>\r\n");
|
|
|
|
sb.append("<td align=\"right\"><tt>");
|
|
if (childResource.isDirectory())
|
|
sb.append(" ");
|
|
else
|
|
sb.append(renderSize(childResource.getContentLength()));
|
|
sb.append("</tt></td>\r\n");
|
|
|
|
sb.append("<td align=\"right\"><tt>");
|
|
sb.append(childResource.getLastModifiedHttp());
|
|
sb.append("</tt></td>\r\n");
|
|
|
|
sb.append("</tr>\r\n");
|
|
}
|
|
|
|
// Render the page footer
|
|
sb.append("</table>\r\n");
|
|
|
|
sb.append("<hr class=\"line\">");
|
|
|
|
String readme = getReadme(resource, encoding);
|
|
if (readme!=null) {
|
|
sb.append(readme);
|
|
sb.append("<hr class=\"line\">");
|
|
}
|
|
|
|
if (showServerInfo) {
|
|
sb.append("<h3>").append(ServerInfo.getServerInfo()).append("</h3>");
|
|
}
|
|
sb.append("</body>\r\n");
|
|
sb.append("</html>\r\n");
|
|
|
|
// Return an input stream to the underlying bytes
|
|
writer.write(sb.toString());
|
|
writer.flush();
|
|
return new ByteArrayInputStream(stream.toByteArray());
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Render the specified file size (in bytes).
|
|
*
|
|
* @param size File size (in bytes)
|
|
* @return the formatted size
|
|
*/
|
|
protected String renderSize(long size) {
|
|
|
|
long leftSide = size / 1024;
|
|
long rightSide = (size % 1024) / 103; // Makes 1 digit
|
|
if ((leftSide == 0) && (rightSide == 0) && (size > 0))
|
|
rightSide = 1;
|
|
|
|
return ("" + leftSide + "." + rightSide + " kb");
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the readme file as a string.
|
|
* @param directory The directory to search
|
|
* @return the readme for the specified directory
|
|
*
|
|
* @deprecated Unused. Will be removed in Tomcat 9
|
|
*/
|
|
@Deprecated
|
|
protected String getReadme(WebResource directory) {
|
|
return getReadme(directory, null);
|
|
}
|
|
|
|
/**
|
|
* Get the readme file as a string.
|
|
* @param directory The directory to search
|
|
* @param encoding The readme encoding
|
|
* @return the readme for the specified directory
|
|
*/
|
|
protected String getReadme(WebResource directory, String encoding) {
|
|
|
|
if (readmeFile != null) {
|
|
WebResource resource = resources.getResource(
|
|
directory.getWebappPath() + readmeFile);
|
|
if (resource.isFile()) {
|
|
StringWriter buffer = new StringWriter();
|
|
InputStreamReader reader = null;
|
|
try (InputStream is = resource.getInputStream()){
|
|
if (encoding != null) {
|
|
reader = new InputStreamReader(is, encoding);
|
|
} else {
|
|
reader = new InputStreamReader(is);
|
|
}
|
|
copyRange(reader, new PrintWriter(buffer));
|
|
} catch (IOException e) {
|
|
log(sm.getString("defaultServlet.readerCloseFailed"), e);
|
|
} finally {
|
|
if (reader != null) {
|
|
try {
|
|
reader.close();
|
|
} catch (IOException e) {
|
|
}
|
|
}
|
|
}
|
|
return buffer.toString();
|
|
} else {
|
|
if (debug > 10)
|
|
log("readme '" + readmeFile + "' not found");
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Return a Source for the xsl template (if possible).
|
|
* @param directory The directory to search
|
|
* @return the source for the specified directory
|
|
* @throws IOException an IO error occurred
|
|
*/
|
|
protected Source findXsltSource(WebResource directory)
|
|
throws IOException {
|
|
|
|
if (localXsltFile != null) {
|
|
WebResource resource = resources.getResource(
|
|
directory.getWebappPath() + localXsltFile);
|
|
if (resource.isFile()) {
|
|
InputStream is = resource.getInputStream();
|
|
if (is != null) {
|
|
if (Globals.IS_SECURITY_ENABLED) {
|
|
return secureXslt(is);
|
|
} else {
|
|
return new StreamSource(is);
|
|
}
|
|
}
|
|
}
|
|
if (debug > 10) {
|
|
log("localXsltFile '" + localXsltFile + "' not found");
|
|
}
|
|
}
|
|
|
|
if (contextXsltFile != null) {
|
|
InputStream is =
|
|
getServletContext().getResourceAsStream(contextXsltFile);
|
|
if (is != null) {
|
|
if (Globals.IS_SECURITY_ENABLED) {
|
|
return secureXslt(is);
|
|
} else {
|
|
return new StreamSource(is);
|
|
}
|
|
}
|
|
|
|
if (debug > 10)
|
|
log("contextXsltFile '" + contextXsltFile + "' not found");
|
|
}
|
|
|
|
/* Open and read in file in one fell swoop to reduce chance
|
|
* chance of leaving handle open.
|
|
*/
|
|
if (globalXsltFile != null) {
|
|
File f = validateGlobalXsltFile();
|
|
if (f != null) {
|
|
long globalXsltFileSize = f.length();
|
|
if (globalXsltFileSize > Integer.MAX_VALUE) {
|
|
log("globalXsltFile [" + f.getAbsolutePath() + "] is too big to buffer");
|
|
} else {
|
|
try (FileInputStream fis = new FileInputStream(f)){
|
|
byte b[] = new byte[(int)f.length()];
|
|
IOTools.readFully(fis, b);
|
|
return new StreamSource(new ByteArrayInputStream(b));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
private File validateGlobalXsltFile() {
|
|
Context context = resources.getContext();
|
|
|
|
File baseConf = new File(context.getCatalinaBase(), "conf");
|
|
File result = validateGlobalXsltFile(baseConf);
|
|
if (result == null) {
|
|
File homeConf = new File(context.getCatalinaHome(), "conf");
|
|
if (!baseConf.equals(homeConf)) {
|
|
result = validateGlobalXsltFile(homeConf);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
private File validateGlobalXsltFile(File base) {
|
|
File candidate = new File(globalXsltFile);
|
|
if (!candidate.isAbsolute()) {
|
|
candidate = new File(base, globalXsltFile);
|
|
}
|
|
|
|
if (!candidate.isFile()) {
|
|
return null;
|
|
}
|
|
|
|
// First check that the resulting path is under the provided base
|
|
try {
|
|
if (!candidate.getCanonicalPath().startsWith(base.getCanonicalPath())) {
|
|
return null;
|
|
}
|
|
} catch (IOException ioe) {
|
|
return null;
|
|
}
|
|
|
|
// Next check that an .xsl or .xslt file has been specified
|
|
String nameLower = candidate.getName().toLowerCase(Locale.ENGLISH);
|
|
if (!nameLower.endsWith(".xslt") && !nameLower.endsWith(".xsl")) {
|
|
return null;
|
|
}
|
|
|
|
return candidate;
|
|
}
|
|
|
|
|
|
private Source secureXslt(InputStream is) {
|
|
// Need to filter out any external entities
|
|
Source result = null;
|
|
try {
|
|
DocumentBuilder builder = factory.newDocumentBuilder();
|
|
builder.setEntityResolver(secureEntityResolver);
|
|
Document document = builder.parse(is);
|
|
result = new DOMSource(document);
|
|
} catch (ParserConfigurationException | SAXException | IOException e) {
|
|
if (debug > 0) {
|
|
log(e.getMessage(), e);
|
|
}
|
|
} finally {
|
|
if (is != null) {
|
|
try {
|
|
is.close();
|
|
} catch (IOException e) {
|
|
// Ignore
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
// -------------------------------------------------------- protected Methods
|
|
|
|
/**
|
|
* Check if sendfile can be used.
|
|
* @param request The Servlet request
|
|
* @param response The Servlet response
|
|
* @param resource The resource
|
|
* @param length The length which will be written (will be used only if
|
|
* range is null)
|
|
* @param range The range that will be written
|
|
* @return <code>true</code> if sendfile should be used (writing is then
|
|
* delegated to the endpoint)
|
|
*/
|
|
protected boolean checkSendfile(HttpServletRequest request,
|
|
HttpServletResponse response,
|
|
WebResource resource,
|
|
long length, Range range) {
|
|
String canonicalPath;
|
|
if (sendfileSize > 0
|
|
&& length > sendfileSize
|
|
&& (Boolean.TRUE.equals(request.getAttribute(Globals.SENDFILE_SUPPORTED_ATTR)))
|
|
&& (request.getClass().getName().equals("org.apache.catalina.connector.RequestFacade"))
|
|
&& (response.getClass().getName().equals("org.apache.catalina.connector.ResponseFacade"))
|
|
&& resource.isFile()
|
|
&& ((canonicalPath = resource.getCanonicalPath()) != null)
|
|
) {
|
|
request.setAttribute(Globals.SENDFILE_FILENAME_ATTR, canonicalPath);
|
|
if (range == null) {
|
|
request.setAttribute(Globals.SENDFILE_FILE_START_ATTR, Long.valueOf(0L));
|
|
request.setAttribute(Globals.SENDFILE_FILE_END_ATTR, Long.valueOf(length));
|
|
} else {
|
|
request.setAttribute(Globals.SENDFILE_FILE_START_ATTR, Long.valueOf(range.start));
|
|
request.setAttribute(Globals.SENDFILE_FILE_END_ATTR, Long.valueOf(range.end + 1));
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Check if the if-match condition is satisfied.
|
|
*
|
|
* @param request The servlet request we are processing
|
|
* @param response The servlet response we are creating
|
|
* @param resource The resource
|
|
* @return <code>true</code> if the resource meets the specified condition,
|
|
* and <code>false</code> if the condition is not satisfied, in which case
|
|
* request processing is stopped
|
|
* @throws IOException an IO error occurred
|
|
*/
|
|
protected boolean checkIfMatch(HttpServletRequest request,
|
|
HttpServletResponse response, WebResource resource)
|
|
throws IOException {
|
|
|
|
String eTag = resource.getETag();
|
|
String headerValue = request.getHeader("If-Match");
|
|
if (headerValue != null) {
|
|
if (headerValue.indexOf('*') == -1) {
|
|
|
|
StringTokenizer commaTokenizer = new StringTokenizer
|
|
(headerValue, ",");
|
|
boolean conditionSatisfied = false;
|
|
|
|
while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
|
|
String currentToken = commaTokenizer.nextToken();
|
|
if (currentToken.trim().equals(eTag))
|
|
conditionSatisfied = true;
|
|
}
|
|
|
|
// If none of the given ETags match, 412 Precondition failed is
|
|
// sent back
|
|
if (!conditionSatisfied) {
|
|
response.sendError
|
|
(HttpServletResponse.SC_PRECONDITION_FAILED);
|
|
return false;
|
|
}
|
|
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Check if the if-modified-since condition is satisfied.
|
|
*
|
|
* @param request The servlet request we are processing
|
|
* @param response The servlet response we are creating
|
|
* @param resource The resource
|
|
* @return <code>true</code> if the resource meets the specified condition,
|
|
* and <code>false</code> if the condition is not satisfied, in which case
|
|
* request processing is stopped
|
|
*/
|
|
protected boolean checkIfModifiedSince(HttpServletRequest request,
|
|
HttpServletResponse response, WebResource resource) {
|
|
try {
|
|
long headerValue = request.getDateHeader("If-Modified-Since");
|
|
long lastModified = resource.getLastModified();
|
|
if (headerValue != -1) {
|
|
|
|
// If an If-None-Match header has been specified, if modified since
|
|
// is ignored.
|
|
if ((request.getHeader("If-None-Match") == null)
|
|
&& (lastModified < headerValue + 1000)) {
|
|
// The entity has not been modified since the date
|
|
// specified by the client. This is not an error case.
|
|
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
|
|
response.setHeader("ETag", resource.getETag());
|
|
|
|
return false;
|
|
}
|
|
}
|
|
} catch (IllegalArgumentException illegalArgument) {
|
|
return true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Check if the if-none-match condition is satisfied.
|
|
*
|
|
* @param request The servlet request we are processing
|
|
* @param response The servlet response we are creating
|
|
* @param resource The resource
|
|
* @return <code>true</code> if the resource meets the specified condition,
|
|
* and <code>false</code> if the condition is not satisfied, in which case
|
|
* request processing is stopped
|
|
* @throws IOException an IO error occurred
|
|
*/
|
|
protected boolean checkIfNoneMatch(HttpServletRequest request,
|
|
HttpServletResponse response, WebResource resource)
|
|
throws IOException {
|
|
|
|
String eTag = resource.getETag();
|
|
String headerValue = request.getHeader("If-None-Match");
|
|
if (headerValue != null) {
|
|
|
|
boolean conditionSatisfied = false;
|
|
|
|
if (!headerValue.equals("*")) {
|
|
|
|
StringTokenizer commaTokenizer =
|
|
new StringTokenizer(headerValue, ",");
|
|
|
|
while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
|
|
String currentToken = commaTokenizer.nextToken();
|
|
if (currentToken.trim().equals(eTag))
|
|
conditionSatisfied = true;
|
|
}
|
|
|
|
} else {
|
|
conditionSatisfied = true;
|
|
}
|
|
|
|
if (conditionSatisfied) {
|
|
|
|
// For GET and HEAD, we should respond with
|
|
// 304 Not Modified.
|
|
// For every other method, 412 Precondition Failed is sent
|
|
// back.
|
|
if ( ("GET".equals(request.getMethod()))
|
|
|| ("HEAD".equals(request.getMethod())) ) {
|
|
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
|
|
response.setHeader("ETag", eTag);
|
|
|
|
return false;
|
|
}
|
|
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check if the if-unmodified-since condition is satisfied.
|
|
*
|
|
* @param request The servlet request we are processing
|
|
* @param response The servlet response we are creating
|
|
* @param resource The resource
|
|
* @return <code>true</code> if the resource meets the specified condition,
|
|
* and <code>false</code> if the condition is not satisfied, in which case
|
|
* request processing is stopped
|
|
* @throws IOException an IO error occurred
|
|
*/
|
|
protected boolean checkIfUnmodifiedSince(HttpServletRequest request,
|
|
HttpServletResponse response, WebResource resource)
|
|
throws IOException {
|
|
try {
|
|
long lastModified = resource.getLastModified();
|
|
long headerValue = request.getDateHeader("If-Unmodified-Since");
|
|
if (headerValue != -1) {
|
|
if ( lastModified >= (headerValue + 1000)) {
|
|
// The entity has not been modified since the date
|
|
// specified by the client. This is not an error case.
|
|
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
|
|
return false;
|
|
}
|
|
}
|
|
} catch(IllegalArgumentException illegalArgument) {
|
|
return true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Copy the contents of the specified input stream to the specified
|
|
* output stream, and ensure that both streams are closed before returning
|
|
* (even in the face of an exception).
|
|
*
|
|
* @param resource The source resource
|
|
* @param is The input stream to read the source resource from
|
|
* @param ostream The output stream to write to
|
|
*
|
|
* @exception IOException if an input/output error occurs
|
|
*
|
|
* @deprecated Unused. This will be removed in Tomcat 9.
|
|
* Use {@link #copy(InputStream, ServletOutputStream)}
|
|
*/
|
|
@Deprecated
|
|
protected void copy(WebResource resource, InputStream is,
|
|
ServletOutputStream ostream)
|
|
throws IOException {
|
|
copy(is, ostream);
|
|
}
|
|
|
|
|
|
/**
|
|
* Copy the contents of the specified input stream to the specified
|
|
* output stream, and ensure that both streams are closed before returning
|
|
* (even in the face of an exception).
|
|
*
|
|
* @param is The input stream to read the source resource from
|
|
* @param ostream The output stream to write to
|
|
*
|
|
* @exception IOException if an input/output error occurs
|
|
*/
|
|
protected void copy(InputStream is, ServletOutputStream ostream) throws IOException {
|
|
|
|
IOException exception = null;
|
|
InputStream istream = new BufferedInputStream(is, input);
|
|
|
|
// Copy the input stream to the output stream
|
|
exception = copyRange(istream, ostream);
|
|
|
|
// Clean up the input stream
|
|
istream.close();
|
|
|
|
// Rethrow any exception that has occurred
|
|
if (exception != null)
|
|
throw exception;
|
|
}
|
|
|
|
|
|
/**
|
|
* Copy the contents of the specified input stream to the specified
|
|
* output stream, and ensure that both streams are closed before returning
|
|
* (even in the face of an exception).
|
|
*
|
|
* @param resource The source resource
|
|
* @param is The input stream to read the source resource from
|
|
* @param writer The writer to write to
|
|
* @param encoding The encoding to use when reading the source input stream
|
|
*
|
|
* @exception IOException if an input/output error occurs
|
|
*
|
|
* @deprecated Unused. This will be removed in Tomcat 9.
|
|
* Use {@link #copy(InputStream, PrintWriter, String)}
|
|
*/
|
|
@Deprecated
|
|
protected void copy(WebResource resource, InputStream is, PrintWriter writer,
|
|
String encoding) throws IOException {
|
|
copy(is, writer,encoding);
|
|
}
|
|
|
|
|
|
/**
|
|
* Copy the contents of the specified input stream to the specified
|
|
* output stream, and ensure that both streams are closed before returning
|
|
* (even in the face of an exception).
|
|
*
|
|
* @param is The input stream to read the source resource from
|
|
* @param writer The writer to write to
|
|
* @param encoding The encoding to use when reading the source input stream
|
|
*
|
|
* @exception IOException if an input/output error occurs
|
|
*/
|
|
protected void copy(InputStream is, PrintWriter writer, String encoding) throws IOException {
|
|
IOException exception = null;
|
|
|
|
Reader reader;
|
|
if (encoding == null) {
|
|
reader = new InputStreamReader(is);
|
|
} else {
|
|
reader = new InputStreamReader(is, encoding);
|
|
}
|
|
|
|
// Copy the input stream to the output stream
|
|
exception = copyRange(reader, writer);
|
|
|
|
// Clean up the reader
|
|
reader.close();
|
|
|
|
// Rethrow any exception that has occurred
|
|
if (exception != null) {
|
|
throw exception;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Copy the contents of the specified input stream to the specified
|
|
* output stream, and ensure that both streams are closed before returning
|
|
* (even in the face of an exception).
|
|
*
|
|
* @param resource The source resource
|
|
* @param ostream The output stream to write to
|
|
* @param range Range the client wanted to retrieve
|
|
* @exception IOException if an input/output error occurs
|
|
*/
|
|
protected void copy(WebResource resource, ServletOutputStream ostream,
|
|
Range range)
|
|
throws IOException {
|
|
|
|
IOException exception = null;
|
|
|
|
InputStream resourceInputStream = resource.getInputStream();
|
|
InputStream istream =
|
|
new BufferedInputStream(resourceInputStream, input);
|
|
exception = copyRange(istream, ostream, range.start, range.end);
|
|
|
|
// Clean up the input stream
|
|
istream.close();
|
|
|
|
// Rethrow any exception that has occurred
|
|
if (exception != null)
|
|
throw exception;
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Copy the contents of the specified input stream to the specified
|
|
* output stream, and ensure that both streams are closed before returning
|
|
* (even in the face of an exception).
|
|
*
|
|
* @param resource The source resource
|
|
* @param ostream The output stream to write to
|
|
* @param ranges Enumeration of the ranges the client wanted to
|
|
* retrieve
|
|
* @param contentType Content type of the resource
|
|
* @exception IOException if an input/output error occurs
|
|
*/
|
|
protected void copy(WebResource resource, ServletOutputStream ostream,
|
|
Iterator<Range> ranges, String contentType)
|
|
throws IOException {
|
|
|
|
IOException exception = null;
|
|
|
|
while ( (exception == null) && (ranges.hasNext()) ) {
|
|
|
|
InputStream resourceInputStream = resource.getInputStream();
|
|
try (InputStream istream = new BufferedInputStream(resourceInputStream, input)) {
|
|
|
|
Range currentRange = ranges.next();
|
|
|
|
// Writing MIME header.
|
|
ostream.println();
|
|
ostream.println("--" + mimeSeparation);
|
|
if (contentType != null)
|
|
ostream.println("Content-Type: " + contentType);
|
|
ostream.println("Content-Range: bytes " + currentRange.start
|
|
+ "-" + currentRange.end + "/"
|
|
+ currentRange.length);
|
|
ostream.println();
|
|
|
|
// Printing content
|
|
exception = copyRange(istream, ostream, currentRange.start,
|
|
currentRange.end);
|
|
}
|
|
}
|
|
|
|
ostream.println();
|
|
ostream.print("--" + mimeSeparation + "--");
|
|
|
|
// Rethrow any exception that has occurred
|
|
if (exception != null)
|
|
throw exception;
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Copy the contents of the specified input stream to the specified
|
|
* output stream, and ensure that both streams are closed before returning
|
|
* (even in the face of an exception).
|
|
*
|
|
* @param istream The input stream to read from
|
|
* @param ostream The output stream to write to
|
|
* @return Exception which occurred during processing
|
|
*/
|
|
protected IOException copyRange(InputStream istream,
|
|
ServletOutputStream ostream) {
|
|
|
|
// Copy the input stream to the output stream
|
|
IOException exception = null;
|
|
byte buffer[] = new byte[input];
|
|
int len = buffer.length;
|
|
while (true) {
|
|
try {
|
|
len = istream.read(buffer);
|
|
if (len == -1)
|
|
break;
|
|
ostream.write(buffer, 0, len);
|
|
} catch (IOException e) {
|
|
exception = e;
|
|
len = -1;
|
|
break;
|
|
}
|
|
}
|
|
return exception;
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Copy the contents of the specified input stream to the specified
|
|
* output stream, and ensure that both streams are closed before returning
|
|
* (even in the face of an exception).
|
|
*
|
|
* @param reader The reader to read from
|
|
* @param writer The writer to write to
|
|
* @return Exception which occurred during processing
|
|
*/
|
|
protected IOException copyRange(Reader reader, PrintWriter writer) {
|
|
|
|
// Copy the input stream to the output stream
|
|
IOException exception = null;
|
|
char buffer[] = new char[input];
|
|
int len = buffer.length;
|
|
while (true) {
|
|
try {
|
|
len = reader.read(buffer);
|
|
if (len == -1)
|
|
break;
|
|
writer.write(buffer, 0, len);
|
|
} catch (IOException e) {
|
|
exception = e;
|
|
len = -1;
|
|
break;
|
|
}
|
|
}
|
|
return exception;
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Copy the contents of the specified input stream to the specified
|
|
* output stream, and ensure that both streams are closed before returning
|
|
* (even in the face of an exception).
|
|
*
|
|
* @param istream The input stream to read from
|
|
* @param ostream The output stream to write to
|
|
* @param start Start of the range which will be copied
|
|
* @param end End of the range which will be copied
|
|
* @return Exception which occurred during processing
|
|
*/
|
|
protected IOException copyRange(InputStream istream,
|
|
ServletOutputStream ostream,
|
|
long start, long end) {
|
|
|
|
if (debug > 10)
|
|
log("Serving bytes:" + start + "-" + end);
|
|
|
|
long skipped = 0;
|
|
try {
|
|
skipped = istream.skip(start);
|
|
} catch (IOException e) {
|
|
return e;
|
|
}
|
|
if (skipped < start) {
|
|
return new IOException(sm.getString("defaultServlet.skipfail",
|
|
Long.valueOf(skipped), Long.valueOf(start)));
|
|
}
|
|
|
|
IOException exception = null;
|
|
long bytesToRead = end - start + 1;
|
|
|
|
byte buffer[] = new byte[input];
|
|
int len = buffer.length;
|
|
while ( (bytesToRead > 0) && (len >= buffer.length)) {
|
|
try {
|
|
len = istream.read(buffer);
|
|
if (bytesToRead >= len) {
|
|
ostream.write(buffer, 0, len);
|
|
bytesToRead -= len;
|
|
} else {
|
|
ostream.write(buffer, 0, (int) bytesToRead);
|
|
bytesToRead = 0;
|
|
}
|
|
} catch (IOException e) {
|
|
exception = e;
|
|
len = -1;
|
|
}
|
|
if (len < buffer.length)
|
|
break;
|
|
}
|
|
|
|
return exception;
|
|
|
|
}
|
|
|
|
|
|
protected static class Range {
|
|
|
|
public long start;
|
|
public long end;
|
|
public long length;
|
|
|
|
/**
|
|
* Validate range.
|
|
*
|
|
* @return true if the range is valid, otherwise false
|
|
*/
|
|
public boolean validate() {
|
|
if (end >= length)
|
|
end = length - 1;
|
|
return (start >= 0) && (end >= 0) && (start <= end) && (length > 0);
|
|
}
|
|
}
|
|
|
|
protected static class CompressionFormat implements Serializable {
|
|
private static final long serialVersionUID = 1L;
|
|
public final String extension;
|
|
public final String encoding;
|
|
|
|
public CompressionFormat(String extension, String encoding) {
|
|
this.extension = extension;
|
|
this.encoding = encoding;
|
|
}
|
|
}
|
|
|
|
private static class PrecompressedResource {
|
|
public final WebResource resource;
|
|
public final CompressionFormat format;
|
|
|
|
private PrecompressedResource(WebResource resource, CompressionFormat format) {
|
|
this.resource = resource;
|
|
this.format = format;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is secure in the sense that any attempt to use an external entity
|
|
* will trigger an exception.
|
|
*/
|
|
private static class SecureEntityResolver implements EntityResolver2 {
|
|
|
|
@Override
|
|
public InputSource resolveEntity(String publicId, String systemId)
|
|
throws SAXException, IOException {
|
|
throw new SAXException(sm.getString("defaultServlet.blockExternalEntity",
|
|
publicId, systemId));
|
|
}
|
|
|
|
@Override
|
|
public InputSource getExternalSubset(String name, String baseURI)
|
|
throws SAXException, IOException {
|
|
throw new SAXException(sm.getString("defaultServlet.blockExternalSubset",
|
|
name, baseURI));
|
|
}
|
|
|
|
@Override
|
|
public InputSource resolveEntity(String name, String publicId,
|
|
String baseURI, String systemId) throws SAXException,
|
|
IOException {
|
|
throw new SAXException(sm.getString("defaultServlet.blockExternalEntity2",
|
|
name, publicId, baseURI, systemId));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the ordering character to be used for a particular column.
|
|
*
|
|
* @param order The order that is currently being applied
|
|
* @param column The column that will be rendered.
|
|
*
|
|
* @return Either 'A' or 'D', to indicate "ascending" or "descending" sort
|
|
* order.
|
|
*/
|
|
private char getOrderChar(SortManager.Order order, char column) {
|
|
if(column == order.column) {
|
|
if(order.ascending) {
|
|
return 'D';
|
|
} else {
|
|
return 'A';
|
|
}
|
|
} else {
|
|
return 'D';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A class encapsulating the sorting of resources.
|
|
*/
|
|
private static class SortManager
|
|
{
|
|
/**
|
|
* The default sort.
|
|
*/
|
|
protected Comparator<WebResource> defaultResourceComparator;
|
|
|
|
/**
|
|
* Comparator to use when sorting resources by name.
|
|
*/
|
|
protected Comparator<WebResource> resourceNameComparator;
|
|
|
|
/**
|
|
* Comparator to use when sorting files by name, ascending (reverse).
|
|
*/
|
|
protected Comparator<WebResource> resourceNameComparatorAsc;
|
|
|
|
/**
|
|
* Comparator to use when sorting resources by size.
|
|
*/
|
|
protected Comparator<WebResource> resourceSizeComparator;
|
|
|
|
/**
|
|
* Comparator to use when sorting files by size, ascending (reverse).
|
|
*/
|
|
protected Comparator<WebResource> resourceSizeComparatorAsc;
|
|
|
|
/**
|
|
* Comparator to use when sorting resources by last-modified date.
|
|
*/
|
|
protected Comparator<WebResource> resourceLastModifiedComparator;
|
|
|
|
/**
|
|
* Comparator to use when sorting files by last-modified date, ascending (reverse).
|
|
*/
|
|
protected Comparator<WebResource> resourceLastModifiedComparatorAsc;
|
|
|
|
public SortManager(boolean directoriesFirst) {
|
|
resourceNameComparator = new ResourceNameComparator();
|
|
resourceNameComparatorAsc = Collections.reverseOrder(resourceNameComparator);
|
|
resourceSizeComparator = new ResourceSizeComparator(resourceNameComparator);
|
|
resourceSizeComparatorAsc = Collections.reverseOrder(resourceSizeComparator);
|
|
resourceLastModifiedComparator = new ResourceLastModifiedDateComparator(resourceNameComparator);
|
|
resourceLastModifiedComparatorAsc = Collections.reverseOrder(resourceLastModifiedComparator);
|
|
|
|
if(directoriesFirst) {
|
|
resourceNameComparator = new DirsFirstComparator(resourceNameComparator);
|
|
resourceNameComparatorAsc = new DirsFirstComparator(resourceNameComparatorAsc);
|
|
resourceSizeComparator = new DirsFirstComparator(resourceSizeComparator);
|
|
resourceSizeComparatorAsc = new DirsFirstComparator(resourceSizeComparatorAsc);
|
|
resourceLastModifiedComparator = new DirsFirstComparator(resourceLastModifiedComparator);
|
|
resourceLastModifiedComparatorAsc = new DirsFirstComparator(resourceLastModifiedComparatorAsc);
|
|
}
|
|
|
|
defaultResourceComparator = resourceNameComparator;
|
|
}
|
|
|
|
/**
|
|
* Sorts an array of resources according to an ordering string.
|
|
*
|
|
* @param resources The array to sort.
|
|
* @param order The ordering string.
|
|
*
|
|
* @see #getOrder(String)
|
|
*/
|
|
public void sort(WebResource[] resources, String order) {
|
|
Comparator<WebResource> comparator = getComparator(order);
|
|
|
|
if(null != comparator)
|
|
Arrays.sort(resources, comparator);
|
|
}
|
|
|
|
public Comparator<WebResource> getComparator(String order) {
|
|
return getComparator(getOrder(order));
|
|
}
|
|
|
|
public Comparator<WebResource> getComparator(Order order) {
|
|
if(null == order)
|
|
return defaultResourceComparator;
|
|
|
|
if('N' == order.column) {
|
|
if(order.ascending) {
|
|
return resourceNameComparatorAsc;
|
|
} else {
|
|
return resourceNameComparator;
|
|
}
|
|
}
|
|
|
|
if('S' == order.column) {
|
|
if(order.ascending) {
|
|
return resourceSizeComparatorAsc;
|
|
} else {
|
|
return resourceSizeComparator;
|
|
}
|
|
}
|
|
|
|
if('M' == order.column) {
|
|
if(order.ascending) {
|
|
return resourceLastModifiedComparatorAsc;
|
|
} else {
|
|
return resourceLastModifiedComparator;
|
|
}
|
|
}
|
|
|
|
return defaultResourceComparator;
|
|
}
|
|
|
|
/**
|
|
* Gets the Order to apply given an ordering-string. This
|
|
* ordering-string matches a subset of the ordering-strings
|
|
* supported by
|
|
* <a href="https://httpd.apache.org/docs/2.4/mod/mod_autoindex.html#query">Apache httpd</a>.
|
|
*
|
|
* @param order The ordering-string provided by the client.
|
|
*
|
|
* @return An Order specifying the column and ascending/descending to
|
|
* be applied to resources.
|
|
*/
|
|
public Order getOrder(String order) {
|
|
if(null == order || 0 == order.trim().length())
|
|
return Order.DEFAULT;
|
|
|
|
String[] options = order.split(";");
|
|
|
|
if(0 == options.length)
|
|
return Order.DEFAULT;
|
|
|
|
char column = '\0';
|
|
boolean ascending = false;
|
|
|
|
for(String option : options) {
|
|
option = option.trim();
|
|
|
|
if(2 < option.length()) {
|
|
char opt = option.charAt(0);
|
|
if('C' == opt)
|
|
column = option.charAt(2);
|
|
else if('O' == opt)
|
|
ascending = ('A' == option.charAt(2));
|
|
}
|
|
}
|
|
|
|
if('N' == column) {
|
|
if(ascending) {
|
|
return Order.NAME_ASC;
|
|
} else {
|
|
return Order.NAME;
|
|
}
|
|
}
|
|
|
|
if('S' == column) {
|
|
if(ascending) {
|
|
return Order.SIZE_ASC;
|
|
} else {
|
|
return Order.SIZE;
|
|
}
|
|
}
|
|
|
|
if('M' == column) {
|
|
if(ascending) {
|
|
return Order.LAST_MODIFIED_ASC;
|
|
} else {
|
|
return Order.LAST_MODIFIED;
|
|
}
|
|
}
|
|
|
|
return Order.DEFAULT;
|
|
}
|
|
|
|
public static class Order {
|
|
final char column;
|
|
final boolean ascending;
|
|
|
|
public Order(char column, boolean ascending) {
|
|
this.column = column;
|
|
this.ascending = ascending;
|
|
}
|
|
|
|
public static final Order NAME = new Order('N', false);
|
|
public static final Order NAME_ASC = new Order('N', true);
|
|
public static final Order SIZE = new Order('S', false);
|
|
public static final Order SIZE_ASC = new Order('S', true);
|
|
public static final Order LAST_MODIFIED = new Order('M', false);
|
|
public static final Order LAST_MODIFIED_ASC = new Order('M', true);
|
|
|
|
public static final Order DEFAULT = NAME;
|
|
}
|
|
}
|
|
|
|
private static class DirsFirstComparator
|
|
implements Comparator<WebResource>
|
|
{
|
|
private final Comparator<WebResource> base;
|
|
|
|
public DirsFirstComparator(Comparator<WebResource> core) {
|
|
this.base = core;
|
|
}
|
|
|
|
@Override
|
|
public int compare(WebResource r1, WebResource r2) {
|
|
if(r1.isDirectory()) {
|
|
if(r2.isDirectory()) {
|
|
return base.compare(r1, r2);
|
|
} else {
|
|
return -1; // r1, directory, first
|
|
}
|
|
} else if(r2.isDirectory()) {
|
|
return 1; // r2, directory, first
|
|
} else {
|
|
return base.compare(r1, r2);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static class ResourceNameComparator
|
|
implements Comparator<WebResource>
|
|
{
|
|
@Override
|
|
public int compare(WebResource r1, WebResource r2) {
|
|
return r1.getName().compareTo(r2.getName());
|
|
}
|
|
}
|
|
|
|
private static class ResourceSizeComparator
|
|
implements Comparator<WebResource>
|
|
{
|
|
private Comparator<WebResource> base;
|
|
|
|
public ResourceSizeComparator(Comparator<WebResource> base) {
|
|
this.base = base;
|
|
}
|
|
|
|
@Override
|
|
public int compare(WebResource r1, WebResource r2) {
|
|
int c = Long.compare(r1.getContentLength(), r2.getContentLength());
|
|
|
|
if(0 == c)
|
|
return base.compare(r1, r2);
|
|
else
|
|
return c;
|
|
}
|
|
}
|
|
|
|
private static class ResourceLastModifiedDateComparator
|
|
implements Comparator<WebResource>
|
|
{
|
|
private Comparator<WebResource> base;
|
|
|
|
public ResourceLastModifiedDateComparator(Comparator<WebResource> base) {
|
|
this.base = base;
|
|
}
|
|
|
|
@Override
|
|
public int compare(WebResource r1, WebResource r2) {
|
|
int c = Long.compare(r1.getLastModified(), r2.getLastModified());
|
|
|
|
if(0 == c)
|
|
return base.compare(r1, r2);
|
|
else
|
|
return c;
|
|
}
|
|
}
|
|
}
|