diff --git a/src/main/java/com/jmal/clouddisk/webdav/BasicAuthenticator.java b/src/main/java/com/jmal/clouddisk/webdav/BasicAuthenticator.java new file mode 100644 index 00000000..416c3a78 --- /dev/null +++ b/src/main/java/com/jmal/clouddisk/webdav/BasicAuthenticator.java @@ -0,0 +1,263 @@ +package com.jmal.clouddisk.webdav; + +import com.jmal.clouddisk.config.FileProperties; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.catalina.authenticator.AuthenticatorBase; +import org.apache.catalina.connector.Request; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.codec.binary.Base64; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.Principal; + +@Component +@Slf4j +@RequiredArgsConstructor +public class BasicAuthenticator extends AuthenticatorBase { + + private Charset charset = StandardCharsets.ISO_8859_1; + private String charsetString = null; + private boolean trimCredentials = true; + + private final FileProperties fileProperties; + + + public String getCharset() { + return charsetString; + } + + + public void setCharset(String charsetString) { + // Only acceptable options are null, "" or "UTF-8" (case insensitive) + if (charsetString == null || charsetString.isEmpty()) { + charset = StandardCharsets.ISO_8859_1; + } else if ("UTF-8".equalsIgnoreCase(charsetString)) { + charset = StandardCharsets.UTF_8; + } else { + throw new IllegalArgumentException(sm.getString("basicAuthenticator.invalidCharset")); + } + this.charsetString = charsetString; + } + + + public boolean getTrimCredentials() { + return trimCredentials; + } + + + public void setTrimCredentials(boolean trimCredentials) { + this.trimCredentials = trimCredentials; + } + + + @Override + protected boolean doAuthenticate(Request request, HttpServletResponse response) throws IOException { + + if (checkForCachedAuthentication(request, response, true)) { + return true; + } + + // Validate any credentials already included with this request + MessageBytes authorization = request.getCoyoteRequest().getMimeHeaders().getValue("authorization"); + + if (authorization != null) { + authorization.toBytes(); + ByteChunk authorizationBC = authorization.getByteChunk(); + BasicCredentials credentials = null; + try { + credentials = new BasicCredentials(authorizationBC, charset, getTrimCredentials()); + String username = credentials.getUsername(); + String usernameByUri = MyRealm.getUsernameByUri(fileProperties.getWebDavPrefixPath(), request.getRequestURI()); + if (usernameByUri == null || !usernameByUri.equals(username)) { + return false; + } + String password = credentials.getPassword(); + + Principal principal = context.getRealm().authenticate(username, password); + if (principal != null) { + register(request, response, principal, HttpServletRequest.BASIC_AUTH, username, password); + return true; + } + } catch (IllegalArgumentException iae) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("basicAuthenticator.invalidAuthorization", iae.getMessage())); + } + } + } else { + if (WebdavMethod.GET.getCode().equals(request.getMethod())) { + String userAgent = request.getHeader("User-Agent"); + if (userAgent != null && userAgent.contains("Mozilla")) { + notAllowBrowser(response); + return false; + } + } + } + + // the request could not be authenticated, so reissue the challenge + StringBuilder value = new StringBuilder(16); + value.append("Basic realm=\""); + value.append(getRealmName(context)); + value.append('\"'); + if (charsetString != null && !charsetString.isEmpty()) { + value.append(", charset="); + value.append(charsetString); + } + response.setHeader(AUTH_HEADER_NAME, value.toString()); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return false; + + } + + /** + * 不允许浏览器访问webDAV + */ + private void notAllowBrowser(HttpServletResponse resp) throws IOException { + resp.setHeader("Content-type", "text/html;charset=UTF-8"); + resp.getWriter().print("

这是 WebDAV 接口。请使用 WebDAV 客户端访问。


"); + resp.getWriter().println("Windows : 文件资源管理器 或者 RaiDrive
"); + resp.getWriter().println("Mac OS : Finder 或者 Cyberduck
"); + resp.getWriter().println("Android : RS 文件浏览器
"); + resp.getWriter().println("iOS : Documents
"); + resp.getWriter().close(); + } + + + @Override + protected String getAuthMethod() { + return HttpServletRequest.BASIC_AUTH; + } + + + @Override + protected boolean isPreemptiveAuthPossible(Request request) { + MessageBytes authorizationHeader = request.getCoyoteRequest().getMimeHeaders().getValue("authorization"); + return authorizationHeader != null && authorizationHeader.startsWithIgnoreCase("basic ", 0); + } + + + /** + * Parser for an HTTP Authorization header for BASIC authentication as per RFC 2617 section 2, and the Base64 + * encoded credentials as per RFC 2045 section 6.8. + */ + public static class BasicCredentials { + + // the only authentication method supported by this parser + // note: we include single white space as its delimiter + private static final String METHOD = "basic "; + + private final Charset charset; + private final boolean trimCredentials; + private final ByteChunk authorization; + private final int initialOffset; + private int base64blobOffset; + private int base64blobLength; + + /** + * -- GETTER -- + * Trivial accessor. + * + * @return the decoded username token as a String, which is never be null, but can be empty. + */ + @Getter + private String username = null; + /** + * -- GETTER -- + * Trivial accessor. + * + * @return the decoded password token as a String, or null if no password was found in the + * credentials. + */ + @Getter + private String password = null; + + /** + * Parse the HTTP Authorization header for BASIC authentication as per RFC 2617 section 2, and the Base64 + * encoded credentials as per RFC 2045 section 6.8. + * + * @param input The header value to parse in-place + * @param charset The character set to use to convert the bytes to a string + * @param trimCredentials Should leading and trailing whitespace be removed from the parsed credentials + * + * @throws IllegalArgumentException If the header does not conform to RFC 2617 + */ + public BasicCredentials(ByteChunk input, Charset charset, boolean trimCredentials) + throws IllegalArgumentException { + authorization = input; + initialOffset = input.getOffset(); + this.charset = charset; + this.trimCredentials = trimCredentials; + + parseMethod(); + byte[] decoded = parseBase64(); + parseCredentials(decoded); + } + + /* + * The authorization method string is case-insensitive and must hae at least one space character as a delimiter. + */ + private void parseMethod() throws IllegalArgumentException { + if (authorization.startsWithIgnoreCase(METHOD, 0)) { + // step past the auth method name + base64blobOffset = initialOffset + METHOD.length(); + base64blobLength = authorization.getLength() - METHOD.length(); + } else { + // is this possible, or permitted? + throw new IllegalArgumentException(sm.getString("basicAuthenticator.notBasic")); + } + } + + /* + * Decode the base64-user-pass token, which RFC 2617 states can be longer than the 76 characters per line limit + * defined in RFC 2045. The base64 decoder will ignore embedded line break characters as well as surplus + * surrounding white space. + */ + private byte[] parseBase64() throws IllegalArgumentException { + byte[] decoded = Base64.decodeBase64(authorization.getBuffer(), base64blobOffset, base64blobLength); + // restore original offset + authorization.setOffset(initialOffset); + if (decoded == null) { + throw new IllegalArgumentException(sm.getString("basicAuthenticator.notBase64")); + } + return decoded; + } + + /* + * Extract the mandatory username token and separate it from the optional password token. Tolerate surplus + * surrounding white space. + */ + private void parseCredentials(byte[] decoded) throws IllegalArgumentException { + + int colon = -1; + for (int i = 0; i < decoded.length; i++) { + if (decoded[i] == ':') { + colon = i; + break; + } + } + + if (colon < 0) { + username = new String(decoded, charset); + // password will remain null! + } else { + username = new String(decoded, 0, colon, charset); + password = new String(decoded, colon + 1, decoded.length - colon - 1, charset); + // tolerate surplus white space around credentials + if (password.length() > 1 && trimCredentials) { + password = password.trim(); + } + } + // tolerate surplus white space around credentials + if (username.length() > 1 && trimCredentials) { + username = username.trim(); + } + } + } +} diff --git a/src/main/java/com/jmal/clouddisk/webdav/MyRealm.java b/src/main/java/com/jmal/clouddisk/webdav/MyRealm.java index c24778fe..4e6c0eb5 100644 --- a/src/main/java/com/jmal/clouddisk/webdav/MyRealm.java +++ b/src/main/java/com/jmal/clouddisk/webdav/MyRealm.java @@ -1,5 +1,6 @@ package com.jmal.clouddisk.webdav; +import cn.hutool.core.util.StrUtil; import com.jmal.clouddisk.config.FileProperties; import com.jmal.clouddisk.config.WebFilter; import com.jmal.clouddisk.service.impl.UserServiceImpl; @@ -62,6 +63,9 @@ protected Principal getPrincipal(String username) { @Override public Principal authenticate(String username, String password) { String hashPassword = userService.getHashPasswordUserName(username); + if (StrUtil.isBlank(hashPassword)) { + return null; + } boolean valid = PasswordHash.validatePassword(password, hashPassword); return valid ? getPrincipal(username) : null; } diff --git a/src/main/java/com/jmal/clouddisk/webdav/MyWebdavServlet.java b/src/main/java/com/jmal/clouddisk/webdav/MyWebdavServlet.java index 1a854e63..834d8990 100644 --- a/src/main/java/com/jmal/clouddisk/webdav/MyWebdavServlet.java +++ b/src/main/java/com/jmal/clouddisk/webdav/MyWebdavServlet.java @@ -20,7 +20,6 @@ import lombok.RequiredArgsConstructor; import org.apache.catalina.WebResource; import org.apache.catalina.connector.ClientAbortException; -import org.apache.catalina.servlets.WebdavServlet; import org.apache.tomcat.util.http.parser.Ranges; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Component; diff --git a/src/main/java/com/jmal/clouddisk/webdav/WebdavAuthenticator.java b/src/main/java/com/jmal/clouddisk/webdav/WebdavAuthenticator.java index ac45f825..8f37ac85 100644 --- a/src/main/java/com/jmal/clouddisk/webdav/WebdavAuthenticator.java +++ b/src/main/java/com/jmal/clouddisk/webdav/WebdavAuthenticator.java @@ -5,7 +5,6 @@ import com.jmal.clouddisk.service.impl.LogService; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.apache.catalina.authenticator.BasicAuthenticator; import org.apache.catalina.connector.Request; import org.springframework.stereotype.Component; @@ -33,7 +32,7 @@ public class WebdavAuthenticator extends BasicAuthenticator { private final LogService logService; public WebdavAuthenticator(FileProperties fileProperties, LogService logService) { - super(); + super(fileProperties); this.fileProperties = fileProperties; this.logService = logService; } @@ -79,17 +78,4 @@ private void recordLog(HttpServletRequest request, HttpServletResponse response, logService.addLogBefore(logOperation, null, request, response); } - /** - * 不允许浏览器访问webDAV - */ - private void notAllowBrowser(HttpServletResponse resp) throws IOException { - resp.setHeader("Content-type", "text/html;charset=UTF-8"); - resp.getWriter().print("

这是 WebDAV 接口。请使用 WebDAV 客户端访问。


"); - resp.getWriter().println("Windows : 文件资源管理器 或者 RaiDrive
"); - resp.getWriter().println("Mac OS : Finder 或者 Cyberduck
"); - resp.getWriter().println("Android : RS 文件浏览器
"); - resp.getWriter().println("iOS : Documents
"); - resp.getWriter().close(); - } - } diff --git a/src/main/java/com/jmal/clouddisk/webdav/WebdavServlet.java b/src/main/java/com/jmal/clouddisk/webdav/WebdavServlet.java new file mode 100644 index 00000000..044cf9d1 --- /dev/null +++ b/src/main/java/com/jmal/clouddisk/webdav/WebdavServlet.java @@ -0,0 +1,2500 @@ +/* + * 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 com.jmal.clouddisk.webdav; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +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.servlets.DefaultServlet; +import org.apache.catalina.util.DOMWriter; +import org.apache.catalina.util.XMLWriter; +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.http.ConcurrentDateFormat; +import org.apache.tomcat.util.http.FastHttpDateFormat; +import org.apache.tomcat.util.http.RequestUtil; +import org.apache.tomcat.util.security.ConcurrentMessageDigest; +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; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.*; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Servlet which adds support for WebDAV + * level 2. All the basic HTTP requests are handled by the + * DefaultServlet. The WebDAVServlet must not be used as the default servlet (ie mapped to '/') as it will not work in + * this configuration. + *

+ * Mapping a subpath (e.g. /webdav/* to this servlet has the effect of re-mounting the entire web + * application under that sub-path, with WebDAV access to all the resources. The WEB-INF and + * META-INF directories are protected in this re-mounted resource tree. + *

+ * To enable WebDAV for a context add the following to web.xml: + * + *

+ * <servlet>
+ *  <servlet-name>webdav</servlet-name>
+ *  <servlet-class>org.apache.catalina.servlets.WebdavServlet</servlet-class>
+ *    <init-param>
+ *      <param-name>debug</param-name>
+ *      <param-value>0</param-value>
+ *    </init-param>
+ *    <init-param>
+ *      <param-name>listings</param-name>
+ *      <param-value>false</param-value>
+ *    </init-param>
+ *  </servlet>
+ *  <servlet-mapping>
+ *    <servlet-name>webdav</servlet-name>
+ *    <url-pattern>/*</url-pattern>
+ *  </servlet-mapping>
+ * 
+ * + * This will enable read only access. To enable read-write access add: + * + *
+ *  <init-param>
+ *    <param-name>readonly</param-name>
+ *    <param-value>false</param-value>
+ *  </init-param>
+ * 
+ * + * To make the content editable via a different URL, use the following mapping: + * + *
+ *  <servlet-mapping>
+ *    <servlet-name>webdav</servlet-name>
+ *    <url-pattern>/webdavedit/*</url-pattern>
+ *  </servlet-mapping>
+ * 
+ * + * By default access to /WEB-INF and META-INF are not available via WebDAV. To enable access to these URLs, use add: + * + *
+ *  <init-param>
+ *    <param-name>allowSpecialPaths</param-name>
+ *    <param-value>true</param-value>
+ *  </init-param>
+ * 
+ * + * 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 + * + * @author Remy Maucherat + * + * @see RFC 4918 + */ +public class WebdavServlet extends DefaultServlet { + + private static final long serialVersionUID = 1L; + + + // -------------------------------------------------------------- Constants + + private static final String METHOD_PROPFIND = "PROPFIND"; + private static final String METHOD_PROPPATCH = "PROPPATCH"; + private static final String METHOD_MKCOL = "MKCOL"; + private static final String METHOD_COPY = "COPY"; + private static final String METHOD_MOVE = "MOVE"; + private static final String METHOD_LOCK = "LOCK"; + private static final String METHOD_UNLOCK = "UNLOCK"; + + + /** + * PROPFIND - Specify a property mask. + */ + private static final int FIND_BY_PROPERTY = 0; + + + /** + * PROPFIND - Display all properties. + */ + private static final int FIND_ALL_PROP = 1; + + + /** + * PROPFIND - Return property names. + */ + private static final int FIND_PROPERTY_NAMES = 2; + + + /** + * Create a new lock. + */ + private static final int LOCK_CREATION = 0; + + + /** + * Refresh lock. + */ + private static final int LOCK_REFRESH = 1; + + + /** + * Default lock timeout value. + */ + private static final int DEFAULT_TIMEOUT = 3600; + + + /** + * Maximum lock timeout. + */ + private static final int MAX_TIMEOUT = 604800; + + + /** + * Default namespace. + */ + protected static final String DEFAULT_NAMESPACE = "DAV:"; + + + /** + * 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")); + + + // ----------------------------------------------------- Instance Variables + + /** + * Repository of the locks put on single resources. + *

+ * Key : path
+ * Value : LockInfo + */ + private final Map resourceLocks = new ConcurrentHashMap<>(); + + + /** + * Repository of the lock-null resources. + *

+ * Key : path of the collection containing the lock-null resource
+ * Value : List of lock-null resource which are members of the collection. Each element of the List is the path + * associated with the lock-null resource. + */ + private final Map> lockNullResources = new ConcurrentHashMap<>(); + + + /** + * List of the inheritable collection locks. + */ + private final List collectionLocks = Collections.synchronizedList(new ArrayList<>()); + + + /** + * Secret information used to generate reasonably secure lock ids. + */ + private String secret = "catalina"; + + + /** + * Default depth in spec is infinite. Limit depth to 3 by default as infinite depth makes operations very expensive. + */ + private int maxDepth = 3; + + + /** + * Is access allowed via WebDAV to the special paths (/WEB-INF and /META-INF)? + */ + private boolean allowSpecialPaths = false; + + + // --------------------------------------------------------- Public Methods + + /** + * Initialize this servlet. + */ + @Override + public void init() throws ServletException { + + super.init(); + + if (getServletConfig().getInitParameter("secret") != null) { + secret = getServletConfig().getInitParameter("secret"); + } + + if (getServletConfig().getInitParameter("maxDepth") != null) { + maxDepth = Integer.parseInt(getServletConfig().getInitParameter("maxDepth")); + } + + if (getServletConfig().getInitParameter("allowSpecialPaths") != null) { + allowSpecialPaths = Boolean.parseBoolean(getServletConfig().getInitParameter("allowSpecialPaths")); + } + } + + + // ------------------------------------------------------ Protected Methods + + /** + * Return JAXP document builder instance. + * + * @return the document builder + * + * @throws ServletException document builder creation failed (wrapped ParserConfigurationException + * exception) + */ + protected DocumentBuilder getDocumentBuilder() throws ServletException { + DocumentBuilder documentBuilder = null; + DocumentBuilderFactory documentBuilderFactory = null; + try { + 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; + } + + + /** + * Handles the special WebDAV methods. + */ + @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); + } + + if (method.equals(METHOD_PROPFIND)) { + doPropfind(req, resp); + } else if (method.equals(METHOD_PROPPATCH)) { + doProppatch(req, resp); + } else if (method.equals(METHOD_MKCOL)) { + doMkcol(req, resp); + } else if (method.equals(METHOD_COPY)) { + doCopy(req, resp); + } else if (method.equals(METHOD_MOVE)) { + doMove(req, resp); + } else if (method.equals(METHOD_LOCK)) { + doLock(req, resp); + } else if (method.equals(METHOD_UNLOCK)) { + doUnlock(req, resp); + } else { + // DefaultServlet processing + super.service(req, resp); + } + } + + + /** + * Checks whether a given path refers to a resource under WEB-INF or META-INF. + * + * @param path the full path of the resource being accessed + * + * @return true if the resource specified is under a special path + */ + private boolean isSpecialPath(final String path) { + return !allowSpecialPaths && (path.toUpperCase(Locale.ENGLISH).startsWith("/WEB-INF") || + path.toUpperCase(Locale.ENGLISH).startsWith("/META-INF")); + } + + + @Override + protected boolean checkIfHeaders(HttpServletRequest request, HttpServletResponse response, WebResource resource) + throws IOException { + + if (!super.checkIfHeaders(request, response, resource)) { + return false; + } + + // TODO : Checking the WebDAV If header + 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 + */ + @Override + protected String getRelativePath(HttpServletRequest request) { + return getRelativePath(request, false); + } + + @Override + protected String getRelativePath(HttpServletRequest request, boolean 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.length() == 0) { + result.append('/'); + } + + return result.toString(); + } + + + /** + * Determines the prefix for standard directory GET listings. + */ + @Override + protected String getPathPrefix(final HttpServletRequest 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; + } + + + /** + * OPTIONS Method. + * + * @param req The Servlet request + * @param resp The Servlet response + * + * @throws ServletException If an error occurs + * @throws IOException If an IO error occurs + */ + @Override + protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.addHeader("DAV", "1,2"); + 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 (!listings) { + sendNotAllowed(req, resp); + return; + } + + String path = getRelativePath(req); + if (path.length() > 1 && path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + + // Properties which are to be displayed. + List properties = null; + // Propfind depth + int depth = maxDepth; + // Propfind type + int type = FIND_ALL_PROP; + + String depthStr = req.getHeader("Depth"); + + if (depthStr == null) { + depth = maxDepth; + } else { + if (depthStr.equals("0")) { + depth = 0; + } else if (depthStr.equals("1")) { + depth = 1; + } else if (depthStr.equals("infinity")) { + depth = maxDepth; + } + } + + Node propNode = null; + + if (req.getContentLengthLong() > 0) { + DocumentBuilder documentBuilder = getDocumentBuilder(); + + try { + Document document = documentBuilder.parse(new InputSource(req.getInputStream())); + + // Get the root element of the document + Element rootElement = document.getDocumentElement(); + 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: + if (currentNode.getNodeName().endsWith("prop")) { + type = FIND_BY_PROPERTY; + propNode = currentNode; + } + if (currentNode.getNodeName().endsWith("propname")) { + type = FIND_PROPERTY_NAMES; + } + if (currentNode.getNodeName().endsWith("allprop")) { + type = FIND_ALL_PROP; + } + break; + } + } + } catch (SAXException | IOException e) { + // Something went wrong - bad request + resp.sendError(WebdavStatus.SC_BAD_REQUEST); + return; + } + } + + if (type == FIND_BY_PROPERTY) { + properties = new ArrayList<>(); + // propNode must be non-null if type == FIND_BY_PROPERTY + @SuppressWarnings("null") + NodeList childList = propNode.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 = currentNode.getNodeName(); + String propertyName = null; + if (nodeName.indexOf(':') != -1) { + propertyName = nodeName.substring(nodeName.indexOf(':') + 1); + } else { + propertyName = nodeName; + } + // href is a live property which is handled differently + properties.add(propertyName); + break; + } + } + } + + WebResource resource = resources.getResource(path); + + if (!resource.exists()) { + int slash = path.lastIndexOf('/'); + if (slash != -1) { + String parentPath = path.substring(0, slash); + List currentLockNullResources = lockNullResources.get(parentPath); + if (currentLockNullResources != null) { + for (String lockNullPath : currentLockNullResources) { + if (lockNullPath.equals(path)) { + 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); + parseLockNullProperties(req, generatedXML, lockNullPath, type, properties); + generatedXML.writeElement("D", "multistatus", XMLWriter.CLOSING); + generatedXML.sendData(); + 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) { + parseProperties(req, generatedXML, path, type, properties); + } else { + // The stack always contains the object of the current level + Deque stack = new ArrayDeque<>(); + stack.addFirst(path); + + // Stack of the objects one level below + Deque stackBelow = new ArrayDeque<>(); + + while ((!stack.isEmpty()) && (depth >= 0)) { + + String currentPath = stack.remove(); + parseProperties(req, generatedXML, currentPath, type, properties); + + resource = resources.getResource(currentPath); + + 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); + } + + // Displaying the lock-null resources present in that + // collection + String lockPath = currentPath; + if (lockPath.endsWith("/")) { + lockPath = lockPath.substring(0, lockPath.length() - 1); + } + List currentLockNullResources = lockNullResources.get(lockPath); + if (currentLockNullResources != null) { + for (String lockNullPath : currentLockNullResources) { + parseLockNullProperties(req, generatedXML, lockNullPath, type, properties); + } + } + } + + if (stack.isEmpty()) { + depth--; + stack = stackBelow; + stackBelow = new ArrayDeque<>(); + } + + generatedXML.sendData(); + + } + } + + generatedXML.writeElement("D", "multistatus", XMLWriter.CLOSING); + + generatedXML.sendData(); + + } + + + /** + * PROPPATCH Method. + * + * @param req The Servlet request + * @param resp The Servlet response + * + * @throws IOException If an IO error occurs + */ + protected void doProppatch(HttpServletRequest req, HttpServletResponse resp) throws IOException { + + if (readOnly) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } + + if (isLocked(req)) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } + + resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); + } + + + /** + * 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); + + // Can't create a collection if a resource already exists at the given + // path + if (resource.exists()) { + sendNotAllowed(req, resp); + return; + } + + if (readOnly) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } + + if (isLocked(req)) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } + + if (req.getContentLengthLong() > 0) { + DocumentBuilder documentBuilder = getDocumentBuilder(); + try { + // Document document = + documentBuilder.parse(new InputSource(req.getInputStream())); + // TODO : Process this request body + resp.sendError(WebdavStatus.SC_NOT_IMPLEMENTED); + return; + + } catch (SAXException saxe) { + // Parse error - assume invalid content + resp.sendError(WebdavStatus.SC_UNSUPPORTED_MEDIA_TYPE); + return; + } + } + + if (resources.mkdir(path)) { + resp.setStatus(WebdavStatus.SC_CREATED); + // Removing any lock-null resource which would be present + lockNullResources.remove(path); + } else { + resp.sendError(WebdavStatus.SC_CONFLICT); + } + } + + + /** + * DELETE Method. + * + * @param req The Servlet request + * @param resp The Servlet response + * + * @throws ServletException If an error occurs + * @throws IOException If an IO error occurs + */ + @Override + protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + if (readOnly) { + sendNotAllowed(req, resp); + return; + } + + if (isLocked(req)) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } + + deleteResource(req, resp); + } + + + /** + * Process a PUT request for the specified resource. + * + * @param req The servlet request we are processing + * @param resp The servlet response we are creating + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet-specified error occurs + */ + @Override + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + if (isLocked(req)) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } + + String path = getRelativePath(req); + WebResource resource = resources.getResource(path); + if (resource.isDirectory()) { + sendNotAllowed(req, resp); + return; + } + + super.doPut(req, resp); + + // Removing any lock-null resource which would be present + lockNullResources.remove(path); + } + + + /** + * 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 (readOnly) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } + + copyResource(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 (readOnly) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } + + if (isLocked(req)) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } + + String path = getRelativePath(req); + + if (copyResource(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 (readOnly) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } + + if (isLocked(req)) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } + + LockInfo lock = new LockInfo(maxDepth); + + // 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 { + lock.depth = maxDepth; + } + } + + // Parsing timeout header + + int lockDuration = DEFAULT_TIMEOUT; + String lockDurationStr = req.getHeader("Timeout"); + if (lockDurationStr != null) { + int commaPos = lockDurationStr.indexOf(','); + // If multiple timeouts, just use the first + if (commaPos != -1) { + lockDurationStr = lockDurationStr.substring(0, commaPos); + } + if (lockDurationStr.startsWith("Second-")) { + lockDuration = Integer.parseInt(lockDurationStr.substring(7)); + } else { + if (lockDurationStr.equalsIgnoreCase("infinity")) { + lockDuration = MAX_TIMEOUT; + } else { + try { + lockDuration = Integer.parseInt(lockDurationStr); + } catch (NumberFormatException e) { + lockDuration = MAX_TIMEOUT; + } + } + } + if (lockDuration == 0) { + lockDuration = DEFAULT_TIMEOUT; + } + if (lockDuration > MAX_TIMEOUT) { + lockDuration = MAX_TIMEOUT; + } + } + lock.expiresAt = System.currentTimeMillis() + (lockDuration * 1000); + + int lockRequestType = LOCK_CREATION; + + Node lockInfoNode = null; + + DocumentBuilder documentBuilder = getDocumentBuilder(); + + try { + Document document = documentBuilder.parse(new InputSource(req.getInputStream())); + + // Get the root element of the document + Element rootElement = document.getDocumentElement(); + lockInfoNode = rootElement; + } catch (IOException | SAXException e) { + lockRequestType = LOCK_REFRESH; + } + + if (lockInfoNode != null) { + + // Reading lock information + + NodeList childList = lockInfoNode.getChildNodes(); + StringWriter strWriter = null; + DOMWriter domWriter = null; + + 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: + String nodeName = currentNode.getNodeName(); + if (nodeName.endsWith("lockscope")) { + lockScopeNode = currentNode; + } + if (nodeName.endsWith("locktype")) { + lockTypeNode = currentNode; + } + if (nodeName.endsWith("owner")) { + 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: + String tempScope = currentNode.getNodeName(); + if (tempScope.indexOf(':') != -1) { + lock.scope = tempScope.substring(tempScope.indexOf(':') + 1); + } else { + lock.scope = tempScope; + } + break; + } + } + + if (lock.scope == null) { + // Bad request + resp.setStatus(WebdavStatus.SC_BAD_REQUEST); + } + + } else { + // Bad request + resp.setStatus(WebdavStatus.SC_BAD_REQUEST); + } + + 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: + String tempType = currentNode.getNodeName(); + if (tempType.indexOf(':') != -1) { + lock.type = tempType.substring(tempType.indexOf(':') + 1); + } else { + lock.type = tempType; + } + break; + } + } + + if (lock.type == null) { + // Bad request + resp.setStatus(WebdavStatus.SC_BAD_REQUEST); + } + + } else { + // Bad request + resp.setStatus(WebdavStatus.SC_BAD_REQUEST); + } + + 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.setStatus(WebdavStatus.SC_BAD_REQUEST); + } + + } else { + lock.owner = ""; + } + } + + String path = getRelativePath(req); + + lock.path = path; + + WebResource resource = resources.getResource(path); + + if (lockRequestType == LOCK_CREATION) { + + // Generating lock id + String lockTokenStr = req.getServletPath() + "-" + lock.type + "-" + lock.scope + "-" + + req.getUserPrincipal() + "-" + lock.depth + "-" + lock.owner + "-" + lock.tokens + "-" + + lock.expiresAt + "-" + System.currentTimeMillis() + "-" + secret; + String lockToken = HexUtils + .toHexString(ConcurrentMessageDigest.digestMD5(lockTokenStr.getBytes(StandardCharsets.ISO_8859_1))); + + 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 + List lockPaths = new ArrayList<>(); + Iterator collectionLocksIterator = collectionLocks.iterator(); + while (collectionLocksIterator.hasNext()) { + LockInfo currentLock = collectionLocksIterator.next(); + if (currentLock.hasExpired()) { + collectionLocksIterator.remove(); + continue; + } + if (currentLock.path.startsWith(lock.path) && (currentLock.isExclusive() || lock.isExclusive())) { + // A child collection of this collection is locked + lockPaths.add(currentLock.path); + } + } + for (LockInfo currentLock : resourceLocks.values()) { + if (currentLock.hasExpired()) { + resourceLocks.remove(currentLock.path); + continue; + } + if (currentLock.path.startsWith(lock.path) && (currentLock.isExclusive() || lock.isExclusive())) { + // A child resource of this collection is locked + lockPaths.add(currentLock.path); + } + } + + if (!lockPaths.isEmpty()) { + + // One of the child paths was locked + // We generate a multistatus error report + + resp.setStatus(WebdavStatus.SC_CONFLICT); + + XMLWriter generatedXML = new XMLWriter(); + generatedXML.writeXMLHeader(); + + generatedXML.writeElement("D", DEFAULT_NAMESPACE, "multistatus", XMLWriter.OPENING); + + 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; + } + + boolean addLock = true; + + // Checking if there is already a shared lock on this path + for (LockInfo currentLock : collectionLocks) { + if (currentLock.path.equals(lock.path)) { + + if (currentLock.isExclusive()) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } else { + if (lock.isExclusive()) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } + } + + currentLock.tokens.add(lockToken); + lock = currentLock; + addLock = false; + } + } + + if (addLock) { + lock.tokens.add(lockToken); + collectionLocks.add(lock); + } + + } else { + + // Locking a single resource + + // Retrieving an already existing lock on that resource + LockInfo presentLock = resourceLocks.get(lock.path); + if (presentLock != null) { + + if ((presentLock.isExclusive()) || (lock.isExclusive())) { + // If either lock is exclusive, the lock can't be + // granted + resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED); + return; + } else { + presentLock.tokens.add(lockToken); + lock = presentLock; + } + + } else { + + lock.tokens.add(lockToken); + resourceLocks.put(lock.path, lock); + + // Checking if a resource exists at this path + if (!resource.exists()) { + + // "Creating" a lock-null resource + int slash = lock.path.lastIndexOf('/'); + String parentPath = lock.path.substring(0, slash); + + lockNullResources.computeIfAbsent(parentPath, k -> new ArrayList<>()).add(lock.path); + } + + // Add the Lock-Token header as by RFC 2518 8.10.1 + // - only do this for newly created locks + resp.addHeader("Lock-Token", ""); + } + } + } + + if (lockRequestType == LOCK_REFRESH) { + + String ifHeader = req.getHeader("If"); + if (ifHeader == null) { + ifHeader = ""; + } + + // Checking resource locks + + LockInfo toRenew = resourceLocks.get(path); + + if (toRenew != null) { + // At least one of the tokens of the locks must have been given + for (String token : toRenew.tokens) { + if (ifHeader.contains(token)) { + toRenew.expiresAt = lock.expiresAt; + lock = toRenew; + } + } + } + + // Checking inheritable collection locks + for (LockInfo collecionLock : collectionLocks) { + if (path.equals(collecionLock.path)) { + for (String token : collecionLock.tokens) { + if (ifHeader.contains(token)) { + collecionLock.expiresAt = lock.expiresAt; + lock = collecionLock; + } + } + } + } + } + + // 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); + + lock.toXML(generatedXML); + + generatedXML.writeElement("D", "lockdiscovery", XMLWriter.CLOSING); + + generatedXML.writeElement("D", "prop", XMLWriter.CLOSING); + + resp.setStatus(WebdavStatus.SC_OK); + 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 (readOnly) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } + + if (isLocked(req)) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } + + String path = getRelativePath(req); + + String lockTokenHeader = req.getHeader("Lock-Token"); + if (lockTokenHeader == null) { + lockTokenHeader = ""; + } + + // Checking resource locks + + LockInfo lock = resourceLocks.get(path); + if (lock != null) { + + // At least one of the tokens of the locks must have been given + Iterator tokenList = lock.tokens.iterator(); + while (tokenList.hasNext()) { + String token = tokenList.next(); + if (lockTokenHeader.contains(token)) { + tokenList.remove(); + } + } + + if (lock.tokens.isEmpty()) { + resourceLocks.remove(path); + // Removing any lock-null resource which would be present + lockNullResources.remove(path); + } + + } + + // Checking inheritable collection locks + Iterator collectionLocksList = collectionLocks.iterator(); + while (collectionLocksList.hasNext()) { + lock = collectionLocksList.next(); + if (path.equals(lock.path)) { + Iterator tokenList = lock.tokens.iterator(); + while (tokenList.hasNext()) { + String token = tokenList.next(); + if (lockTokenHeader.contains(token)) { + tokenList.remove(); + break; + } + } + if (lock.tokens.isEmpty()) { + collectionLocksList.remove(); + // Removing any lock-null resource which would be present + lockNullResources.remove(path); + } + } + } + + resp.setStatus(WebdavStatus.SC_NO_CONTENT); + } + + + // -------------------------------------------------------- Private Methods + + /** + * Check to see if a resource is currently write locked. The method will look at the "If" header to make sure the + * client has give the appropriate lock tokens. + * + * @param req Servlet request + * + * @return true 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(HttpServletRequest req) { + + String path = getRelativePath(req); + + String ifHeader = req.getHeader("If"); + if (ifHeader == null) { + ifHeader = ""; + } + + String lockTokenHeader = req.getHeader("Lock-Token"); + if (lockTokenHeader == null) { + lockTokenHeader = ""; + } + + return isLocked(path, ifHeader + lockTokenHeader); + } + + + /** + * Check to see if a resource is currently write locked. + * + * @param path Path of the resource + * @param ifHeader "If" HTTP header which was included in the request + * + * @return true 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 ifHeader) { + + // Checking resource locks + + LockInfo lock = resourceLocks.get(path); + if ((lock != null) && (lock.hasExpired())) { + resourceLocks.remove(path); + } else if (lock != null) { + + // At least one of the tokens of the locks must have been given + + boolean tokenMatch = false; + for (String token : lock.tokens) { + if (ifHeader.contains(token)) { + tokenMatch = true; + break; + } + } + if (!tokenMatch) { + return true; + } + } + + // Checking inheritable collection locks + Iterator collectionLockList = collectionLocks.iterator(); + while (collectionLockList.hasNext()) { + lock = collectionLockList.next(); + if (lock.hasExpired()) { + collectionLockList.remove(); + } else if (path.startsWith(lock.path)) { + boolean tokenMatch = false; + for (String token : lock.tokens) { + if (ifHeader.contains(token)) { + tokenMatch = true; + break; + } + } + if (!tokenMatch) { + return true; + } + } + } + + return false; + } + + + /** + * Copy a resource. + * + * @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(HttpServletRequest req, HttpServletResponse resp) throws IOException { + + // Check the source exists + String path = getRelativePath(req); + WebResource source = resources.getResource(path); + if (!source.exists()) { + resp.sendError(WebdavStatus.SC_NOT_FOUND); + 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; + } + } + + // Cross-context operations aren't supported + String reqContextPath = req.getContextPath(); + if (!destinationPath.startsWith(reqContextPath + "/")) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return false; + } + + // Remove context path & servlet path + destinationPath = destinationPath.substring(reqContextPath.length() + req.getServletPath().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; + } + + boolean overwrite = true; + String overwriteHeader = req.getHeader("Overwrite"); + if (overwriteHeader != null) { + if (overwriteHeader.equalsIgnoreCase("T")) { + overwrite = true; + } else { + overwrite = false; + } + } + + // 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 errorList = new HashMap<>(); + + boolean result = copyResource(errorList, path, destinationPath); + + 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); + } + + // Removing any lock-null resource which would be present at + // the destination path + lockNullResources.remove(destinationPath); + + 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 + * + * @return true if the copy was successful + */ + private boolean copyResource(Map errorList, String source, String dest) { + + if (debug > 1) { + log("Copy: " + source + " To: " + dest); + } + + 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; + } + } + + 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); + } + } 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; + } + } catch (IOException e) { + log(sm.getString("webdavservlet.inputstreamclosefail", source), e); + } + } else { + errorList.put(source, Integer.valueOf(WebdavStatus.SC_INTERNAL_SERVER_ERROR)); + return false; + } + return true; + } + + + /** + * Delete a resource. + * + * @param req Servlet request + * @param resp Servlet response + * + * @return true if the delete is successful + * + * @throws IOException If an IO error occurs + */ + private boolean deleteResource(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String path = getRelativePath(req); + return deleteResource(path, req, resp, 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 true 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 { + + String ifHeader = req.getHeader("If"); + if (ifHeader == null) { + ifHeader = ""; + } + + String lockTokenHeader = req.getHeader("Lock-Token"); + if (lockTokenHeader == null) { + lockTokenHeader = ""; + } + + if (isLocked(path, ifHeader + lockTokenHeader)) { + 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()) { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + return false; + } + } else { + + Map errorList = new HashMap<>(); + + deleteCollection(req, path, errorList); + if (!resource.delete()) { + errorList.put(path, Integer.valueOf(WebdavStatus.SC_INTERNAL_SERVER_ERROR)); + } + + 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 errorList) { + + if (debug > 1) { + log("Delete:" + 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 lockTokenHeader = req.getHeader("Lock-Token"); + if (lockTokenHeader == null) { + lockTokenHeader = ""; + } + + String[] entries = resources.list(path); + + for (String entry : entries) { + String childName = path; + if (!childName.equals("/")) { + childName += "/"; + } + childName += entry; + + if (isLocked(childName, ifHeader + lockTokenHeader)) { + + errorList.put(childName, Integer.valueOf(WebdavStatus.SC_LOCKED)); + + } else { + WebResource childResource = resources.getResource(childName); + if (childResource.isDirectory()) { + deleteCollection(req, childName, errorList); + } + + if (!childResource.delete()) { + if (!childResource.isDirectory()) { + // If it's not a collection, then it's an unknown + // error + errorList.put(childName, Integer.valueOf(WebdavStatus.SC_INTERNAL_SERVER_ERROR)); + } + } + } + } + } + + + /** + * 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 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 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(getServletContext().getContextPath() + errorPath); + 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(); + } + + + /** + * Propfind helper method. + * + * @param req The servlet request + * @param generatedXML XML response to the Propfind request + * @param path Path of the current resource + * @param type Propfind type + * @param properties If the propfind type is find properties by name, then this List contains those properties + */ + private void parseProperties(HttpServletRequest req, XMLWriter generatedXML, String path, int type, + List properties) { + + // Exclude any resource in the /WEB-INF and /META-INF subdirectories + if (isSpecialPath(path)) { + return; + } + + WebResource resource = resources.getResource(path); + if (!resource.exists()) { + // File is in directory listing but doesn't appear to exist + // Broken symlink or odd permission settings? + return; + } + + String href = req.getContextPath() + req.getServletPath(); + if ((href.endsWith("/")) && (path.startsWith("/"))) { + href += path.substring(1); + } else { + href += path; + } + if (resource.isDirectory() && (!href.endsWith("/"))) { + href += "/"; + } + + String rewrittenUrl = rewriteUrl(href); + + generatePropFindResponse(generatedXML, rewrittenUrl, path, type, properties, resource.isFile(), false, + resource.getCreation(), resource.getLastModified(), resource.getContentLength(), + getServletContext().getMimeType(resource.getName()), generateETag(resource)); + } + + + /** + * Propfind helper method. Displays the properties of a lock-null resource. + * + * @param req The servlet request + * @param generatedXML XML response to the Propfind request + * @param path Path of the current resource + * @param type Propfind type + * @param properties If the propfind type is find properties by name, then this List contains those properties + */ + private void parseLockNullProperties(HttpServletRequest req, XMLWriter generatedXML, String path, int type, + List properties) { + + // Exclude any resource in the /WEB-INF and /META-INF subdirectories + if (isSpecialPath(path)) { + return; + } + + // Retrieving the lock associated with the lock-null resource + LockInfo lock = resourceLocks.get(path); + + if (lock == null) { + return; + } + + String absoluteUri = req.getRequestURI(); + String relativePath = getRelativePath(req); + String toAppend = path.substring(relativePath.length()); + if (!toAppend.startsWith("/")) { + toAppend = "/" + toAppend; + } + + String rewrittenUrl = rewriteUrl(RequestUtil.normalize(absoluteUri + toAppend)); + + generatePropFindResponse(generatedXML, rewrittenUrl, path, type, properties, true, true, + lock.creationDate.getTime(), lock.creationDate.getTime(), 0, "", ""); + } + + + private void generatePropFindResponse(XMLWriter generatedXML, String rewrittenUrl, String path, int propFindType, + List properties, boolean isFile, boolean isLockNull, 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); + + String resourceName = path; + int lastSlash = path.lastIndexOf('/'); + if (lastSlash != -1) { + resourceName = resourceName.substring(lastSlash + 1); + } + + 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.writeElement("D", "displayname", XMLWriter.OPENING); + generatedXML.writeData(resourceName); + generatedXML.writeElement("D", "displayname", XMLWriter.CLOSING); + if (isFile) { + generatedXML.writeProperty("D", "getlastmodified", FastHttpDateFormat.formatDate(lastModified)); + generatedXML.writeProperty("D", "getcontentlength", Long.toString(contentLength)); + if (contentType != null) { + generatedXML.writeProperty("D", "getcontenttype", contentType); + } + generatedXML.writeProperty("D", "getetag", eTag); + if (isLockNull) { + generatedXML.writeElement("D", "resourcetype", XMLWriter.OPENING); + generatedXML.writeElement("D", "lock-null", XMLWriter.NO_CONTENT); + generatedXML.writeElement("D", "resourcetype", XMLWriter.CLOSING); + } else { + generatedXML.writeElement("D", "resourcetype", XMLWriter.NO_CONTENT); + } + } else { + generatedXML.writeProperty("D", "getlastmodified", FastHttpDateFormat.formatDate(lastModified)); + generatedXML.writeElement("D", "resourcetype", XMLWriter.OPENING); + generatedXML.writeElement("D", "collection", XMLWriter.NO_CONTENT); + generatedXML.writeElement("D", "resourcetype", XMLWriter.CLOSING); + } + + generatedXML.writeProperty("D", "source", ""); + + String supportedLocks = "" + "" + + "" + "" + "" + + "" + "" + + ""; + generatedXML.writeElement("D", "supportedlock", XMLWriter.OPENING); + generatedXML.writeRaw(supportedLocks); + 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); + generatedXML.writeElement("D", "displayname", XMLWriter.NO_CONTENT); + if (isFile) { + generatedXML.writeElement("D", "getcontentlanguage", XMLWriter.NO_CONTENT); + generatedXML.writeElement("D", "getcontentlength", XMLWriter.NO_CONTENT); + 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", "source", XMLWriter.NO_CONTENT); + generatedXML.writeElement("D", "lockdiscovery", 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; + + case FIND_BY_PROPERTY: + + List propertiesNotFound = new ArrayList<>(); + + // Parse the list of properties + + generatedXML.writeElement("D", "propstat", XMLWriter.OPENING); + generatedXML.writeElement("D", "prop", XMLWriter.OPENING); + + for (String property : properties) { + if (property.equals("creationdate")) { + generatedXML.writeProperty("D", "creationdate", getISOCreationDate(created)); + } else if (property.equals("displayname")) { + generatedXML.writeElement("D", "displayname", XMLWriter.OPENING); + generatedXML.writeData(resourceName); + generatedXML.writeElement("D", "displayname", XMLWriter.CLOSING); + } else if (property.equals("getcontentlanguage")) { + if (isFile) { + generatedXML.writeElement("D", "getcontentlanguage", XMLWriter.NO_CONTENT); + } else { + propertiesNotFound.add(property); + } + } else if (property.equals("getcontentlength")) { + if (isFile) { + generatedXML.writeProperty("D", "getcontentlength", Long.toString(contentLength)); + } else { + propertiesNotFound.add(property); + } + } else if (property.equals("getcontenttype")) { + if (isFile) { + generatedXML.writeProperty("D", "getcontenttype", contentType); + } else { + propertiesNotFound.add(property); + } + } else if (property.equals("getetag")) { + if (isFile) { + generatedXML.writeProperty("D", "getetag", eTag); + } else { + propertiesNotFound.add(property); + } + } else if (property.equals("getlastmodified")) { + if (isFile) { + generatedXML.writeProperty("D", "getlastmodified", + FastHttpDateFormat.formatDate(lastModified)); + } else { + propertiesNotFound.add(property); + } + } else if (property.equals("resourcetype")) { + if (isFile) { + if (isLockNull) { + generatedXML.writeElement("D", "resourcetype", XMLWriter.OPENING); + generatedXML.writeElement("D", "lock-null", XMLWriter.NO_CONTENT); + generatedXML.writeElement("D", "resourcetype", XMLWriter.CLOSING); + } else { + 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("source")) { + generatedXML.writeProperty("D", "source", ""); + } else if (property.equals("supportedlock")) { + supportedLocks = "" + "" + + "" + "" + "" + + "" + "" + + ""; + generatedXML.writeElement("D", "supportedlock", XMLWriter.OPENING); + generatedXML.writeRaw(supportedLocks); + generatedXML.writeElement("D", "supportedlock", XMLWriter.CLOSING); + } else if (property.equals("lockdiscovery")) { + if (!generateLockDiscovery(path, generatedXML)) { + propertiesNotFound.add(property); + } + } else { + propertiesNotFound.add(property); + } + } + + 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 (String propertyNotFound : propertiesNotFound) { + generatedXML.writeElement("D", propertyNotFound, 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 + * + * @return true if at least one lock was displayed + */ + private boolean generateLockDiscovery(String path, XMLWriter generatedXML) { + + LockInfo resourceLock = resourceLocks.get(path); + + boolean wroteStart = false; + + if (resourceLock != null) { + wroteStart = true; + generatedXML.writeElement("D", "lockdiscovery", XMLWriter.OPENING); + resourceLock.toXML(generatedXML); + } + + for (LockInfo currentLock : collectionLocks) { + if (path.startsWith(currentLock.path)) { + if (!wroteStart) { + wroteStart = true; + generatedXML.writeElement("D", "lockdiscovery", XMLWriter.OPENING); + } + currentLock.toXML(generatedXML); + } + } + + if (wroteStart) { + generatedXML.writeElement("D", "lockdiscovery", XMLWriter.CLOSING); + } else { + return false; + } + + return true; + } + + + /** + * Get creation date in ISO format. + * + * @return the formatted creation date + */ + private String getISOCreationDate(long creationDate) { + return creationDateFormat.format(new Date(creationDate)); + } + + + /** + * Determines the methods normally allowed for the resource. + * + * @param req The Servlet request + * + * @return The allowed HTTP methods + */ + @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 (!readOnly) { + 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 (listings) { + methodsAllowed.append(", PROPFIND"); + } + + if (!resource.exists()) { + methodsAllowed.append(", MKCOL"); + } + + return methodsAllowed.toString(); + } + + + // -------------------------------------------------- LockInfo Inner Class + + /** + * Holds a lock information. + */ + private static class LockInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + LockInfo(int maxDepth) { + this.maxDepth = maxDepth; + } + + + // ------------------------------------------------- Instance Variables + + private final int maxDepth; + + String path = "/"; + String type = "write"; + String scope = "exclusive"; + int depth = 0; + String owner = ""; + List tokens = Collections.synchronizedList(new ArrayList<>()); + long expiresAt = 0; + Date creationDate = new Date(); + + + // ----------------------------------------------------- Public Methods + + /** + * Get a String representation of this lock token. + */ + @Override + public String toString() { + + StringBuilder result = new StringBuilder("Type:"); + result.append(type); + result.append("\nScope:"); + result.append(scope); + result.append("\nDepth:"); + result.append(depth); + result.append("\nOwner:"); + result.append(owner); + result.append("\nExpiration:"); + result.append(FastHttpDateFormat.formatDate(expiresAt)); + for (String token : tokens) { + result.append("\nToken:"); + result.append(token); + } + result.append("\n"); + return result.toString(); + } + + + /** + * @return true if the lock has expired. + */ + public boolean hasExpired() { + return 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", "locktoken", XMLWriter.OPENING); + for (String token : tokens) { + generatedXML.writeElement("D", "href", XMLWriter.OPENING); + generatedXML.writeText("opaquelocktoken:" + 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 false. External + * references are filtered out for security reasons. See CVE-2007-5461. + */ + private static class WebdavResolver implements EntityResolver { + private 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")); + } + } +} + + +// -------------------------------------------------------- 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. + * + * @author Marc Eaddy + */ +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; + // This one collides with HTTP 1.1 + // "207 Partial Update OK" + + + /** + * Status code (418) indicating the entity body submitted with the PATCH method was not understood by the resource. + */ + public static final int SC_UNPROCESSABLE_ENTITY = 418; + // This one collides with HTTP 1.1 + // "418 Reauthentication Required" + + + /** + * Status code (419) indicating that the resource does not have sufficient space to record the state of the resource + * after the execution of this method. + */ + public static final int SC_INSUFFICIENT_SPACE_ON_RESOURCE = 419; + // This one collides with HTTP 1.1 + // "419 Proxy Reauthentication Required" + + + /** + * 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; +}