ttomcat-1778514358873.zip-extract/apache-tomcat-11.0.18-src/java/org/apache/catalina/servlets/DefaultServlet.java

Path
ttomcat-1778514358873.zip-extract/apache-tomcat-11.0.18-src/java/org/apache/catalina/servlets/DefaultServlet.java
Status
scanned
Type
file
Name
DefaultServlet.java
Extension
.java
Programming language
Java
Mime type
text/plain
File type
ASCII text, with CRLF line terminators
Tag

      
    
Rootfs path

      
    
Size
118630 (115.8 KB)
MD5
c0fe73c64aae201df0f02f6fe03964f0
SHA1
09adab39337c13d8f7b0f0a58b2a5cc8109c8439
SHA256
65a3707d23f2e5860925c964feb1e1f661c615ad72de69ae7be7bec2cc555c1a
SHA512

      
    
SHA1_git
63f13062e46cd1905b152e5d18de2c381d4a6b68
Is binary

      
    
Is text
True
Is archive

      
    
Is media

      
    
Is legal

      
    
Is manifest

      
    
Is readme

      
    
Is top level

      
    
Is key file

      
    
DefaultServlet.java | 115.8 KB |

/* * 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.Serial; 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.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Enumeration; import java.util.List; import java.util.Locale; import java.util.function.Function; import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import jakarta.servlet.DispatcherType; import jakarta.servlet.RequestDispatcher; import jakarta.servlet.ServletContext; import jakarta.servlet.ServletException; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.ServletResponse; import jakarta.servlet.ServletResponseWrapper; import jakarta.servlet.UnavailableException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; 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.FastHttpDateFormat; import org.apache.tomcat.util.http.Method; import org.apache.tomcat.util.http.ResponseUtil; import org.apache.tomcat.util.http.parser.ContentRange; import org.apache.tomcat.util.http.parser.EntityTag; import org.apache.tomcat.util.http.parser.Ranges; import org.apache.tomcat.util.res.StringManager; import org.apache.tomcat.util.security.Escape; /** * <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> * &lt;servlet-mapping&gt; * &lt;servlet-name&gt;default&lt;/servlet-name&gt; * &lt;url-pattern&gt;/&lt;/url-pattern&gt; * &lt;/servlet-mapping&gt; * </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> * &lt;servlet-mapping&gt; * &lt;servlet-name&gt;default&lt;/servlet-name&gt; * &lt;url-pattern&gt;/static/*&lt;/url-pattern&gt; * &lt;/servlet-mapping&gt; * </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> */ public class DefaultServlet extends HttpServlet { @Serial private static final long serialVersionUID = 1L; /** * The string manager for this package. */ protected static final StringManager sm = StringManager.getManager(DefaultServlet.class); /** * Full range marker. */ protected static final Ranges FULL = new Ranges(null, new ArrayList<>()); private static final ContentRange IGNORE = new ContentRange(null, 0, 0, 0); /** * MIME multipart separation string */ protected static final String mimeSeparation = "CATALINA_MIME_BOUNDARY"; /** * Size of file transfer buffer in bytes. */ protected static final int BUFFER_SIZE = 4096; // ----------------------------------------------------- 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; /** * Status code to use for directory redirects. */ protected int directoryRedirectStatusCode = HttpServletResponse.SC_FOUND; /** * 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? Will default to {@link BomConfig#TRUE} in * {@link #init()}. */ private BomConfig useBomIfPresent = null; /** * Minimum size for sendfile usage in bytes. */ protected int sendfileSize = 48 * 1024; /** * Should the Accept-Ranges: bytes header be send with static resources? * * @deprecated This option will be removed without replacement in Tomcat 12 onwards where it will effectively be * hard coded to {@code true}. */ @Deprecated 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; /** * Use strong etags whenever possible. */ private boolean useStrongETags = false; /** * Will direct ({@link DispatcherType#REQUEST} or {@link DispatcherType#ASYNC}) requests using the POST method be * processed as GET requests. If not allowed, direct requests using the POST method will be rejected with a 405 * (method not allowed). */ private boolean allowPostAsGet = true; // --------------------------------------------------------- Public Methods @Override public void destroy() { // NOOP } @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("directoryRedirectStatusCode") != null) { String statusCodeString = getServletConfig().getInitParameter("directoryRedirectStatusCode"); int statusCode = Integer.parseInt(statusCodeString); switch (statusCode) { case HttpServletResponse.SC_MOVED_PERMANENTLY: case HttpServletResponse.SC_FOUND: case HttpServletResponse.SC_TEMPORARY_REDIRECT: case HttpServletResponse.SC_PERMANENT_REDIRECT: directoryRedirectStatusCode = statusCode; break; default: log(sm.getString("defaultServlet.invalidRedirectStatusCode", Integer.valueOf(statusCode))); } } 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); } } String useBomIfPresent = getServletConfig().getInitParameter("useBomIfPresent"); if (useBomIfPresent == null) { // Use default this.useBomIfPresent = BomConfig.TRUE; } else { for (BomConfig bomConfig : BomConfig.values()) { if (bomConfig.configurationValue.equalsIgnoreCase(useBomIfPresent)) { this.useBomIfPresent = bomConfig; break; } } if (this.useBomIfPresent == null) { // Unrecognised configuration value IllegalArgumentException iae = new IllegalArgumentException(sm.getString("defaultServlet.unknownBomConfig", useBomIfPresent)); throw new ServletException(iae); } } 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")); } // Prevent the use of buffer sizes that are too small 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")); } if (getServletConfig().getInitParameter("useStrongETags") != null) { useStrongETags = Boolean.parseBoolean(getServletConfig().getInitParameter("useStrongETags")); } if (getServletConfig().getInitParameter("allowPostAsGet") != null) { allowPostAsGet = Boolean.parseBoolean(getServletConfig().getInitParameter("allowPostAsGet")); } } 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[0]); } // ------------------------------------------------------ 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.isEmpty()) { result.append(servletPath); } if (pathInfo != null) { result.append(pathInfo); } if (result.isEmpty() && !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(); } protected boolean isListings() { return listings; } protected boolean isReadOnly() { return readOnly || resources.isReadOnly(); } @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { if (req.getDispatcherType() == DispatcherType.ERROR) { doGet(req, resp); } else { super.service(req, resp); } } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { // Serve the requested resource, including the data content serveResource(request, response, true, fileEncoding); } @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)); } /** * Determines the methods normally allowed for the resource. * * @param req The Servlet request * * @return The allowed HTTP methods */ protected String determineMethodsAllowed(HttpServletRequest req) { StringBuilder allow = new StringBuilder(); // Start with methods that are always allowed allow.append("OPTIONS, GET, HEAD"); if (allowPostAsGet) { allow.append(", POST"); } // PUT and DELETE depend on readonly if (!isReadOnly()) { 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(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { if (allowPostAsGet) { doGet(request, response); } else { // Use a switch without a default to ensure all possibilities are explicitly handled switch (request.getDispatcherType()) { case ASYNC: case REQUEST: { // Direct POST requests may not be processed as GET sendNotAllowed(request, response); break; } case ERROR: case FORWARD: case INCLUDE: { /* * Forward and Include are processed as GET as it is possible that a POST to a servlet may use a * forward or an include as part of generating the response. * * Error should have already been converted to GET but convert here anyway as that is better than * failing the request. */ doGet(request, response); break; } } } } @Override protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { if (isReadOnly()) { sendNotAllowed(req, resp); return; } String path = getRelativePath(req); WebResource resource = resources.getResource(path); ContentRange range = parseContentRange(req, resp); if (range == null) { // Processing error. parseContentRange() set the error code return; } if (!checkIfHeaders(req, resp, resource)) { return; } InputStream resourceInputStream = null; File tempContentFile = 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 { tempContentFile = executePartialPut(req, range, path); if (tempContentFile != null) { resourceInputStream = new FileInputStream(tempContentFile); } } if (resourceInputStream != null && resources.write(path, resourceInputStream, true)) { if (resource.exists()) { resp.setStatus(HttpServletResponse.SC_NO_CONTENT); } else { resp.setStatus(HttpServletResponse.SC_CREATED); } } else { try { resp.sendError(resourceInputStream != null ? HttpServletResponse.SC_CONFLICT : HttpServletResponse.SC_BAD_REQUEST); } catch (IllegalStateException e) { // Already committed, ignore } } } finally { if (resourceInputStream != null) { try { resourceInputStream.close(); } catch (IOException ignore) { // Ignore } } if (tempContentFile != null) { if (!tempContentFile.delete()) { log(sm.getString("defaultServlet.deleteTempFileFailed", tempContentFile.getAbsolutePath())); } } } } /** * 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, ContentRange 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); File contentFile = File.createTempFile("put-part-", null, tempDir); 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.getLength()); // Append data in request input stream to contentFile randAccessContentFile.seek(range.getStart()); long received = 0; int numBytesRead; byte[] transferBuffer = new byte[BUFFER_SIZE]; try (BufferedInputStream requestBufInStream = new BufferedInputStream(req.getInputStream(), BUFFER_SIZE)) { long rangeBytes = range.getEnd() - range.getStart() + 1L; while ((numBytesRead = requestBufInStream.read(transferBuffer)) != -1) { received += numBytesRead; if (received > rangeBytes) { throw new IllegalStateException(sm.getString("defaultServlet.wrongByteCountForRange", String.valueOf(received), String.valueOf(rangeBytes))); } randAccessContentFile.write(transferBuffer, 0, numBytesRead); } if (received < rangeBytes) { throw new IllegalStateException(sm.getString("defaultServlet.wrongByteCountForRange", String.valueOf(received), String.valueOf(rangeBytes))); } } } catch (IOException | RuntimeException | Error e) { // This has to be done this way to be able to close the file without changing the method signature if (!contentFile.delete()) { log(sm.getString("defaultServlet.deleteTempFileFailed", contentFile.getAbsolutePath())); } return null; } return contentFile; } @Override protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { if (isReadOnly()) { sendNotAllowed(req, resp); return; } String path = getRelativePath(req); WebResource resource = resources.getResource(path); if (!checkIfHeaders(req, resp, resource)) { return; } if (resource.exists()) { if (resource.delete()) { resp.setStatus(HttpServletResponse.SC_NO_CONTENT); } else { sendNotAllowed(req, resp); } } 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 { if (request.getHeader("If-Match") != null) { if (!checkIfMatch(request, response, resource)) { return false; } } else if (request.getHeader("If-Unmodified-Since") != null) { if (!checkIfUnmodifiedSince(request, response, resource)) { return false; } } if (request.getHeader("If-None-Match") != null) { return checkIfNoneMatch(request, response, resource); } else if (request.getHeader("If-Modified-Since") != null) { return checkIfModifiedSince(request, response, resource); } return true; } /** * 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.isEmpty()) { // 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 { // Need to check If headers before we return a 404 if (!checkIfHeaders(request, response, resource)) { return; } response.sendError(HttpServletResponse.SC_NOT_FOUND, sm.getString("defaultServlet.missingResource", 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; // 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 = generateETag(resource); lastModifiedHttp = resource.getLastModifiedHttp(); } // 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; } } // 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; } } } Ranges 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 (!isListings()) { response.sendError(HttpServletResponse.SC_NOT_FOUND, sm.getString("defaultServlet.missingResource", 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 || writer != 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 ignore) { // Content has already been written - this must be an include. Ignore the error and continue. } 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, useBomIfPresent.stripBom); if (bomCharset != null && useBomIfPresent.useBomEncoding) { 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, useBomIfPresent.stripBom); if (bomCharset != null && useBomIfPresent.useBomEncoding) { 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.getEntries().isEmpty()) { return; } // Partial content response. response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); if (ranges.getEntries().size() == 1) { Ranges.Entry range = ranges.getEntries().get(0); long start = getStart(range, contentLength); long end = getEnd(range, contentLength); response.addHeader("Content-Range", "bytes " + start + "-" + end + "/" + contentLength); long length = end - 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 ignore) { // Content has already been written - this must be an include. Ignore the error and continue. } if (ostream != null) { if (!checkSendfile(request, response, resource, contentLength, range)) { copy(resource, contentLength, 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) { // Content has already been written - this must be an include. Ignore the error and continue. } if (ostream != null) { copy(resource, contentLength, ostream, ranges, contentType); } else { // we should not get here throw new IllegalStateException(); } } } } } /* * Code borrowed heavily from Jasper's EncodingDetector */ private static Charset processBom(InputStream is, boolean stripBom) 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, stripBom); 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, stripBom); return StandardCharsets.UTF_16BE; } // Delay the UTF_16LE check if there are more than 2 bytes since it // overlaps with UTF-32LE. if (count == 2 && b0 == 0xFF && b1 == 0xFE) { skip(is, 2, stripBom); return StandardCharsets.UTF_16LE; } // Remaining BOMs are at least 3 bytes if (count < 3) { skip(is, 0, stripBom); return null; } // UTF-8 is only 3-byte BOM int b2 = bom[2] & 0xFF; if (b0 == 0xEF && b1 == 0xBB && b2 == 0xBF) { skip(is, 3, stripBom); return StandardCharsets.UTF_8; } if (count < 4) { skip(is, 0, stripBom); 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, stripBom); return StandardCharsets.UTF_16LE; } skip(is, 0, stripBom); return null; } private static void skip(InputStream is, int skip, boolean stripBom) throws IOException { is.reset(); if (stripBom) { while (skip-- > 0) { if (is.read() < 0) { // Ignore since included break; } } } } private static boolean isText(String contentType) { return contentType == null || contentType.startsWith("text") || contentType.endsWith("xml") || contentType.contains("/javascript"); } private static boolean validate(Ranges ranges, long length) { List<long[]> rangeContext = new ArrayList<>(); int overlapCount = 0; for (Ranges.Entry range : ranges.getEntries()) { long start = getStart(range, length); long end = getEnd(range, length); if (start < 0 || start > end) { // Invalid range return false; } /* * See https://www.rfc-editor.org/rfc/rfc9110.html#name-range and * https://www.rfc-editor.org/rfc/rfc9110.html#status.416 * * The server MAY ignore or reject Range headers with: * * - "Many" (undefined) small ranges not in ascending order - not currently enforced. * * - More than two overlapping ranges (enforced) */ for (long[] r : rangeContext) { long s2 = r[0]; long e2 = r[1]; // Given valid [s1,e1] and [s2,e2] // If { s1>e2 || s2>e1 } then no overlap // equivalent to // If not { s1>e2 || s2>e1 } then overlap // De Morgan's law if (start <= e2 && s2 <= end) { overlapCount++; // Off by one is deliberate. There is 1 more overlapping range than there are overlaps. if (overlapCount > 1) { return false; } } } rangeContext.add(new long[] { start, end }); } return true; } private static long getStart(Ranges.Entry range, long length) { long start = range.getStart(); if (start == -1) { long end = range.getEnd(); // If there is no start, then the start is based on the end if (end >= length) { return 0; } else { return length - end; } } else { return start; } } private static long getEnd(Ranges.Entry range, long length) { long end = range.getEnd(); if (range.getStart() == -1 || end == -1 || end >= length) { return length - 1; } else { return end; } } 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()), directoryRedirectStatusCode); } /** * 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 ContentRange 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 (!"bytes".equals(contentRange.getUnits())) { response.sendError(HttpServletResponse.SC_BAD_REQUEST); return null; } return contentRange; } /** * Parse the range header. * <p> * The caller is required to have confirmed that the requested resource exists and is a file before calling this * method. * * @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 Ranges parseRange(HttpServletRequest request, HttpServletResponse response, WebResource resource) throws IOException { // 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; } if (!Method.GET.equals(request.getMethod()) || !isRangeRequestsSupported()) { // RFC 9110 - Section 14.2: GET is the only method for which range handling is defined. // Otherwise MUST ignore a Range header field return FULL; } // Evaluate If-Range if (!checkIfRange(request, response, resource)) { if (response.isCommitted()) { /* * Ideally, checkIfRange() would be changed to return Boolean so the three states (satisfied, * unsatisfied and error) could each be communicated via the return value. There isn't a backwards * compatible way to do that that doesn't involve changing the method name and there are benefits to * retaining the consistency of the existing method name pattern. Hence, this 'trick'. For the error * state, checkIfRange() will call response.sendError() which will commit the response which this method * can then detect. */ return null; } // No error but If-Range not satisfied 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; } 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; } if (!validate(ranges, fileLength)) { response.addHeader("Content-Range", "bytes */" + fileLength); response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return null; } return ranges; } /** * 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 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(Escape.xml(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(" longDate='").append(childResource.getLastModified()).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. Thread currentThread = Thread.currentThread(); ClassLoader original = currentThread.getContextClassLoader(); try { 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 { currentThread.setContextClassLoader(original); } } /** * 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(); // Get the right strings StringManager sm = StringManager.getManager(DefaultServlet.class.getPackageName(), request.getLocales()); 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> "); sb.append("<html lang=\"").append(sm.getLocale().getLanguage()).append("\"> "); sb.append("<head> "); sb.append("<title>"); sb.append(sm.getString("defaultServlet.directory.title", directoryWebappPath)); sb.append("</title> "); sb.append("<style>"); sb.append(org.apache.catalina.util.TomcatCSS.TOMCAT_CSS); sb.append("</style> "); sb.append("</head> "); sb.append("<body> "); sb.append("<h1>"); sb.append(sm.getString("defaultServlet.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(" \u2013 <a href=\""); sb.append(rewrittenContextPath); if (parent.isEmpty()) { parent = "/"; } sb.append(rewriteUrl(parent)); if (!parent.endsWith("/")) { sb.append('/'); } sb.append("\">"); sb.append("<b>"); sb.append(sm.getString("defaultServlet.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\"> "); SortManager.Order order; if (sortListings) { order = sortManager.getOrder(request.getQueryString()); } else { order = null; } // Render the column headings sb.append("<thead> "); sb.append("<tr> "); sb.append("<th align=\"left\"><font size=\"+1\"><strong>"); if (order != null) { sb.append("<a href=\"?C=N;O="); sb.append(getOrderChar(order, 'N')); sb.append("\">"); sb.append(sm.getString("defaultServlet.resource.name")); sb.append("</a>"); } else { sb.append(sm.getString("defaultServlet.resource.name")); } sb.append("</strong></font></th> "); sb.append("<th align=\"center\"><font size=\"+1\"><strong>"); if (order != null) { sb.append("<a href=\"?C=S;O="); sb.append(getOrderChar(order, 'S')); sb.append("\">"); sb.append(sm.getString("defaultServlet.resource.size")); sb.append("</a>"); } else { sb.append(sm.getString("defaultServlet.resource.size")); } sb.append("</strong></font></th> "); sb.append("<th align=\"right\"><font size=\"+1\"><strong>"); if (order != null) { sb.append("<a href=\"?C=M;O="); sb.append(getOrderChar(order, 'M')); sb.append("\">"); sb.append(sm.getString("defaultServlet.resource.lastModified")); sb.append("</a>"); } else { sb.append(sm.getString("defaultServlet.resource.lastModified")); } sb.append("</strong></font></th> "); sb.append("</tr> "); sb.append("</thead> "); if (null != sortManager) { sortManager.sort(entries, request.getQueryString()); } boolean shade = false; sb.append("<tbody> "); 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("> "); shade = !shade; sb.append("<td align=\"left\">&nbsp;&nbsp; "); 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> "); sb.append("<td align=\"right\"><tt>"); if (childResource.isDirectory()) { sb.append("&nbsp;"); } else { sb.append(renderSize(childResource.getContentLength())); } sb.append("</tt></td> "); sb.append("<td align=\"right\"><tt>"); sb.append(renderTimestamp(childResource.getLastModified())); sb.append("</tt></td> "); sb.append("</tr> "); } sb.append("</tbody> "); // Render the page footer sb.append("</table> "); 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> "); sb.append("</html> "); // 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 (String.valueOf(leftSide) + "." + String.valueOf(rightSide) + " KiB"); } /** * Render the specified file timestamp. * * @param timestamp File timestamp * * @return the formatted timestamp */ protected String renderTimestamp(long timestamp) { return FastHttpDateFormat.formatDate(timestamp); } /** * 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); } IOException e = copyRange(reader, new PrintWriter(buffer)); if (debug > 10) { log("readme '" + readmeFile + "' output error: " + ((e != null) ? e.getMessage() : "")); } } catch (IOException ioe) { log(sm.getString("defaultServlet.readerCloseFailed"), ioe); } finally { if (reader != null) { try { reader.close(); } catch (IOException ignore) { // Ignore } } } 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) { return new StreamSource(is); } } if (debug > 10) { log("localXsltFile '" + localXsltFile + "' not found"); } } if (contextXsltFile != null) { InputStream is = getServletContext().getResourceAsStream(contextXsltFile); if (is != null) { return new StreamSource(is); } if (debug > 10) { log("contextXsltFile '" + contextXsltFile + "' not found"); } } /* * Open and read in file in one fell swoop to reduce the chance of leaving handle open. */ if (globalXsltFile != null) { File f = validateGlobalXsltFile(); if (f != null) { long globalXsltFileSize = f.length(); if (globalXsltFileSize > Integer.MAX_VALUE) { log(sm.getString("defaultServlet.globalXSLTTooBig", f.getAbsolutePath())); } 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.getCanonicalFile().toPath().startsWith(base.getCanonicalFile().toPath())) { 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; } // -------------------------------------------------------- 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, Ranges.Entry 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(getStart(range, length))); request.setAttribute(Globals.SENDFILE_FILE_END_ATTR, Long.valueOf(getEnd(range, length) + 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 { boolean conditionSatisfied = false; Enumeration<String> headerValues = request.getHeaders("If-Match"); String resourceETag = generateETag(resource); boolean hasAsteriskValue = false;// check existence of special header value '*' int headerCount = 0; while (headerValues.hasMoreElements() && !conditionSatisfied) { headerCount++; String headerValue = headerValues.nextElement(); if ("*".equals(headerValue)) { hasAsteriskValue = true; if (resourceETag != null) { conditionSatisfied = true; } } else { // RFC 7232 requires strong comparison for If-Match headers Boolean matched = EntityTag.compareEntityTag(new StringReader(headerValue), false, resourceETag); if (matched == null) { if (debug > 10) { log("DefaultServlet.checkIfMatch: Invalid header value [" + headerValue + "]"); } response.sendError(HttpServletResponse.SC_BAD_REQUEST); return false; } else { conditionSatisfied = matched.booleanValue(); } } } if (headerValues.hasMoreElements()) { headerCount++; } if (hasAsteriskValue && headerCount > 1) { // Note that an If-Match header field with a list value containing "*" and other values (including other // instances of "*") is syntactically invalid (therefore not allowed to be generated) and furthermore is // unlikely to be interoperable. response.sendError(HttpServletResponse.SC_BAD_REQUEST); return false; } 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) { String method = request.getMethod(); if (!Method.GET.equals(method) && !Method.HEAD.equals(method)) { return true; } long resourceLastModified = resource.getLastModified(); if (resourceLastModified <= 0) { // MUST ignore if the resource does not have a modification date available. return true; } // Must be at least one header for this method to be called Enumeration<String> headerEnum = request.getHeaders("If-Modified-Since"); headerEnum.nextElement(); if (headerEnum.hasMoreElements()) { // If-Modified-Since is a list of dates return true; } try { // Header is present so -1 will be not returned. Only a valid date or an IAE are possible. long headerValue = request.getDateHeader("If-Modified-Since"); if (resourceLastModified < (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", generateETag(resource)); 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 resourceETag = generateETag(resource); Enumeration<String> headerValues = request.getHeaders("If-None-Match"); boolean hasAsteriskValue = false;// check existence of special header value '*' boolean conditionSatisfied = true; int headerCount = 0; while (headerValues.hasMoreElements()) { headerCount++; String headerValue = headerValues.nextElement(); if (headerValue.equals("*")) { hasAsteriskValue = true; if (headerCount > 1 || headerValues.hasMoreElements()) { conditionSatisfied = false; } else { // asterisk '*' is the only field value. // RFC9110: If the field value is "*", the condition is false if the origin server has a current // representation for the target resource. if (resourceETag != null) { conditionSatisfied = false; } } break; } else { // RFC 7232 requires weak comparison for If-None-Match headers Boolean matched = EntityTag.compareEntityTag(new StringReader(headerValue), true, resourceETag); if (matched == null) { if (debug > 10) { log("DefaultServlet.checkIfNoneMatch: Invalid header value [" + headerValue + "]"); } response.sendError(HttpServletResponse.SC_BAD_REQUEST); return false; } if (matched.booleanValue()) { // RFC9110: If the field value is a list of entity tags, the condition is false if one of the // listed tags // matches the entity tag of the selected representation. conditionSatisfied = false; break; } } } if (headerValues.hasMoreElements()) { headerCount++; } if (hasAsteriskValue && headerCount > 1) { // Note that an If-None-Match header field with a list value containing "*" and other values (including // other instances of "*") is syntactically invalid (therefore not allowed to be generated) and furthermore // is unlikely to be interoperable. response.sendError(HttpServletResponse.SC_BAD_REQUEST); return false; } if (!conditionSatisfied) { // For GET and HEAD, we should respond with // 304 Not Modified. // For every other method, 412 Precondition Failed is sent // back. if (Method.GET.equals(request.getMethod()) || Method.HEAD.equals(request.getMethod())) { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); response.setHeader("ETag", resourceETag); } else { 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 { long resourceLastModified = resource.getLastModified(); if (resourceLastModified <= 0) { // MUST ignore if the resource does not have a modification date available. return true; } // Must be at least one header for this method to be called Enumeration<String> headerEnum = request.getHeaders("If-Unmodified-Since"); headerEnum.nextElement(); if (headerEnum.hasMoreElements()) { // If-Unmodified-Since is a list of dates return true; } try { // Header is present so -1 will be not returned. Only a valid date or an IAE are possible. long headerValue = request.getDateHeader("If-Unmodified-Since"); if (resourceLastModified >= (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; } /** * Check if the if-range condition is satisfied. The calling method is required to ensure a Range header is present * and that Range requests are supported for the current resource. * * @param request The servlet request we are processing * @param response The servlet response we are creating * @param resource The resource * * @return {@code true} if the resource meets the specified condition, and {@code false} if the condition is not * satisfied, resulting in transfer of the new selected representation instead of a 412 (Precondition * Failed) response. If the if-range condition is not valid then an appropriate status code will be set, * the response will be committed and this method will return {@code false} * * @throws IOException an IO error occurred */ protected boolean checkIfRange(HttpServletRequest request, HttpServletResponse response, WebResource resource) throws IOException { String resourceETag = generateETag(resource); long resourceLastModified = resource.getLastModified(); Enumeration<String> headerEnum = request.getHeaders("If-Range"); if (!headerEnum.hasMoreElements()) { // If-Range is not present return true; } String headerValue = headerEnum.nextElement().trim(); if (headerEnum.hasMoreElements()) { // Multiple If-Range headers response.sendError(HttpServletResponse.SC_BAD_REQUEST); return false; } if (headerValue.length() > 2 && (headerValue.charAt(0) == '"' || headerValue.charAt(2) == '"')) { boolean weakETag = headerValue.startsWith("W/\""); if ((!weakETag && headerValue.charAt(0) != '"') || headerValue.charAt(headerValue.length() - 1) != '"' || headerValue.indexOf('"', weakETag ? 3 : 1) != headerValue.length() - 1) { // Not a single entity tag response.sendError(HttpServletResponse.SC_BAD_REQUEST); return false; } // If the ETag the client gave does not match the entity // etag, then the entire entity is returned. return !weakETag && resourceETag != null && resourceETag.equals(headerValue); } else { long headerValueTime = -1L; try { headerValueTime = request.getDateHeader("If-Range"); } catch (IllegalArgumentException ignore) { // Ignore } if (headerValueTime >= 0) { // unit of HTTP date is second, ignore millisecond part. return resourceLastModified >= headerValueTime && resourceLastModified < headerValueTime + 1000; } else { // Not a single entity tag and not a valid date either response.sendError(HttpServletResponse.SC_BAD_REQUEST); return false; } } } /** * Checks if range request is supported by server * * @return <code>true</code> server supports range requests feature. */ protected boolean isRangeRequestsSupported() { // Range-Requests optional feature is enabled implicitly. return true; } /** * Provides the entity tag (the ETag header) for the given resource. Intended to be over-ridden by custom * DefaultServlet implementations that wish to use an alternative format for the entity tag. * * @param resource The resource for which an entity tag is required. * * @return The result of calling {@link WebResource#getETag()} on the given resource */ protected String generateETag(WebResource resource) { if (useStrongETags) { return resource.getStrongETag(); } else { return resource.getETag(); } } /** * 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 { InputStream istream = new BufferedInputStream(is, input); // Copy the input stream to the output stream IOException 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 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 { Reader reader; if (encoding == null) { reader = new InputStreamReader(is); } else { reader = new InputStreamReader(is, encoding); } // Copy the input stream to the output stream IOException 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 length the resource length * @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, long length, ServletOutputStream ostream, Ranges.Entry range) throws IOException { InputStream resourceInputStream = resource.getInputStream(); InputStream istream = new BufferedInputStream(resourceInputStream, input); IOException exception = copyRange(istream, ostream, getStart(range, length), getEnd(range, length)); // 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 length the resource length * @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, long length, ServletOutputStream ostream, Ranges ranges, String contentType) throws IOException { IOException exception = null; for (Ranges.Entry range : ranges.getEntries()) { if (exception != null) { break; } InputStream resourceInputStream = resource.getInputStream(); try (InputStream istream = new BufferedInputStream(resourceInputStream, input)) { // Writing MIME header. ostream.println(); ostream.println("--" + mimeSeparation); if (contentType != null) { ostream.println("Content-Type: " + contentType); } long start = getStart(range, length); long end = getEnd(range, length); ostream.println("Content-Range: bytes " + start + "-" + end + "/" + length); ostream.println(); // Printing content exception = copyRange(istream, ostream, start, 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]; while (true) { try { int len = istream.read(buffer); if (len == -1) { break; } ostream.write(buffer, 0, len); } catch (IOException ioe) { exception = ioe; 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]; while (true) { try { int len = reader.read(buffer); if (len == -1) { break; } writer.write(buffer, 0, len); } catch (IOException ioe) { exception = ioe; 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; try { skipped = istream.skip(start); } catch (IOException ioe) { return ioe; } 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 ioe) { exception = ioe; len = -1; } } return exception; } protected record CompressionFormat(String extension, String encoding) implements Serializable { @Serial private static final long serialVersionUID = 1L; } private record PrecompressedResource(WebResource resource, CompressionFormat format) { } /** * 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. */ protected 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; SortManager(boolean directoriesFirst) { resourceNameComparator = Comparator.comparing(WebResource::getName); resourceNameComparatorAsc = resourceNameComparator.reversed(); resourceSizeComparator = Comparator.comparing(WebResource::getContentLength).thenComparing(resourceNameComparator); resourceSizeComparatorAsc = resourceSizeComparator.reversed(); resourceLastModifiedComparator = Comparator.comparing(WebResource::getLastModified).thenComparing(resourceNameComparator); resourceLastModifiedComparatorAsc = resourceLastModifiedComparator.reversed(); if (directoriesFirst) { Comparator<WebResource> dirsFirst = comparingTrueFirst(WebResource::isDirectory); resourceNameComparator = dirsFirst.thenComparing(resourceNameComparator); resourceNameComparatorAsc = dirsFirst.thenComparing(resourceNameComparatorAsc); resourceSizeComparator = dirsFirst.thenComparing(resourceSizeComparator); resourceSizeComparatorAsc = dirsFirst.thenComparing(resourceSizeComparatorAsc); resourceLastModifiedComparator = dirsFirst.thenComparing(resourceLastModifiedComparator); resourceLastModifiedComparatorAsc = dirsFirst.thenComparing(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 || order.trim().isEmpty()) { 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; 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 Comparator<WebResource> comparingTrueFirst(Function<WebResource,Boolean> keyExtractor) { return (s1, s2) -> { Boolean r1 = keyExtractor.apply(s1); Boolean r2 = keyExtractor.apply(s2); if (r1.booleanValue()) { if (r2.booleanValue()) { return 0; } else { return -1; // r1 (property is true) first } } else if (r2.booleanValue()) { return 1; // r2 (property is true) first } else { return 0; } }; } enum BomConfig { /** * BoM is stripped if present and any BoM found used to determine the encoding used to read the resource. */ TRUE("true", true, true), /** * BoM is stripped if present but the configured file encoding is used to read the resource. */ FALSE("false", true, false), /** * BoM is not stripped and the configured file encoding is used to read the resource. */ PASS_THROUGH("pass-through", false, false); final String configurationValue; final boolean stripBom; final boolean useBomEncoding; BomConfig(String configurationValue, boolean stripBom, boolean useBomEncoding) { this.configurationValue = configurationValue; this.stripBom = stripBom; this.useBomEncoding = useBomEncoding; } } }
Detected license expression
apache-2.0
Detected license expression (SPDX)
Apache-2.0
Percentage of license text
1.17
Copyrights

      
    
Holders

      
    
Authors

      
    
License detections License expression License expression SPDX
apache_2_0-4bde3f57-78aa-4201-96bf-531cba09e7de apache-2.0 Apache-2.0
URL Start line End line
http://www.apache.org/licenses/LICENSE-2.0 9 9
https://www.rfc-editor.org/rfc/rfc9110.html#name-range 1349 1349
https://www.rfc-editor.org/rfc/rfc9110.html#status.416 1350 1350
https://httpd.apache.org/docs/2.4/mod/mod_autoindex.html#query 2910 2910