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

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

      
    
Rootfs path

      
    
Size
128151 (125.1 KB)
MD5
ddfc672bca892b9cd3247c6c83387368
SHA1
3c4f6b284363773729e491a27b37567486cf610f
SHA256
3842f270ed57d6fe9b141df30cf4269bf97f350e75bcb41031d5d34dd208b490
SHA512

      
    
SHA1_git
7436a6b6cacacf96db55ffd2ef51d14a1817ca90
Is binary

      
    
Is text
True
Is archive

      
    
Is media

      
    
Is legal

      
    
Is manifest

      
    
Is readme

      
    
Is top level

      
    
Is key file

      
    
WebdavServlet.java | 125.1 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.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.Serial; import java.io.Serializable; import java.io.StringReader; import java.io.StringWriter; import java.io.Writer; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.Deque; import java.util.Enumeration; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TimeZone; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import jakarta.servlet.DispatcherType; import jakarta.servlet.RequestDispatcher; import jakarta.servlet.ServletContext; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRegistration; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.apache.catalina.WebResource; import org.apache.catalina.connector.RequestFacade; import org.apache.catalina.util.DOMWriter; import org.apache.catalina.util.IOTools; import org.apache.catalina.util.XMLWriter; import org.apache.tomcat.PeriodicEventListener; import org.apache.tomcat.util.IntrospectionUtils; import org.apache.tomcat.util.http.ConcurrentDateFormat; import org.apache.tomcat.util.http.FastHttpDateFormat; import org.apache.tomcat.util.http.Method; import org.apache.tomcat.util.http.RequestUtil; import org.apache.tomcat.util.http.WebdavIfHeader; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.xml.sax.SAXException; /** * This servlet adds support for <a href="https://tools.ietf.org/html/rfc4918">WebDAV</a> * <a href="https://tools.ietf.org/html/rfc4918#section-18">level 3</a>. All the basic HTTP requests are handled by the * DefaultServlet. * <p> * The WebDAV servlet is only designed for use with path mapping. The WebdavServlet must not be used as the default * servlet (i.e. mapped to '/') or with any other mapping types as it will not work in those configurations. * <p> * By default, the entire web application is exposed via the WebDAV servlet. Mapping the WebDAV servlet to * <code>/*</code> provides WebDAV access to all the resources within the web application. To aid separation of normal * users and WebDAV users, the WebDAV servlet may be mounted at a sub-path (e.g. <code>/webdav/*</code>) which creates * an additional mapping for the entire web application under that sub-path, with WebDAV access to all the resources. * <p> * By default, the <code>WEB-INF</code> and <code>META-INF</code> directories are not accessible via WebDAV. This may be * changed by setting the <code>allowSpecialPaths</code> initialisation parameter to <code>true</code>. * <p> * It is also possible to enable WebDAV access to a sub-set of the standard web application URL space rather than * creating an additional, WebDAV specific mapping. To do this, map the WebDAV servlet to the desired sub-path and set * the <code>serveSubpathOnly</code> initialisation parameter to <code>true</code>. * <p> * Security constraints using the same URL pattern as the mapping (e.g. <code>/webdav/*</code>) can be used to limit the * users with access to WebDAV functionality. Care is required if using security constraints to further limit WebDAV * functionality. In particular, administrators should be aware that security constraints apply only to the request URL. * Security constraints do not apply to any destination URL associated with the WebDAV operation (such as COPY or MOVE). * <p> * If WebDAV functionality is included in a web application where legitimate users may access it via a browser, it is * recommended that the application include CORS protection. * <p> * To enable WebDAV for a context add the following to web.xml: * * <pre> * &lt;servlet&gt; * &lt;servlet-name&gt;webdav&lt;/servlet-name&gt; * &lt;servlet-class&gt;org.apache.catalina.servlets.WebdavServlet&lt;/servlet-class&gt; * &lt;init-param&gt; * &lt;param-name&gt;debug&lt;/param-name&gt; * &lt;param-value&gt;0&lt;/param-value&gt; * &lt;/init-param&gt; * &lt;init-param&gt; * &lt;param-name&gt;listings&lt;/param-name&gt; * &lt;param-value&gt;true&lt;/param-value&gt; * &lt;/init-param&gt; * &lt;/servlet&gt; * &lt;servlet-mapping&gt; * &lt;servlet-name&gt;webdav&lt;/servlet-name&gt; * &lt;url-pattern&gt;/*&lt;/url-pattern&gt; * &lt;/servlet-mapping&gt; * </pre> * * This will enable read only access with folder listings enabled. To enable read-write access add: * * <pre> * &lt;init-param&gt; * &lt;param-name&gt;readonly&lt;/param-name&gt; * &lt;param-value&gt;false&lt;/param-value&gt; * &lt;/init-param&gt; * </pre> * * To make the content editable via a different URL, use the following mapping: * * <pre> * &lt;servlet-mapping&gt; * &lt;servlet-name&gt;webdav&lt;/servlet-name&gt; * &lt;url-pattern&gt;/webdavedit/*&lt;/url-pattern&gt; * &lt;/servlet-mapping&gt; * </pre> * * By default, access to /WEB-INF and META-INF are not available via WebDAV. To enable access to these URLs, add: * * <pre> * &lt;init-param&gt; * &lt;param-name&gt;allowSpecialPaths&lt;/param-name&gt; * &lt;param-value&gt;true&lt;/param-value&gt; * &lt;/init-param&gt; * </pre> * * Don't forget to secure access appropriately to the editing URLs, especially if allowSpecialPaths is used. With the * mapping configuration above, the context will be accessible to normal users as before. Those users with the necessary * access will be able to edit content available via http://host:port/context/content using * http://host:port/context/webdavedit/content * <p> * The Servlet provides support for arbitrary dead properties on all resources (dead properties are properties whose * values are not protected by the server, such as the content length of a resource). By default, the Servlet will use * non persistent memory storage for them. Persistence can be achieved by implementing the <code>PropertyStore</code> * interface and configuring the Servlet to use that store. The <code>propertyStore</code> init-param allows configuring * the class name of the store to use, while the parameters in the form of <code>store.xxx</code> will be set on the * store object as bean properties. For example, this would configure a store with class * <code>com.MyPropertyStore</code>, and set its property <code>myName</code> to value <code>myValue</code>: * * <pre> * &lt;init-param&gt; * &lt;param-name&gt;propertyStore&lt;/param-name&gt; * &lt;param-value&gt;com.MyPropertyStore&lt;/param-value&gt; * &lt;/init-param&gt; * &lt;init-param&gt; * &lt;param-name&gt;store.myName&lt;/param-name&gt; * &lt;param-value&gt;myValue&lt;/param-value&gt; * &lt;/init-param&gt; * </pre> * <p> * * @see <a href="https://tools.ietf.org/html/rfc4918">RFC 4918</a> */ public class WebdavServlet extends DefaultServlet implements PeriodicEventListener { @Serial private static final long serialVersionUID = 1L; /** * Default lock timeout value. */ private static final int DEFAULT_TIMEOUT = 3600; /** * Maximum lock timeout. */ private static final int MAX_TIMEOUT = 604800; /** * Default maximum depth. */ private static final int MAX_DEPTH = 3; /** * Default namespace. */ protected static final String DEFAULT_NAMESPACE = "DAV:"; /** * Pre generated raw XML for supported locks. */ protected static final String SUPPORTED_LOCKS = " <D:lockentry><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockentry> " + " <D:lockentry><D:lockscope><D:shared/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockentry> "; /** * Simple date format for the creation date ISO representation (partial). */ protected static final ConcurrentDateFormat creationDateFormat = new ConcurrentDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US, TimeZone.getTimeZone("GMT")); /** * Lock scheme used. */ protected static final String LOCK_SCHEME = "urn:uuid:"; // ----------------------------------------------------- Instance Variables /** * Repository of all locks, keyed by path. */ private final ConcurrentHashMap<String,LockInfo> resourceLocks = new ConcurrentHashMap<>(); /** * Map of all shared locks, keyed by lock token. */ private final ConcurrentHashMap<String,LockInfo> sharedLocks = new ConcurrentHashMap<>(); /** * Default depth in spec is infinite. */ private int maxDepth = MAX_DEPTH; /** * Is access allowed via WebDAV to the special paths (/WEB-INF and /META-INF)? */ private boolean allowSpecialPaths = false; /** * Is the if header processing strict. */ private boolean strictIfProcessing = true; /** * Serve resources from the mounted subpath only, restoring the behavior of {@code DefaultServlet}. */ private boolean serveSubpathOnly = false; /** * Property store used for storage of dead properties. */ private PropertyStore store = null; // --------------------------------------------------------- Public Methods @Override public void init() throws ServletException { super.init(); // Validate that the Servlet is only mapped to wildcard mappings String servletName = getServletConfig().getServletName(); ServletRegistration servletRegistration = getServletConfig().getServletContext().getServletRegistration(servletName); Collection<String> servletMappings = servletRegistration.getMappings(); for (String mapping : servletMappings) { if (!mapping.endsWith("/*")) { log(sm.getString("webdavservlet.nonWildcardMapping", mapping)); } } if (getServletConfig().getInitParameter("maxDepth") != null) { maxDepth = Integer.parseInt(getServletConfig().getInitParameter("maxDepth")); } if (getServletConfig().getInitParameter("allowSpecialPaths") != null) { allowSpecialPaths = Boolean.parseBoolean(getServletConfig().getInitParameter("allowSpecialPaths")); } if (getServletConfig().getInitParameter("strictIfProcessing") != null) { strictIfProcessing = Boolean.parseBoolean(getServletConfig().getInitParameter("strictIfProcessing")); } if (getServletConfig().getInitParameter("serveSubpathOnly") != null) { serveSubpathOnly = Boolean.parseBoolean(getServletConfig().getInitParameter("serveSubpathOnly")); } String propertyStore = getServletConfig().getInitParameter("propertyStore"); if (propertyStore != null) { try { Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass(propertyStore); store = (PropertyStore) clazz.getConstructor().newInstance(); // Set init parameters as properties on the store Enumeration<String> parameterNames = getServletConfig().getInitParameterNames(); while (parameterNames.hasMoreElements()) { String parameterName = parameterNames.nextElement(); if (parameterName.startsWith("store.")) { StringBuilder actualMethod = new StringBuilder(); String parameterValue = getServletConfig().getInitParameter(parameterName); parameterName = parameterName.substring("store.".length()); if (!IntrospectionUtils.setProperty(store, parameterName, parameterValue, true, actualMethod)) { log(sm.getString("webdavservlet.noStoreParameter", parameterName, parameterValue)); } } } } catch (Exception e) { log(sm.getString("webdavservlet.storeError"), e); } } if (store == null) { log(sm.getString("webdavservlet.memorystore")); store = new MemoryPropertyStore(); } store.init(); } @Override public void destroy() { store.destroy(); } @Override public void periodicEvent() { // Check expiration of all locks for (LockInfo currentLock : sharedLocks.values()) { if (currentLock.hasExpired()) { sharedLocks.remove(currentLock.path); } } for (LockInfo currentLock : resourceLocks.values()) { if (currentLock.isExclusive()) { if (currentLock.hasExpired()) { resourceLocks.remove(currentLock.path); } } else { currentLock.sharedTokens.removeIf(token -> sharedLocks.get(token) == null); if (currentLock.sharedTokens.isEmpty()) { resourceLocks.remove(currentLock.path); } } } store.periodicEvent(); } // ------------------------------------------------ PropertyStore Interface /** * Handling of dead properties on resources. This interface allows providing storage for dead properties. Store * configuration is done through the <code>propertyStore</code> init parameter of the WebDAV Servlet, which should * contain the class name of the store. */ public interface PropertyStore { /** * Initialize the store. This is tied to the Servlet lifecycle and is called by its init method. */ void init(); /** * Destroy the store. This is tied to the Servlet lifecycle and is called by its destroy method. */ void destroy(); /** * Periodic event for maintenance tasks. */ void periodicEvent(); /** * Copy resource. Dead properties should be copied to the destination path. * * @param source the copy source path * @param destination the copy destination path */ void copy(String source, String destination); /** * Delete specified resource. Dead properties on a deleted resource should be deleted. * * @param resource the path of the resource to delete */ void delete(String resource); /** * Generate propfind XML fragments for dead properties. * * @param resource the resource path * @param property the dead property, if null then all dead properties must be written * @param nameOnly true if only the property name element should be generated * @param generatedXML the current generated XML for the PROPFIND response * * @return true if a property was specified and a corresponding dead property was found on the resource, false * otherwise */ boolean propfind(String resource, Node property, boolean nameOnly, XMLWriter generatedXML); /** * Apply proppatch to the specified resource. * * @param resource the resource path on which to apply the proppatch * @param operations the set and remove to apply, the final status codes of the result should be set on each * operation */ void proppatch(String resource, ArrayList<ProppatchOperation> operations); } // ----------------------------------------- ProppatchOperation Inner Class /** * Represents a PROPPATCH sub operation to be performed. */ public static class ProppatchOperation { private final PropertyUpdateType updateType; private final Node propertyNode; private final boolean protectedProperty; private int statusCode = HttpServletResponse.SC_OK; /** * PROPPATCH operation constructor. * * @param updateType the update type, either SET or REMOVE * @param propertyNode the XML node that contains the property name (and value if SET) */ public ProppatchOperation(PropertyUpdateType updateType, Node propertyNode) { this.updateType = updateType; this.propertyNode = propertyNode; String davName = getDAVNode(propertyNode); // displayname and getcontentlanguage are the DAV: properties that should not be protected protectedProperty = davName != null && (!(davName.equals("displayname") || davName.equals("getcontentlanguage"))); } /** * @return the updateType for this operation */ public PropertyUpdateType getUpdateType() { return this.updateType; } /** * @return the propertyNode the XML node that contains the property name (and value if SET) */ public Node getPropertyNode() { return this.propertyNode; } /** * @return the statusCode to set as a result of the operation */ public int getStatusCode() { return this.statusCode; } /** * @param statusCode the statusCode to set as a result of the operation */ public void setStatusCode(int statusCode) { this.statusCode = statusCode; } /** * @return <code>true</code> if the property is protected */ public boolean getProtectedProperty() { return this.protectedProperty; } } /** * Type of PROPFIND request. */ public enum PropfindType { FIND_BY_PROPERTY, FIND_ALL_PROP, FIND_PROPERTY_NAMES } /** * Type of property update in a PROPPATCH. */ public enum PropertyUpdateType { SET, REMOVE } // ------------------------------------------------------ Protected Methods /** * Return JAXP document builder instance. * * @return the document builder * * @throws ServletException document builder creation failed (wrapped <code>ParserConfigurationException</code> * exception) */ protected DocumentBuilder getDocumentBuilder() throws ServletException { DocumentBuilder documentBuilder; try { DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setNamespaceAware(true); documentBuilderFactory.setExpandEntityReferences(false); documentBuilder = documentBuilderFactory.newDocumentBuilder(); documentBuilder.setEntityResolver(new WebdavResolver(this.getServletContext())); } catch (ParserConfigurationException e) { throw new ServletException(sm.getString("webdavservlet.jaxpfailed")); } return documentBuilder; } @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { final String path = getRelativePath(req); // Error page check needs to come before special path check since // custom error pages are often located below WEB-INF so they are // not directly accessible. if (req.getDispatcherType() == DispatcherType.ERROR) { doGet(req, resp); return; } // Block access to special subdirectories. // DefaultServlet assumes it services resources from the root of the web app // and doesn't add any special path protection // WebdavServlet remounts the webapp under a new path, so this check is // necessary on all methods (including GET). if (isSpecialPath(path)) { resp.sendError(WebdavStatus.SC_NOT_FOUND); return; } final String method = req.getMethod(); if (debug > 0) { log("[" + method + "] " + path); } switch (method) { case Method.PROPFIND -> doPropfind(req, resp); case Method.PROPPATCH -> doProppatch(req, resp); case Method.MKCOL -> doMkcol(req, resp); case Method.COPY -> doCopy(req, resp); case Method.MOVE -> doMove(req, resp); case Method.LOCK -> doLock(req, resp); case Method.UNLOCK -> doUnlock(req, resp); // DefaultServlet processing default -> super.service(req, resp); } } @Override protected boolean checkIfHeaders(HttpServletRequest request, HttpServletResponse response, WebResource resource) throws IOException { // Skip regular HTTP evaluation for a null resource if (resource != null && !super.checkIfHeaders(request, response, resource)) { return false; } // Process the WebDAV If header using Apache Jackrabbit code String ifHeaderValue = request.getHeader("If"); if (ifHeaderValue != null) { WebdavIfHeader ifHeader = new WebdavIfHeader(getUriPrefix(request), ifHeaderValue); if (!ifHeader.hasValue()) { // Allow bad if syntax, will only be used for lock tokens return !strictIfProcessing; } String path = getRelativePath(request); // Get all hrefs from the if header Iterator<String> hrefs = ifHeader.getResources(); String currentPath; String currentHref; WebResource currentWebResource; if (hrefs.hasNext()) { currentHref = hrefs.next(); currentPath = getPathFromHref(currentHref, request); if (currentPath == null) { // The path was invalid return false; } currentWebResource = resources.getResource(currentPath); } else { currentPath = path; currentHref = getEncodedPath(path, resource, request); currentWebResource = resource; } // Iterate over all resources do { boolean exists = currentWebResource != null && currentWebResource.exists(); String eTag = exists ? generateETag(currentWebResource) : ""; // Collect all locks active on resource ArrayList<String> lockTokens = new ArrayList<>(); // No lock evaluation for non existing paths in strict mode // Problem: when doing a put with a locked parent folder, need to submit a tagged production with // the parent path and the token, simply submitting the token in the if would fail the precondition. if (!strictIfProcessing || exists) { String parentPath = currentPath; do { LockInfo parentLock = resourceLocks.get(parentPath); if (parentLock != null) { if (parentLock.hasExpired()) { resourceLocks.remove(parentPath); } else { // parentPath == currentPath is a check for the first loop if (parentPath == currentPath || parentLock.depth > 0) { if (parentLock.isExclusive()) { lockTokens.add(LOCK_SCHEME + parentLock.token); } else { parentLock.sharedTokens.removeIf(token -> sharedLocks.get(token) == null); if (parentLock.sharedTokens.isEmpty()) { resourceLocks.remove(parentLock.path); } for (String token : parentLock.sharedTokens) { LockInfo sharedLock = sharedLocks.get(token); if (sharedLock != null) { if (parentPath == currentPath || sharedLock.depth > 0) { lockTokens.add(LOCK_SCHEME + token); } } } } } } } int slash = parentPath.lastIndexOf('/'); if (slash < 0) { break; } parentPath = parentPath.substring(0, slash); } while (true); } // Evaluation if (ifHeader.matches(currentHref, lockTokens, eTag)) { return true; } if (hrefs.hasNext()) { currentHref = hrefs.next(); currentPath = getPathFromHref(currentHref, request); if (currentPath == null) { // The path was invalid return false; } currentWebResource = resources.getResource(currentPath); } else { break; } } while (true); return false; } return true; } /** * Override the DefaultServlet implementation and only use the PathInfo. If the ServletPath is non-null, it will be * because the WebDAV servlet has been mapped to a url other than /* to configure editing at different url than * normal viewing. * * @param request The servlet request we are processing * @param allowEmptyPath Used only to identify a call from DefaultServlet, to avoid removing the trailing slash * * @return the relative path */ @Override protected String getRelativePath(HttpServletRequest request, boolean allowEmptyPath) { if (serveSubpathOnly) { return super.getRelativePath(request, allowEmptyPath); } 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); } else { pathInfo = request.getPathInfo(); } StringBuilder result = new StringBuilder(); if (pathInfo != null) { result.append(pathInfo); } if (result.isEmpty()) { result.append('/'); } String resultString = result.toString(); if (!allowEmptyPath && resultString.length() > 1 && resultString.endsWith("/")) { resultString = resultString.substring(0, resultString.length() - 1); } return resultString; } @Override protected String getPathPrefix(final HttpServletRequest request) { if (serveSubpathOnly) { return super.getPathPrefix(request); } // Repeat the servlet path (e.g. /webdav/) in the listing path String contextPath = request.getContextPath(); if (request.getServletPath() != null) { contextPath = contextPath + request.getServletPath(); } return contextPath; } @Override protected String determineMethodsAllowed(HttpServletRequest req) { WebResource resource = resources.getResource(getRelativePath(req)); // These methods are always allowed. They may return a 404 (not a 405) // if the resource does not exist. StringBuilder methodsAllowed = new StringBuilder("OPTIONS, GET, POST, HEAD"); if (!isReadOnly()) { methodsAllowed.append(", DELETE"); if (!resource.isDirectory()) { methodsAllowed.append(", PUT"); } } // Trace - assume disabled unless we can prove otherwise if (req instanceof RequestFacade && ((RequestFacade) req).getAllowTrace()) { methodsAllowed.append(", TRACE"); } methodsAllowed.append(", LOCK, UNLOCK, PROPPATCH, COPY, MOVE"); if (isListings()) { methodsAllowed.append(", PROPFIND"); } if (!resource.exists()) { methodsAllowed.append(", MKCOL"); } return methodsAllowed.toString(); } @Override protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.addHeader("DAV", "1,2,3"); resp.addHeader("Allow", determineMethodsAllowed(req)); resp.addHeader("MS-Author-Via", "DAV"); } /** * PROPFIND Method. * * @param req The Servlet request * @param resp The Servlet response * * @throws ServletException If an error occurs * @throws IOException If an IO error occurs */ protected void doPropfind(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { if (!isListings()) { sendNotAllowed(req, resp); return; } String path = getRelativePath(req); // Properties which are to be displayed. List<Node> properties = new ArrayList<>(); // Propfind depth int depth; // Propfind type PropfindType type = null; String depthStr = req.getHeader("Depth"); if (depthStr == null) { depth = maxDepth; } else { switch (depthStr) { case "0" -> depth = 0; case "1" -> depth = 1; case "infinity" -> depth = maxDepth; default -> { resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } } } byte[] body; try (InputStream is = req.getInputStream(); ByteArrayOutputStream os = new ByteArrayOutputStream()) { IOTools.flow(is, os); body = os.toByteArray(); } catch (IOException ioe) { resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } if (body.length > 0) { DocumentBuilder documentBuilder = getDocumentBuilder(); try { Document document = documentBuilder.parse(new InputSource(new ByteArrayInputStream(body))); // Get the root element of the document Element rootElement = document.getDocumentElement(); if (!"propfind".equals(getDAVNode(rootElement))) { resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } NodeList childList = rootElement.getChildNodes(); for (int i = 0; i < childList.getLength(); i++) { Node currentNode = childList.item(i); switch (currentNode.getNodeType()) { case Node.TEXT_NODE: break; case Node.ELEMENT_NODE: String nodeName = getDAVNode(currentNode); if ("prop".equals(nodeName)) { if (type != null) { // Another was already defined resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } type = PropfindType.FIND_BY_PROPERTY; NodeList propChildList = currentNode.getChildNodes(); for (int j = 0; j < propChildList.getLength(); j++) { Node currentNode2 = propChildList.item(j); switch (currentNode2.getNodeType()) { case Node.TEXT_NODE: break; case Node.ELEMENT_NODE: properties.add(currentNode2); break; } } } if ("propname".equals(nodeName)) { if (type != null) { // Another was already defined resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } type = PropfindType.FIND_PROPERTY_NAMES; } if ("allprop".equals(nodeName)) { if (type != null) { // Another was already defined resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } type = PropfindType.FIND_ALL_PROP; } break; } } } catch (SAXException | IOException e) { // Something went wrong - bad request resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } if (type == null) { // Nothing meaningful in the propfind element resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } } else { type = PropfindType.FIND_ALL_PROP; } WebResource resource = resources.getResource(path); if (!checkIfHeaders(req, resp, resource)) { resp.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED); return; } if (!resource.exists()) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } resp.setStatus(WebdavStatus.SC_MULTI_STATUS); resp.setContentType("text/xml; charset=UTF-8"); // Create multistatus object XMLWriter generatedXML = new XMLWriter(resp.getWriter()); generatedXML.writeXMLHeader(); generatedXML.writeElement("D", DEFAULT_NAMESPACE, "multistatus", XMLWriter.OPENING); if (depth == 0) { propfindResource(generatedXML, getEncodedPath(path, resource, req), path, type, properties, resource.isFile(), resource.getCreation(), resource.getLastModified(), resource.getContentLength(), getServletContext().getMimeType(resource.getName()), generateETag(resource)); } else { // The stack always contains the object of the current level Deque<String> stack = new ArrayDeque<>(); stack.addFirst(path); // Stack of the objects one level below Deque<String> stackBelow = new ArrayDeque<>(); while ((!stack.isEmpty()) && (depth >= 0)) { String currentPath = stack.remove(); // Exclude any resource in the /WEB-INF and /META-INF subdirectories if (isSpecialPath(currentPath)) { continue; } resource = resources.getResource(currentPath); // File is in directory listing but doesn't appear to exist // Broken symlink or odd permission settings? if (resource.exists()) { propfindResource(generatedXML, getEncodedPath(currentPath, resource, req), currentPath, type, properties, resource.isFile(), resource.getCreation(), resource.getLastModified(), resource.getContentLength(), getServletContext().getMimeType(resource.getName()), generateETag(resource)); } if (resource.isDirectory() && (depth > 0)) { String[] entries = resources.list(currentPath); for (String entry : entries) { String newPath = currentPath; if (!(newPath.endsWith("/"))) { newPath += "/"; } newPath += entry; stackBelow.addFirst(newPath); } } if (stack.isEmpty()) { depth--; stack = stackBelow; stackBelow = new ArrayDeque<>(); } generatedXML.sendData(); } } generatedXML.writeElement("D", "multistatus", XMLWriter.CLOSING); generatedXML.sendData(); } /** * PROPPATCH Method. Dead properties support is a SHOULD in the specification and are not implemented. * * @param req The Servlet request * @param resp The Servlet response * * @throws ServletException If an error occurs * @throws IOException If an IO error occurs */ protected void doProppatch(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String path = getRelativePath(req); WebResource resource = resources.getResource(path); if (!checkIfHeaders(req, resp, resource)) { resp.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED); return; } if (!resource.exists()) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } if (isReadOnly()) { resp.sendError(WebdavStatus.SC_FORBIDDEN); return; } if (isLocked(path, req)) { resp.sendError(WebdavStatus.SC_LOCKED); return; } DocumentBuilder documentBuilder = getDocumentBuilder(); ArrayList<ProppatchOperation> operations = new ArrayList<>(); byte[] body; try (InputStream is = req.getInputStream(); ByteArrayOutputStream os = new ByteArrayOutputStream()) { IOTools.flow(is, os); body = os.toByteArray(); } catch (IOException ioe) { resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } if (body.length == 0) { resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } try { Document document = documentBuilder.parse(new InputSource(new ByteArrayInputStream(body))); // Get the root element of the document Element rootElement = document.getDocumentElement(); if (!"propertyupdate".equals(getDAVNode(rootElement))) { resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } NodeList childList = rootElement.getChildNodes(); for (int i = 0; i < childList.getLength(); i++) { Node currentNode = childList.item(i); switch (currentNode.getNodeType()) { case Node.TEXT_NODE: break; case Node.ELEMENT_NODE: String nodeName = getDAVNode(currentNode); if ("set".equals(nodeName)) { NodeList setChildList = currentNode.getChildNodes(); for (int j = 0; j < setChildList.getLength(); j++) { Node currentNode2 = setChildList.item(j); switch (currentNode2.getNodeType()) { case Node.TEXT_NODE: break; case Node.ELEMENT_NODE: if ("prop".equals(getDAVNode(currentNode2))) { Node property = getNode(currentNode2); if (property != null) { operations .add(new ProppatchOperation(PropertyUpdateType.SET, property)); } else { resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } } break; } } } if ("remove".equals(nodeName)) { NodeList removeChildList = currentNode.getChildNodes(); for (int j = 0; j < removeChildList.getLength(); j++) { Node currentNode2 = removeChildList.item(j); switch (currentNode2.getNodeType()) { case Node.TEXT_NODE: break; case Node.ELEMENT_NODE: if ("prop".equals(getDAVNode(currentNode2))) { Node property = getNode(currentNode2); if (property != null) { operations.add( new ProppatchOperation(PropertyUpdateType.REMOVE, property)); } else { resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } } break; } } } break; } } } catch (SAXException | IOException e) { // Something went wrong - bad request resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } store.proppatch(path, operations); resp.setStatus(WebdavStatus.SC_MULTI_STATUS); resp.setContentType("text/xml; charset=UTF-8"); // Create multistatus object XMLWriter generatedXML = new XMLWriter(resp.getWriter()); generatedXML.writeXMLHeader(); generatedXML.writeElement("D", DEFAULT_NAMESPACE, "multistatus", XMLWriter.OPENING); generatedXML.writeElement("D", "response", XMLWriter.OPENING); // Generating href element generatedXML.writeElement("D", "href", XMLWriter.OPENING); generatedXML.writeText(getEncodedPath(path, resource, req)); generatedXML.writeElement("D", "href", XMLWriter.CLOSING); for (ProppatchOperation operation : operations) { generatedXML.writeElement("D", "propstat", XMLWriter.OPENING); generatedXML.writeElement("D", "prop", XMLWriter.OPENING); generatedXML.writeElement(operation.propertyNode.getPrefix(), operation.propertyNode.getNamespaceURI(), operation.propertyNode.getLocalName(), XMLWriter.NO_CONTENT); generatedXML.writeElement("D", "prop", XMLWriter.CLOSING); generatedXML.writeElement("D", "status", XMLWriter.OPENING); generatedXML.writeText("HTTP/1.1 " + String.valueOf(operation.getStatusCode()) + " "); generatedXML.writeElement("D", "status", XMLWriter.CLOSING); if (operation.getProtectedProperty() && operation.getStatusCode() == HttpServletResponse.SC_FORBIDDEN) { generatedXML.writeElement("D", "error", XMLWriter.OPENING); generatedXML.writeElement("D", "cannot-modify-protected-property", XMLWriter.NO_CONTENT); generatedXML.writeElement("D", "error", XMLWriter.CLOSING); } generatedXML.writeElement("D", "propstat", XMLWriter.CLOSING); } generatedXML.writeElement("D", "response", XMLWriter.CLOSING); generatedXML.writeElement("D", "multistatus", XMLWriter.CLOSING); generatedXML.sendData(); } private static Node getNode(Node currentNode2) { NodeList propChildList = currentNode2.getChildNodes(); Node property = null; for (int k = 0; k < propChildList.getLength(); k++) { Node currentNode3 = propChildList.item(k); switch (currentNode3.getNodeType()) { case Node.TEXT_NODE: break; case Node.ELEMENT_NODE: property = currentNode3; break; } } return property; } /** * MKCOL Method. * * @param req The Servlet request * @param resp The Servlet response * * @throws ServletException If an error occurs * @throws IOException If an IO error occurs */ protected void doMkcol(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String path = getRelativePath(req); WebResource resource = resources.getResource(path); if (!checkIfHeaders(req, resp, resource)) { resp.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED); return; } // Can't create a collection if a resource already exists at the given // path if (resource.exists()) { sendNotAllowed(req, resp); return; } if (isReadOnly()) { resp.sendError(WebdavStatus.SC_FORBIDDEN); return; } if (isLocked(path, req)) { resp.sendError(WebdavStatus.SC_LOCKED); return; } if (req.getContentLengthLong() > 0 || "chunked".equalsIgnoreCase(req.getHeader("Transfer-Encoding"))) { // No support for MKCOL bodies, which are non standard resp.sendError(WebdavStatus.SC_UNSUPPORTED_MEDIA_TYPE); return; } if (resources.mkdir(path)) { resp.setStatus(WebdavStatus.SC_CREATED); } else { resp.sendError(WebdavStatus.SC_CONFLICT); } } @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)) { resp.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED); } deleteResource(path, req, resp, true); } @Override protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String path = getRelativePath(req); WebResource resource = resources.getResource(path); if (!checkIfHeaders(req, resp, resource)) { resp.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED); return; } if (isLocked(path, req)) { resp.sendError(WebdavStatus.SC_LOCKED); return; } if (resource.isDirectory()) { sendNotAllowed(req, resp); return; } super.doPut(req, resp); } /** * COPY Method. * * @param req The Servlet request * @param resp The Servlet response * * @throws IOException If an IO error occurs */ protected void doCopy(HttpServletRequest req, HttpServletResponse resp) throws IOException { if (isReadOnly()) { resp.sendError(WebdavStatus.SC_FORBIDDEN); return; } String path = getRelativePath(req); copyResource(path, req, resp); } /** * MOVE Method. * * @param req The Servlet request * @param resp The Servlet response * * @throws IOException If an IO error occurs */ protected void doMove(HttpServletRequest req, HttpServletResponse resp) throws IOException { if (isReadOnly()) { resp.sendError(WebdavStatus.SC_FORBIDDEN); return; } String path = getRelativePath(req); if (isLocked(path, req)) { resp.sendError(WebdavStatus.SC_LOCKED); return; } if (copyResource(path, req, resp)) { deleteResource(path, req, resp, false); } } /** * LOCK Method. * * @param req The Servlet request * @param resp The Servlet response * * @throws ServletException If an error occurs * @throws IOException If an IO error occurs */ protected void doLock(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { if (isReadOnly()) { resp.sendError(WebdavStatus.SC_FORBIDDEN); return; } String path = getRelativePath(req); WebResource resource = resources.getResource(path); if (!checkIfHeaders(req, resp, resource)) { resp.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED); return; } LockInfo lock = new LockInfo(maxDepth); lock.principal = req.getRemoteUser(); // Parsing lock request // Parsing depth header String depthStr = req.getHeader("Depth"); if (depthStr == null) { lock.depth = maxDepth; } else { if (depthStr.equals("0")) { lock.depth = 0; } else if (depthStr.equals("infinity")) { lock.depth = maxDepth; } else { resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } } // Parsing timeout header int lockDuration = DEFAULT_TIMEOUT; String lockDurationStr = req.getHeader("Timeout"); if (lockDurationStr != null) { if (lockDurationStr.startsWith("Second-")) { try { lockDuration = Integer.parseInt(lockDurationStr.substring("Second-".length())); } catch (NumberFormatException e) { // Ignore } } else if (lockDurationStr.equals("Infinite")) { lockDuration = MAX_TIMEOUT; } if (lockDuration == 0) { lockDuration = DEFAULT_TIMEOUT; } if (lockDuration > MAX_TIMEOUT) { lockDuration = MAX_TIMEOUT; } } lock.expiresAt = System.currentTimeMillis() + (lockDuration * 1000L); boolean lockCreation = false; Node lockInfoNode = null; byte[] body; try (InputStream is = req.getInputStream(); ByteArrayOutputStream os = new ByteArrayOutputStream()) { IOTools.flow(is, os); body = os.toByteArray(); } catch (IOException ioe) { resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } if (body.length > 0) { DocumentBuilder documentBuilder = getDocumentBuilder(); try { Document document = documentBuilder.parse(new InputSource(new ByteArrayInputStream(body))); // Get the root element of the document Element rootElement = document.getDocumentElement(); if (!"lockinfo".equals(getDAVNode(rootElement))) { resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } lockInfoNode = rootElement; lockCreation = true; } catch (IOException | SAXException e) { resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } } if (lockInfoNode != null) { // Reading lock information NodeList childList = lockInfoNode.getChildNodes(); StringWriter strWriter; DOMWriter domWriter; Node lockScopeNode = null; Node lockTypeNode = null; Node lockOwnerNode = null; for (int i = 0; i < childList.getLength(); i++) { Node currentNode = childList.item(i); switch (currentNode.getNodeType()) { case Node.TEXT_NODE: break; case Node.ELEMENT_NODE: if ("lockscope".equals(getDAVNode(currentNode))) { lockScopeNode = currentNode; } if ("locktype".equals(getDAVNode(currentNode))) { lockTypeNode = currentNode; } if ("owner".equals(getDAVNode(currentNode))) { lockOwnerNode = currentNode; } break; } } if (lockScopeNode != null) { childList = lockScopeNode.getChildNodes(); for (int i = 0; i < childList.getLength(); i++) { Node currentNode = childList.item(i); switch (currentNode.getNodeType()) { case Node.TEXT_NODE: break; case Node.ELEMENT_NODE: lock.scope = getDAVNode(currentNode); break; } } if (lock.scope == null) { // Bad request resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } } else { // Bad request resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } if (lockTypeNode != null) { childList = lockTypeNode.getChildNodes(); for (int i = 0; i < childList.getLength(); i++) { Node currentNode = childList.item(i); switch (currentNode.getNodeType()) { case Node.TEXT_NODE: break; case Node.ELEMENT_NODE: lock.type = getDAVNode(currentNode); break; } } if (lock.type == null) { // Bad request resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } } else { // Bad request resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } if (lockOwnerNode != null) { childList = lockOwnerNode.getChildNodes(); for (int i = 0; i < childList.getLength(); i++) { Node currentNode = childList.item(i); switch (currentNode.getNodeType()) { case Node.TEXT_NODE: lock.owner += currentNode.getNodeValue(); break; case Node.ELEMENT_NODE: strWriter = new StringWriter(); domWriter = new DOMWriter(strWriter); domWriter.print(currentNode); lock.owner += strWriter.toString(); break; } } if (lock.owner == null) { // Bad request resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } } else { lock.owner = ""; } } lock.path = path; if (lockCreation) { // Check if the resource or a parent is already locked String parentPath = path; do { LockInfo parentLock = resourceLocks.get(parentPath); if (parentLock != null) { if (parentLock.hasExpired()) { resourceLocks.remove(parentPath); } else if (parentLock.isExclusive() || lock.isExclusive()) { // A parent collection of this collection is locked resp.setStatus(WebdavStatus.SC_LOCKED); return; } } int slash = parentPath.lastIndexOf('/'); if (slash < 0) { break; } parentPath = parentPath.substring(0, slash); } while (true); // Generating lock id lock.token = UUID.randomUUID().toString(); if (resource.isDirectory() && lock.depth == maxDepth) { // Locking a collection (and all its member resources) // Checking if a child resource of this collection is // already locked // Note: it is likely faster in many cases to go over the full lock list than trying to go over all the // children (recursively) List<String> lockPaths = new ArrayList<>(); for (LockInfo currentLock : resourceLocks.values()) { if (currentLock.hasExpired()) { resourceLocks.remove(currentLock.path); continue; } if ((currentLock.isExclusive() || lock.isExclusive()) && currentLock.path.startsWith(lock.path + "/")) { // A child resource of this collection is locked lockPaths.add(currentLock.lockroot); } } if (!lockPaths.isEmpty()) { // One of the child paths was locked // We generate a multistatus error report resp.setStatus(WebdavStatus.SC_MULTI_STATUS); XMLWriter generatedXML = new XMLWriter(); generatedXML.writeXMLHeader(); generatedXML.writeElement("D", DEFAULT_NAMESPACE, "multistatus", XMLWriter.OPENING); generatedXML.writeElement("D", "response", XMLWriter.OPENING); generatedXML.writeElement("D", "href", XMLWriter.OPENING); generatedXML.writeText(getEncodedPath(path, resource, req)); generatedXML.writeElement("D", "href", XMLWriter.CLOSING); generatedXML.writeElement("D", "status", XMLWriter.OPENING); generatedXML.writeText("HTTP/1.1 " + WebdavStatus.SC_FAILED_DEPENDENCY + " "); generatedXML.writeElement("D", "status", XMLWriter.CLOSING); generatedXML.writeElement("D", "response", XMLWriter.CLOSING); for (String lockPath : lockPaths) { generatedXML.writeElement("D", "response", XMLWriter.OPENING); generatedXML.writeElement("D", "href", XMLWriter.OPENING); generatedXML.writeText(lockPath); generatedXML.writeElement("D", "href", XMLWriter.CLOSING); generatedXML.writeElement("D", "status", XMLWriter.OPENING); generatedXML.writeText("HTTP/1.1 " + WebdavStatus.SC_LOCKED + " "); generatedXML.writeElement("D", "status", XMLWriter.CLOSING); generatedXML.writeElement("D", "response", XMLWriter.CLOSING); } generatedXML.writeElement("D", "multistatus", XMLWriter.CLOSING); Writer writer = resp.getWriter(); writer.write(generatedXML.toString()); writer.close(); return; } } else { // Locking a single resource // Checking if a resource exists at this path if (!resource.exists()) { // RFC 4918 removes lock null, instead an empty file is created if (!resources.write(path, new ByteArrayInputStream(new byte[0]), false)) { resp.sendError(WebdavStatus.SC_CONFLICT); return; } else { resp.setStatus(HttpServletResponse.SC_CREATED); } } } lock.lockroot = getEncodedPath(lock.path, resource, req); if (lock.isExclusive()) { resourceLocks.put(path, lock); } else { // Checking if there is already a shared lock on this path LockInfo sharedLock = resourceLocks.get(path); if (sharedLock == null) { sharedLock = new LockInfo(maxDepth); sharedLock.scope = "shared"; sharedLock.path = path; sharedLock.lockroot = lock.lockroot; sharedLock.depth = maxDepth; resourceLocks.put(path, sharedLock); } sharedLock.sharedTokens.add(lock.token); sharedLocks.put(lock.token, lock); } // Add the Lock-Token header as by RFC 2518 8.10.1 resp.addHeader("Lock-Token", "<" + LOCK_SCHEME + lock.token + ">"); } if (!lockCreation) { String ifHeader = req.getHeader("If"); if (ifHeader == null) { // Bad request resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } LockInfo toRenew = null; String parentPath = path; do { LockInfo parentLock = resourceLocks.get(parentPath); if (parentLock != null) { if (parentLock.hasExpired()) { resourceLocks.remove(parentPath); } else { // parentPath == currentPath is a check for the first loop if (parentPath == path || parentLock.depth > 0) { if (parentLock.isExclusive()) { if (ifHeader.contains(":" + parentLock.token + ">") && (parentLock.principal == null || parentLock.principal.equals(req.getRemoteUser()))) { toRenew = parentLock; break; } } else { for (String token : parentLock.sharedTokens) { if (ifHeader.contains(":" + token + ">")) { LockInfo sharedLock = sharedLocks.get(token); if (sharedLock != null && (sharedLock.principal == null || sharedLock.principal.equals(req.getRemoteUser()))) { if (parentPath == path || sharedLock.depth > 0) { toRenew = sharedLock; break; } } } } } } } } int slash = parentPath.lastIndexOf('/'); if (slash < 0) { break; } parentPath = parentPath.substring(0, slash); } while (true); if (toRenew != null) { if (!toRenew.hasExpired()) { toRenew.expiresAt = lock.expiresAt; } else { toRenew = null; } } lock = toRenew; } // Set the status, then generate the XML response containing // the lock information XMLWriter generatedXML = new XMLWriter(); generatedXML.writeXMLHeader(); generatedXML.writeElement("D", DEFAULT_NAMESPACE, "prop", XMLWriter.OPENING); generatedXML.writeElement("D", "lockdiscovery", XMLWriter.OPENING); if (lock != null) { lock.toXML(generatedXML); } generatedXML.writeElement("D", "lockdiscovery", XMLWriter.CLOSING); generatedXML.writeElement("D", "prop", XMLWriter.CLOSING); resp.setContentType("text/xml; charset=UTF-8"); Writer writer = resp.getWriter(); writer.write(generatedXML.toString()); writer.close(); } /** * UNLOCK Method. * * @param req The Servlet request * @param resp The Servlet response * * @throws IOException If an IO error occurs */ protected void doUnlock(HttpServletRequest req, HttpServletResponse resp) throws IOException { if (isReadOnly()) { resp.sendError(WebdavStatus.SC_FORBIDDEN); return; } String path = getRelativePath(req); WebResource resource = resources.getResource(path); if (!checkIfHeaders(req, resp, resource)) { resp.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED); return; } String lockTokenHeader = req.getHeader("Lock-Token"); if (lockTokenHeader == null) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST); return; } boolean unlocked = false; String parentPath = path; do { LockInfo parentLock = resourceLocks.get(parentPath); if (parentLock != null) { if (parentLock.hasExpired()) { resourceLocks.remove(parentPath); } else { // parentPath == currentPath is a check for the first loop if (parentPath == path || parentLock.depth > 0) { if (parentLock.isExclusive()) { if (lockTokenHeader.contains(":" + parentLock.token + ">") && (parentLock.principal == null || parentLock.principal.equals(req.getRemoteUser()))) { resourceLocks.remove(parentPath); unlocked = true; } else { // No parent exclusive lock will be found unlocked = false; } break; } else { for (String token : parentLock.sharedTokens) { if (lockTokenHeader.contains(":" + token + ">")) { LockInfo lock = sharedLocks.get(token); if (lock == null) { parentLock.sharedTokens.remove(token); } else if (lock.principal == null || lock.principal.equals(req.getRemoteUser())) { // The shared lock might not have the same depth if (parentPath == path || lock.depth > 0) { parentLock.sharedTokens.remove(token); sharedLocks.remove(token); unlocked = true; } } // Unlike the if header, this can only match one token break; } } if (parentLock.sharedTokens.isEmpty()) { resourceLocks.remove(parentPath); } } } } } int slash = parentPath.lastIndexOf('/'); if (slash < 0) { break; } parentPath = parentPath.substring(0, slash); } while (true); if (unlocked) { resp.setStatus(WebdavStatus.SC_NO_CONTENT); } else { sendReport(req, resp, parentPath, WebdavStatus.SC_CONFLICT, "lock-token-matches-request-uri"); } } // -------------------------------------------------------- Private Methods /** * Checks whether a given path refers to a resource under <code>WEB-INF</code> or <code>META-INF</code>. * * @param path the full path of the resource being accessed * * @return <code>true</code> if the resource specified is under a special path */ private boolean isSpecialPath(final String path) { if (!allowSpecialPaths) { String upperCasePath = path.toUpperCase(Locale.ENGLISH); return upperCasePath.startsWith("/WEB-INF/") || upperCasePath.startsWith("/META-INF/") || upperCasePath.equals("/WEB-INF") || upperCasePath.equals("/META-INF"); } return false; } private String getEncodedPath(String path, WebResource resource, HttpServletRequest request) { String href = getPathPrefix(request); if ((href.endsWith("/")) && (path.startsWith("/"))) { href += path.substring(1); } else { href += path; } if (resource != null && resource.isDirectory() && (!href.endsWith("/"))) { href += "/"; } return rewriteUrl(href); } private String getUriPrefix(HttpServletRequest request) { return request.getScheme() + "://" + request.getServerName(); } private String getPathFromHref(String href, HttpServletRequest req) { if (href == null || href.isEmpty()) { return null; } URI hrefUri; try { hrefUri = new URI(href); } catch (URISyntaxException e) { return null; } String hrefPath = hrefUri.getPath(); // Avoid path traversals if (!hrefPath.equals(RequestUtil.normalize(hrefPath))) { return null; } if (hrefUri.isAbsolute()) { if (!req.getServerName().equals(hrefUri.getHost())) { return null; } } if (hrefPath.length() > 1 && hrefPath.endsWith("/")) { hrefPath = hrefPath.substring(0, hrefPath.length() - 1); } // Verify context path String reqContextPath = getPathPrefix(req); if (!hrefPath.startsWith(reqContextPath + "/")) { return null; } // Remove context path & servlet path hrefPath = hrefPath.substring(reqContextPath.length()); if (debug > 0) { log(href + " Href path: " + hrefPath); } // Protect special subdirectories if (isSpecialPath(hrefPath)) { return null; } return hrefPath; } /** * Check to see if a resource is currently write locked. The method will look at the "If" header to make sure the * client has demonstrated knowledge of the appropriate lock tokens. * * @param path The relative path * @param req Servlet request * * @return <code>true</code> if the resource is locked (and no appropriate lock token has been found for at least * one of the non-shared locks which are present on the resource). */ private boolean isLocked(String path, HttpServletRequest req) { if (path == null) { path = getRelativePath(req); } String ifHeader = req.getHeader("If"); if (ifHeader == null) { ifHeader = ""; } return isLocked(path, req.getRemoteUser(), ifHeader); } /** * Check to see if a resource is currently write locked. * * @param path Path of the resource * @param principal The authenticated principal name * @param ifHeader "If" HTTP header which was included in the request * * @return <code>true</code> if the resource is locked (and no appropriate lock token has been found for at least * one of the non-shared locks which are present on the resource). */ private boolean isLocked(String path, String principal, String ifHeader) { boolean unmatchedSharedLock = false; // Check if the resource or a parent is already locked String parentPath = path; do { LockInfo parentLock = resourceLocks.get(parentPath); if (parentLock != null) { if (parentLock.hasExpired()) { resourceLocks.remove(parentPath); } else { // parentPath == currentPath is a check for the first loop if (parentPath == path || parentLock.depth > 0) { if (parentLock.isExclusive()) { return !ifHeader.contains(":" + parentLock.token + ">") || (parentLock.principal != null && !parentLock.principal.equals(principal)); } else { for (String token : parentLock.sharedTokens) { LockInfo lock = sharedLocks.get(token); if (lock != null) { // The shared lock might not have the same depth if (parentPath == path || lock.depth > 0) { if (ifHeader.contains(":" + token + ">") && (lock.principal == null || lock.principal.equals(principal))) { return false; } // Since it is a shared lock, continue to look up the tree but note that there // was a lock unmatchedSharedLock = true; } } } } } } } int slash = parentPath.lastIndexOf('/'); if (slash < 0) { break; } parentPath = parentPath.substring(0, slash); } while (true); return unmatchedSharedLock; } /** * Copy a resource. * * @param path Path of the resource to copy * @param req Servlet request * @param resp Servlet response * * @return boolean true if the copy is successful * * @throws IOException If an IO error occurs */ private boolean copyResource(String path, HttpServletRequest req, HttpServletResponse resp) throws IOException { // Check the source exists WebResource source = resources.getResource(path); if (!source.exists()) { resp.sendError(WebdavStatus.SC_NOT_FOUND); return false; } if (!checkIfHeaders(req, resp, source)) { resp.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED); return false; } // Parsing destination header // See RFC 4918 String destinationHeader = req.getHeader("Destination"); if (destinationHeader == null || destinationHeader.isEmpty()) { resp.sendError(WebdavStatus.SC_BAD_REQUEST); return false; } URI destinationUri; try { destinationUri = new URI(destinationHeader); } catch (URISyntaxException e) { resp.sendError(WebdavStatus.SC_BAD_REQUEST); return false; } String destinationPath = destinationUri.getPath(); // Destination isn't allowed to use '.' or '..' segments if (!destinationPath.equals(RequestUtil.normalize(destinationPath))) { resp.sendError(WebdavStatus.SC_BAD_REQUEST); return false; } if (destinationUri.isAbsolute()) { // Scheme and host need to match if (!req.getScheme().equals(destinationUri.getScheme()) || !req.getServerName().equals(destinationUri.getHost())) { resp.sendError(WebdavStatus.SC_FORBIDDEN); return false; } // Port needs to match too but handled separately as the logic is a // little more complicated if (req.getServerPort() != destinationUri.getPort()) { if (destinationUri.getPort() == -1 && ("http".equals(req.getScheme()) && req.getServerPort() == 80 || "https".equals(req.getScheme()) && req.getServerPort() == 443)) { // All good. } else { resp.sendError(WebdavStatus.SC_FORBIDDEN); return false; } } } if (destinationPath.length() > 1 && destinationPath.endsWith("/")) { destinationPath = destinationPath.substring(0, destinationPath.length() - 1); } // Cross-context operations aren't supported String reqContextPath = getPathPrefix(req); String expectedTargetPath = reqContextPath; // Also ensure copy (and move) operations do not escape the configured sub-path when limited to the sub-path if (serveSubpathOnly && req.getServletPath() != null) { expectedTargetPath = expectedTargetPath + req.getServletPath(); } if (!destinationPath.startsWith(expectedTargetPath + "/")) { resp.sendError(WebdavStatus.SC_FORBIDDEN); return false; } // Remove context path & servlet path destinationPath = destinationPath.substring(reqContextPath.length()); if (debug > 0) { log("Dest path: " + destinationPath); } // Check destination path to protect special subdirectories if (isSpecialPath(destinationPath)) { resp.sendError(WebdavStatus.SC_FORBIDDEN); return false; } if (destinationPath.equals(path)) { resp.sendError(WebdavStatus.SC_FORBIDDEN); return false; } // Check src / dest are not sub-dirs of each other if (destinationPath.startsWith(path) && destinationPath.charAt(path.length()) == '/' || path.startsWith(destinationPath) && path.charAt(destinationPath.length()) == '/') { resp.sendError(WebdavStatus.SC_FORBIDDEN); return false; } // Check if destination is locked if (isLocked(destinationPath, req)) { resp.sendError(WebdavStatus.SC_LOCKED); return false; } boolean overwrite = true; String overwriteHeader = req.getHeader("Overwrite"); if (overwriteHeader != null) { overwrite = overwriteHeader.equalsIgnoreCase("T"); } // Overwriting the destination WebResource destination = resources.getResource(destinationPath); if (overwrite) { // Delete destination resource, if it exists if (destination.exists()) { if (!deleteResource(destinationPath, req, resp, true)) { return false; } } else { resp.setStatus(WebdavStatus.SC_CREATED); } } else { // If the destination exists, then it's a conflict if (destination.exists()) { resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED); return false; } } // Copying source to destination Map<String,Integer> errorList = new LinkedHashMap<>(); boolean infiniteCopy = true; String depthHeader = req.getHeader("Depth"); if (depthHeader != null) { if (depthHeader.equals("infinity")) { // NO-OP - this is the default } else if (depthHeader.equals("0")) { infiniteCopy = false; } else { resp.sendError(WebdavStatus.SC_BAD_REQUEST); return false; } } boolean result = copyResource(errorList, path, destinationPath, infiniteCopy); if ((!result) || (!errorList.isEmpty())) { if (errorList.size() == 1) { resp.sendError(errorList.values().iterator().next().intValue()); } else { sendReport(req, resp, errorList); } return false; } // Copy was successful if (destination.exists()) { resp.setStatus(WebdavStatus.SC_NO_CONTENT); } else { resp.setStatus(WebdavStatus.SC_CREATED); } return true; } /** * Copy a collection. * * @param errorList Map containing the list of errors which occurred during the copy operation * @param source Path of the resource to be copied * @param dest Destination path * @param infiniteCopy {@code true} if this copy is to be an infinite copy, otherwise {@code false} for a shallow * copy * * @return <code>true</code> if the copy was successful */ private boolean copyResource(Map<String,Integer> errorList, String source, String dest, boolean infiniteCopy) { if (debug > 1) { log("Copy: " + source + " To: " + dest + " Infinite: " + infiniteCopy); } WebResource sourceResource = resources.getResource(source); if (sourceResource.isDirectory()) { if (!resources.mkdir(dest)) { WebResource destResource = resources.getResource(dest); if (!destResource.isDirectory()) { errorList.put(dest, Integer.valueOf(WebdavStatus.SC_CONFLICT)); return false; } } else { store.copy(source, dest); } if (infiniteCopy) { String[] entries = resources.list(source); for (String entry : entries) { String childDest = dest; if (!childDest.equals("/")) { childDest += "/"; } childDest += entry; String childSrc = source; if (!childSrc.equals("/")) { childSrc += "/"; } childSrc += entry; copyResource(errorList, childSrc, childDest, true); } } } else if (sourceResource.isFile()) { WebResource destResource = resources.getResource(dest); if (!destResource.exists() && !destResource.getWebappPath().endsWith("/")) { int lastSlash = destResource.getWebappPath().lastIndexOf('/'); if (lastSlash > 0) { String parent = destResource.getWebappPath().substring(0, lastSlash); WebResource parentResource = resources.getResource(parent); if (!parentResource.isDirectory()) { errorList.put(source, Integer.valueOf(WebdavStatus.SC_CONFLICT)); return false; } } } // WebDAV Litmus test attempts to copy/move a file over a collection // Need to remove trailing / from destination to enable test to pass if (!destResource.exists() && dest.endsWith("/") && dest.length() > 1) { // Convert destination name from collection (with trailing '/') // to file (without trailing '/') dest = dest.substring(0, dest.length() - 1); } try (InputStream is = sourceResource.getInputStream()) { if (!resources.write(dest, is, false)) { errorList.put(source, Integer.valueOf(WebdavStatus.SC_INTERNAL_SERVER_ERROR)); return false; } else { store.copy(source, dest); } } catch (IOException ioe) { log(sm.getString("webdavservlet.inputstreamclosefail", source), ioe); } } else { errorList.put(source, Integer.valueOf(WebdavStatus.SC_INTERNAL_SERVER_ERROR)); return false; } return true; } /** * Delete a resource. * * @param path Path of the resource which is to be deleted * @param req Servlet request * @param resp Servlet response * @param setStatus Should the response status be set on successful completion * * @return <code>true</code> if the delete is successful * * @throws IOException If an IO error occurs */ private boolean deleteResource(String path, HttpServletRequest req, HttpServletResponse resp, boolean setStatus) throws IOException { if (isLocked(path, req)) { resp.sendError(WebdavStatus.SC_LOCKED); return false; } WebResource resource = resources.getResource(path); if (!resource.exists()) { resp.sendError(WebdavStatus.SC_NOT_FOUND); return false; } if (!resource.isDirectory()) { if (!resource.delete()) { sendNotAllowed(req, resp); return false; } deletedResource(path); } else { Map<String,Integer> errorList = new LinkedHashMap<>(); deleteCollection(req, path, errorList); if (!resource.delete()) { /* * See RFC 4918, section 9.6.1, last paragraph. * * If a child resource can't be deleted then the parent resource SHOULD NOT be included in the * multi-status response since the notice of the failure to delete the child implies that all parent * resources could also not be deleted. */ if (resources.list(path).length == 0) { /* * The resource could not be deleted. If the resource is a directory and it has no children (or all * those children have been successfully deleted) then it should be listed in the multi-status * response. */ errorList.put(path, Integer.valueOf(WebdavStatus.SC_METHOD_NOT_ALLOWED)); } } else { deletedResource(path); } if (!errorList.isEmpty()) { sendReport(req, resp, errorList); return false; } } if (setStatus) { resp.setStatus(WebdavStatus.SC_NO_CONTENT); } return true; } /** * Deletes a collection. * * @param req The Servlet request * @param path Path to the collection to be deleted * @param errorList Contains the list of the errors which occurred */ private void deleteCollection(HttpServletRequest req, String path, Map<String,Integer> errorList) { if (debug > 1) { log("Delete collection: " + path); } // Prevent deletion of special subdirectories if (isSpecialPath(path)) { errorList.put(path, Integer.valueOf(WebdavStatus.SC_FORBIDDEN)); return; } String ifHeader = req.getHeader("If"); if (ifHeader == null) { ifHeader = ""; } String[] entries = resources.list(path); for (String entry : entries) { String childName = path; if (!childName.equals("/")) { childName += "/"; } childName += entry; if (isLocked(childName, req.getRemoteUser(), ifHeader)) { errorList.put(childName, Integer.valueOf(WebdavStatus.SC_LOCKED)); } else { WebResource childResource = resources.getResource(childName); if (childResource.isDirectory()) { deleteCollection(req, childName, errorList); } if (!childResource.delete()) { /* * See RFC 4918, section 9.6.1, last paragraph. * * If a child resource can't be deleted then the parent resource SHOULD NOT be included in the * multi-status response since the notice of the failure to delete the child implies that all parent * resources could also not be deleted. */ if (!childResource.isDirectory() || resources.list(childName).length == 0) { /* * The resource could not be deleted. If the resource is not a directory or if the resource is a * directory and it has no children (or all those children have been successfully deleted) then * it should be listed in the multi-status response. */ errorList.put(childName, Integer.valueOf(WebdavStatus.SC_METHOD_NOT_ALLOWED)); } } else { deletedResource(childName); } } } } private void deletedResource(String path) { LockInfo lock = resourceLocks.remove(path); if (lock != null && !lock.isExclusive()) { for (String token : lock.sharedTokens) { sharedLocks.remove(token); } } store.delete(path); } /** * Send a multistatus element containing a complete error report to the client. * * @param req Servlet request * @param resp Servlet response * @param errorList List of error to be displayed * * @throws IOException If an IO error occurs */ private void sendReport(HttpServletRequest req, HttpServletResponse resp, Map<String,Integer> errorList) throws IOException { resp.setStatus(WebdavStatus.SC_MULTI_STATUS); XMLWriter generatedXML = new XMLWriter(); generatedXML.writeXMLHeader(); generatedXML.writeElement("D", DEFAULT_NAMESPACE, "multistatus", XMLWriter.OPENING); for (Map.Entry<String,Integer> errorEntry : errorList.entrySet()) { String errorPath = errorEntry.getKey(); int errorCode = errorEntry.getValue().intValue(); generatedXML.writeElement("D", "response", XMLWriter.OPENING); generatedXML.writeElement("D", "href", XMLWriter.OPENING); generatedXML.writeText(getEncodedPath(errorPath, null, req)); generatedXML.writeElement("D", "href", XMLWriter.CLOSING); generatedXML.writeElement("D", "status", XMLWriter.OPENING); generatedXML.writeText("HTTP/1.1 " + errorCode + " "); generatedXML.writeElement("D", "status", XMLWriter.CLOSING); generatedXML.writeElement("D", "response", XMLWriter.CLOSING); } generatedXML.writeElement("D", "multistatus", XMLWriter.CLOSING); Writer writer = resp.getWriter(); writer.write(generatedXML.toString()); writer.close(); } private void sendReport(HttpServletRequest req, HttpServletResponse resp, String errorPath, int errorCode, String error) throws IOException { resp.setStatus(errorCode); XMLWriter generatedXML = new XMLWriter(); generatedXML.writeXMLHeader(); generatedXML.writeElement("D", DEFAULT_NAMESPACE, "error", XMLWriter.OPENING); if (errorPath != null && !errorPath.isEmpty()) { generatedXML.writeElement("D", error, XMLWriter.OPENING); generatedXML.writeElement("D", "href", XMLWriter.OPENING); generatedXML.writeText(getEncodedPath(errorPath, null, req)); generatedXML.writeElement("D", "href", XMLWriter.CLOSING); generatedXML.writeElement("D", error, XMLWriter.CLOSING); } else { generatedXML.writeElement("D", error, XMLWriter.NO_CONTENT); } generatedXML.writeElement("D", "error", XMLWriter.CLOSING); Writer writer = resp.getWriter(); writer.write(generatedXML.toString()); writer.close(); } private void propfindResource(XMLWriter generatedXML, String rewrittenUrl, String path, PropfindType propFindType, List<Node> properties, boolean isFile, long created, long lastModified, long contentLength, String contentType, String eTag) { generatedXML.writeElement("D", "response", XMLWriter.OPENING); String status = "HTTP/1.1 " + WebdavStatus.SC_OK + " "; // Generating href element generatedXML.writeElement("D", "href", XMLWriter.OPENING); generatedXML.writeText(rewrittenUrl); generatedXML.writeElement("D", "href", XMLWriter.CLOSING); switch (propFindType) { case FIND_ALL_PROP: generatedXML.writeElement("D", "propstat", XMLWriter.OPENING); generatedXML.writeElement("D", "prop", XMLWriter.OPENING); generatedXML.writeProperty("D", "creationdate", getISOCreationDate(created)); generatedXML.writeProperty("D", "getlastmodified", FastHttpDateFormat.formatDate(lastModified)); if (isFile) { generatedXML.writeProperty("D", "getcontentlength", Long.toString(contentLength)); if (contentType != null) { generatedXML.writeProperty("D", "getcontenttype", contentType); } generatedXML.writeProperty("D", "getetag", eTag); generatedXML.writeElement("D", "resourcetype", XMLWriter.NO_CONTENT); } else { generatedXML.writeElement("D", "resourcetype", XMLWriter.OPENING); generatedXML.writeElement("D", "collection", XMLWriter.NO_CONTENT); generatedXML.writeElement("D", "resourcetype", XMLWriter.CLOSING); } store.propfind(path, null, false, generatedXML); generatedXML.writeElement("D", "supportedlock", XMLWriter.OPENING); generatedXML.writeRaw(SUPPORTED_LOCKS); generatedXML.writeElement("D", "supportedlock", XMLWriter.CLOSING); generateLockDiscovery(path, generatedXML); generatedXML.writeElement("D", "prop", XMLWriter.CLOSING); generatedXML.writeElement("D", "status", XMLWriter.OPENING); generatedXML.writeText(status); generatedXML.writeElement("D", "status", XMLWriter.CLOSING); generatedXML.writeElement("D", "propstat", XMLWriter.CLOSING); break; case FIND_PROPERTY_NAMES: generatedXML.writeElement("D", "propstat", XMLWriter.OPENING); generatedXML.writeElement("D", "prop", XMLWriter.OPENING); generatedXML.writeElement("D", "creationdate", XMLWriter.NO_CONTENT); if (isFile) { generatedXML.writeElement("D", "getcontentlength", XMLWriter.NO_CONTENT); if (contentType != null) { generatedXML.writeElement("D", "getcontenttype", XMLWriter.NO_CONTENT); } generatedXML.writeElement("D", "getetag", XMLWriter.NO_CONTENT); } generatedXML.writeElement("D", "getlastmodified", XMLWriter.NO_CONTENT); generatedXML.writeElement("D", "resourcetype", XMLWriter.NO_CONTENT); generatedXML.writeElement("D", "lockdiscovery", XMLWriter.NO_CONTENT); generatedXML.writeElement("D", "supportedlock", XMLWriter.NO_CONTENT); store.propfind(path, null, true, generatedXML); generatedXML.writeElement("D", "prop", XMLWriter.CLOSING); generatedXML.writeElement("D", "status", XMLWriter.OPENING); generatedXML.writeText(status); generatedXML.writeElement("D", "status", XMLWriter.CLOSING); generatedXML.writeElement("D", "propstat", XMLWriter.CLOSING); break; case FIND_BY_PROPERTY: List<Node> propertiesNotFound = new ArrayList<>(); // Parse the list of properties generatedXML.writeElement("D", "propstat", XMLWriter.OPENING); generatedXML.writeElement("D", "prop", XMLWriter.OPENING); for (Node propertyNode : properties) { String property = getDAVNode(propertyNode); boolean protectedProperty = property != null && (!(property.equals("displayname") || property.equals("getcontentlanguage"))); if (property == null || !protectedProperty) { if (!store.propfind(path, propertyNode, false, generatedXML)) { propertiesNotFound.add(propertyNode); } } else if (property.equals("creationdate")) { generatedXML.writeProperty("D", "creationdate", getISOCreationDate(created)); } else if (property.equals("getcontentlength")) { if (isFile) { generatedXML.writeProperty("D", "getcontentlength", Long.toString(contentLength)); } else { propertiesNotFound.add(propertyNode); } } else if (property.equals("getcontenttype")) { if (isFile && contentType != null) { generatedXML.writeProperty("D", "getcontenttype", contentType); } else { propertiesNotFound.add(propertyNode); } } else if (property.equals("getetag")) { if (isFile) { generatedXML.writeProperty("D", "getetag", eTag); } else { propertiesNotFound.add(propertyNode); } } else if (property.equals("getlastmodified")) { generatedXML.writeProperty("D", "getlastmodified", FastHttpDateFormat.formatDate(lastModified)); } else if (property.equals("resourcetype")) { if (isFile) { generatedXML.writeElement("D", "resourcetype", XMLWriter.NO_CONTENT); } else { generatedXML.writeElement("D", "resourcetype", XMLWriter.OPENING); generatedXML.writeElement("D", "collection", XMLWriter.NO_CONTENT); generatedXML.writeElement("D", "resourcetype", XMLWriter.CLOSING); } } else if (property.equals("supportedlock")) { generatedXML.writeElement("D", "supportedlock", XMLWriter.OPENING); generatedXML.writeRaw(SUPPORTED_LOCKS); generatedXML.writeElement("D", "supportedlock", XMLWriter.CLOSING); } else if (property.equals("lockdiscovery")) { generateLockDiscovery(path, generatedXML); } else { propertiesNotFound.add(propertyNode); } } generatedXML.writeElement("D", "prop", XMLWriter.CLOSING); generatedXML.writeElement("D", "status", XMLWriter.OPENING); generatedXML.writeText(status); generatedXML.writeElement("D", "status", XMLWriter.CLOSING); generatedXML.writeElement("D", "propstat", XMLWriter.CLOSING); if (!propertiesNotFound.isEmpty()) { status = "HTTP/1.1 " + WebdavStatus.SC_NOT_FOUND + " "; generatedXML.writeElement("D", "propstat", XMLWriter.OPENING); generatedXML.writeElement("D", "prop", XMLWriter.OPENING); for (Node propertyNotFoundNode : propertiesNotFound) { String propertyNotFound = getDAVNode(propertyNotFoundNode); if (propertyNotFound != null) { generatedXML.writeElement("D", propertyNotFound, XMLWriter.NO_CONTENT); } else { generatedXML.writeElement(null, propertyNotFoundNode.getNamespaceURI(), propertyNotFoundNode.getLocalName(), XMLWriter.NO_CONTENT); } } generatedXML.writeElement("D", "prop", XMLWriter.CLOSING); generatedXML.writeElement("D", "status", XMLWriter.OPENING); generatedXML.writeText(status); generatedXML.writeElement("D", "status", XMLWriter.CLOSING); generatedXML.writeElement("D", "propstat", XMLWriter.CLOSING); } break; } generatedXML.writeElement("D", "response", XMLWriter.CLOSING); } /** * Print the lock discovery information associated with a path. * * @param path Path * @param generatedXML XML data to which the locks info will be appended */ private void generateLockDiscovery(String path, XMLWriter generatedXML) { generatedXML.writeElement("D", "lockdiscovery", XMLWriter.OPENING); String parentPath = path; do { LockInfo parentLock = resourceLocks.get(parentPath); if (parentLock != null) { if (parentLock.hasExpired()) { resourceLocks.remove(parentPath); } else { // parentPath == currentPath is a check for the first loop if (parentPath == path || parentLock.depth > 0) { if (parentLock.isExclusive()) { parentLock.toXML(generatedXML); } else { for (String lockToken : parentLock.sharedTokens) { LockInfo sharedLock = sharedLocks.get(lockToken); if (sharedLock != null) { if (sharedLock.hasExpired()) { sharedLocks.remove(lockToken); } else { if (parentPath == path || sharedLock.depth > 0) { sharedLock.toXML(generatedXML); } } } } } } } } int slash = parentPath.lastIndexOf('/'); if (slash < 0) { break; } parentPath = parentPath.substring(0, slash); } while (true); generatedXML.writeElement("D", "lockdiscovery", XMLWriter.CLOSING); } /** * Get creation date in ISO format. * * @return the formatted creation date */ private static String getISOCreationDate(long creationDate) { return creationDateFormat.format(new Date(creationDate)); } private static String getDAVNode(Node node) { if (DEFAULT_NAMESPACE.equals(node.getNamespaceURI())) { return node.getLocalName(); } return null; } private static boolean propertyEquals(Node node1, Node node2) { return node1.getLocalName().equals(node2.getLocalName()) && ((node1.getNamespaceURI() == null && node2.getNamespaceURI() == null) || (node1.getNamespaceURI() != null && node1.getNamespaceURI().equals(node2.getNamespaceURI()))); } // -------------------------------------------------- LockInfo Inner Class /** * Holds a lock information. */ private static class LockInfo implements Serializable { @Serial private static final long serialVersionUID = 1L; LockInfo(int maxDepth) { this.maxDepth = maxDepth; } // ------------------------------------------------- Instance Variables private final int maxDepth; String path = "/"; String lockroot = "/"; String type = "write"; String scope = "exclusive"; int depth = 0; String owner = ""; String token = ""; List<String> sharedTokens = new CopyOnWriteArrayList<>(); long expiresAt = 0; String principal = null; // ----------------------------------------------------- Public Methods @Override public String toString() { return "Type:" + type + " Scope:" + scope + " Depth:" + String.valueOf(depth) + " Owner:" + owner + " Expiration:" + FastHttpDateFormat.formatDate(expiresAt) + " Token:" + token + " "; } /** * @return true if the lock has expired. */ public boolean hasExpired() { return sharedTokens.isEmpty() && System.currentTimeMillis() > expiresAt; } /** * @return true if the lock is exclusive. */ public boolean isExclusive() { return scope.equals("exclusive"); } /** * Get an XML representation of this lock token. * * @param generatedXML The XML write to which the fragment will be appended */ public void toXML(XMLWriter generatedXML) { generatedXML.writeElement("D", "activelock", XMLWriter.OPENING); generatedXML.writeElement("D", "locktype", XMLWriter.OPENING); generatedXML.writeElement("D", type, XMLWriter.NO_CONTENT); generatedXML.writeElement("D", "locktype", XMLWriter.CLOSING); generatedXML.writeElement("D", "lockscope", XMLWriter.OPENING); generatedXML.writeElement("D", scope, XMLWriter.NO_CONTENT); generatedXML.writeElement("D", "lockscope", XMLWriter.CLOSING); generatedXML.writeElement("D", "depth", XMLWriter.OPENING); if (depth == maxDepth) { generatedXML.writeText("Infinity"); } else { generatedXML.writeText("0"); } generatedXML.writeElement("D", "depth", XMLWriter.CLOSING); generatedXML.writeElement("D", "owner", XMLWriter.OPENING); generatedXML.writeText(owner); generatedXML.writeElement("D", "owner", XMLWriter.CLOSING); generatedXML.writeElement("D", "timeout", XMLWriter.OPENING); long timeout = (expiresAt - System.currentTimeMillis()) / 1000; generatedXML.writeText("Second-" + timeout); generatedXML.writeElement("D", "timeout", XMLWriter.CLOSING); generatedXML.writeElement("D", "lockroot", XMLWriter.OPENING); generatedXML.writeElement("D", "href", XMLWriter.OPENING); generatedXML.writeText(lockroot); generatedXML.writeElement("D", "href", XMLWriter.CLOSING); generatedXML.writeElement("D", "lockroot", XMLWriter.CLOSING); generatedXML.writeElement("D", "locktoken", XMLWriter.OPENING); generatedXML.writeElement("D", "href", XMLWriter.OPENING); generatedXML.writeText(LOCK_SCHEME + token); generatedXML.writeElement("D", "href", XMLWriter.CLOSING); generatedXML.writeElement("D", "locktoken", XMLWriter.CLOSING); generatedXML.writeElement("D", "activelock", XMLWriter.CLOSING); } } // --------------------------------------------- WebdavResolver Inner Class /** * Work around for XML parsers that don't fully respect * {@link DocumentBuilderFactory#setExpandEntityReferences(boolean)} when called with <code>false</code>. External * references are filtered out for security reasons. See CVE-2007-5461. */ private static class WebdavResolver implements EntityResolver { private final ServletContext context; WebdavResolver(ServletContext theContext) { context = theContext; } @Override public InputSource resolveEntity(String publicId, String systemId) { context.log(sm.getString("webdavservlet.externalEntityIgnored", publicId, systemId)); return new InputSource(new StringReader("Ignored external entity")); } } // ------------------------------------- TransientPropertyStore Inner Class /** * Default property store, which provides memory storage without persistence. */ public static class MemoryPropertyStore implements PropertyStore { private final ConcurrentHashMap<String,ArrayList<Node>> deadProperties = new ConcurrentHashMap<>(); @Override public void init() { } @Override public void destroy() { } @Override public void periodicEvent() { } @Override public void copy(String source, String destination) { ArrayList<Node> properties = deadProperties.get(source); ArrayList<Node> propertiesDest = deadProperties.get(destination); if (properties != null) { if (propertiesDest == null) { propertiesDest = new ArrayList<>(); deadProperties.put(destination, propertiesDest); } synchronized (properties) { synchronized (propertiesDest) { for (Node node : properties) { node = node.cloneNode(true); boolean found = false; for (int i = 0; i < propertiesDest.size(); i++) { Node propertyNode = propertiesDest.get(i); if (propertyEquals(node, propertyNode)) { found = true; propertiesDest.set(i, node); break; } } if (!found) { propertiesDest.add(node); } } } } } } @Override public void delete(String resource) { deadProperties.remove(resource); } @Override public boolean propfind(String resource, Node property, boolean nameOnly, XMLWriter generatedXML) { ArrayList<Node> properties = deadProperties.get(resource); if (properties != null) { synchronized (properties) { if (nameOnly) { // Add the names of all properties for (Node node : properties) { generatedXML.writeElement(null, node.getNamespaceURI(), node.getLocalName(), XMLWriter.NO_CONTENT); } } else if (property != null) { // Add a single property Node foundNode = null; for (Node node : properties) { if (propertyEquals(node, property)) { foundNode = node; } } if (foundNode != null) { StringWriter strWriter = new StringWriter(); DOMWriter domWriter = new DOMWriter(strWriter); domWriter.print(foundNode); generatedXML.writeRaw(strWriter.toString()); return true; } } else { StringWriter strWriter = new StringWriter(); DOMWriter domWriter = new DOMWriter(strWriter); // Add all properties for (Node node : properties) { domWriter.print(node); } generatedXML.writeRaw(strWriter.toString()); } } } return false; } @Override public void proppatch(String resource, ArrayList<ProppatchOperation> operations) { boolean protectedProperty = false; // Check for the protected properties for (ProppatchOperation operation : operations) { if (operation.getProtectedProperty()) { protectedProperty = true; operation.setStatusCode(HttpServletResponse.SC_FORBIDDEN); } } if (protectedProperty) { for (ProppatchOperation operation : operations) { if (!operation.getProtectedProperty()) { operation.setStatusCode(WebdavStatus.SC_FAILED_DEPENDENCY); } } } else { ArrayList<Node> properties = deadProperties.computeIfAbsent(resource, k -> new ArrayList<>()); synchronized (properties) { for (ProppatchOperation operation : operations) { if (operation.getUpdateType() == PropertyUpdateType.SET) { Node node = operation.getPropertyNode().cloneNode(true); boolean found = false; for (int i = 0; i < properties.size(); i++) { Node propertyNode = properties.get(i); if (propertyEquals(node, propertyNode)) { found = true; properties.set(i, node); break; } } if (!found) { properties.add(node); } } if (operation.getUpdateType() == PropertyUpdateType.REMOVE) { Node node = operation.getPropertyNode(); for (int i = 0; i < properties.size(); i++) { Node propertyNode = properties.get(i); if (propertyEquals(node, propertyNode)) { properties.remove(i); break; } } } } } } } } } // -------------------------------------------------------- WebdavStatus Class /** * Wraps the HttpServletResponse class to abstract the specific protocol used. To support other protocols we would only * need to modify this class and the WebDavRetCode classes. */ class WebdavStatus { // ------------------------------------------------------ HTTP Status Codes /** * Status code (200) indicating the request succeeded normally. */ public static final int SC_OK = HttpServletResponse.SC_OK; /** * Status code (201) indicating the request succeeded and created a new resource on the server. */ public static final int SC_CREATED = HttpServletResponse.SC_CREATED; /** * Status code (202) indicating that a request was accepted for processing, but was not completed. */ public static final int SC_ACCEPTED = HttpServletResponse.SC_ACCEPTED; /** * Status code (204) indicating that the request succeeded but that there was no new information to return. */ public static final int SC_NO_CONTENT = HttpServletResponse.SC_NO_CONTENT; /** * Status code (301) indicating that the resource has permanently moved to a new location, and that future * references should use a new URI with their requests. */ public static final int SC_MOVED_PERMANENTLY = HttpServletResponse.SC_MOVED_PERMANENTLY; /** * Status code (302) indicating that the resource has temporarily moved to another location, but that future * references should still use the original URI to access the resource. */ public static final int SC_MOVED_TEMPORARILY = HttpServletResponse.SC_MOVED_TEMPORARILY; /** * Status code (304) indicating that a conditional GET operation found that the resource was available and not * modified. */ public static final int SC_NOT_MODIFIED = HttpServletResponse.SC_NOT_MODIFIED; /** * Status code (400) indicating the request sent by the client was syntactically incorrect. */ public static final int SC_BAD_REQUEST = HttpServletResponse.SC_BAD_REQUEST; /** * Status code (401) indicating that the request requires HTTP authentication. */ public static final int SC_UNAUTHORIZED = HttpServletResponse.SC_UNAUTHORIZED; /** * Status code (403) indicating the server understood the request but refused to fulfill it. */ public static final int SC_FORBIDDEN = HttpServletResponse.SC_FORBIDDEN; /** * Status code (404) indicating that the requested resource is not available. */ public static final int SC_NOT_FOUND = HttpServletResponse.SC_NOT_FOUND; /** * Status code (500) indicating an error inside the HTTP service which prevented it from fulfilling the request. */ public static final int SC_INTERNAL_SERVER_ERROR = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; /** * Status code (501) indicating the HTTP service does not support the functionality needed to fulfill the request. */ public static final int SC_NOT_IMPLEMENTED = HttpServletResponse.SC_NOT_IMPLEMENTED; /** * Status code (502) indicating that the HTTP server received an invalid response from a server it consulted when * acting as a proxy or gateway. */ public static final int SC_BAD_GATEWAY = HttpServletResponse.SC_BAD_GATEWAY; /** * Status code (503) indicating that the HTTP service is temporarily overloaded, and unable to handle the request. */ public static final int SC_SERVICE_UNAVAILABLE = HttpServletResponse.SC_SERVICE_UNAVAILABLE; /** * Status code (100) indicating the client may continue with its request. This interim response is used to inform * the client that the initial part of the request has been received and has not yet been rejected by the server. */ public static final int SC_CONTINUE = HttpServletResponse.SC_CONTINUE; /** * Status code (405) indicating the method specified is not allowed for the resource. */ public static final int SC_METHOD_NOT_ALLOWED = HttpServletResponse.SC_METHOD_NOT_ALLOWED; /** * Status code (409) indicating that the request could not be completed due to a conflict with the current state of * the resource. */ public static final int SC_CONFLICT = HttpServletResponse.SC_CONFLICT; /** * Status code (412) indicating the precondition given in one or more of the request-header fields evaluated to * false when it was tested on the server. */ public static final int SC_PRECONDITION_FAILED = HttpServletResponse.SC_PRECONDITION_FAILED; /** * Status code (413) indicating the server is refusing to process a request because the request entity is larger * than the server is willing or able to process. */ public static final int SC_REQUEST_TOO_LONG = HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE; /** * Status code (415) indicating the server is refusing to service the request because the entity of the request is * in a format not supported by the requested resource for the requested method. */ public static final int SC_UNSUPPORTED_MEDIA_TYPE = HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE; // -------------------------------------------- Extended WebDav status code /** * Status code (207) indicating that the response requires providing status for multiple independent operations. */ public static final int SC_MULTI_STATUS = 207; /** * Status code (422) indicating that the server understands the content type of the request but is unable to process * the contained instructions. */ public static final int SC_UNPROCESSABLE_CONTENT = HttpServletResponse.SC_UNPROCESSABLE_CONTENT; /** * Status code (420) indicating the method was not executed on a particular resource within its scope because some * part of the method's execution failed causing the entire method to be aborted. */ public static final int SC_METHOD_FAILURE = 420; /** * Status code (423) indicating the destination resource of a method is locked, and either the request did not * contain a valid Lock-Info header, or the Lock-Info header identifies a lock held by another principal. */ public static final int SC_LOCKED = 423; /** * Status code (424) indicating that another dependent operation failed. */ public static final int SC_FAILED_DEPENDENCY = 424; /** * Status code (507) indicating that the server does not have enough storage to complete the operation. */ public static final int SC_INSUFFICIENT_STORAGE = 507; }
Detected license expression
apache-2.0
Detected license expression (SPDX)
Apache-2.0
Percentage of license text
1.16
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://tools.ietf.org/html/rfc4918 79 79
https://tools.ietf.org/html/rfc4918#section-18 80 80