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;
+}