This is an automated email from the ASF dual-hosted git repository. remm pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/main by this push: new ac76965a76 Rewrite implementation of WebDAV shared locks ac76965a76 is described below commit ac76965a760fac80518bb62db6e9b3f2b4436f91 Author: remm <r...@apache.org> AuthorDate: Fri Oct 18 16:48:30 2024 +0200 Rewrite implementation of WebDAV shared locks During my review of RFC 4918, I found the implementation of shared locks was non compliant due to wrong design (one lock with multiple tokens, for example). Exclusive locks are simpler and are mostly unchanged. Collection locks now go into the same map as resource locks (and path traversal is used to discover them) to avoid having to use four maps. Implement new RFC 4918 behavior regarding auth principals and locks. Implement the many new clarifications on unlock and lock discovery. Add a shared locks test case with lots of edge cases. --- .../apache/catalina/servlets/WebdavServlet.java | 525 +++++++++++---------- .../catalina/servlets/TestWebdavServlet.java | 245 +++++++++- webapps/docs/changelog.xml | 4 + 3 files changed, 525 insertions(+), 249 deletions(-) diff --git a/java/org/apache/catalina/servlets/WebdavServlet.java b/java/org/apache/catalina/servlets/WebdavServlet.java index 193fb8d4e0..fd12574655 100644 --- a/java/org/apache/catalina/servlets/WebdavServlet.java +++ b/java/org/apache/catalina/servlets/WebdavServlet.java @@ -29,10 +29,8 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Date; import java.util.Deque; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; @@ -216,18 +214,15 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen // ----------------------------------------------------- Instance Variables /** - * Repository of the locks put on single resources. - * <p> - * Key : path <br> - * Value : LockInfo + * Repository of all locks, keyed by path. */ - private final ConcurrentHashMap<String,LockInfo> resourceLocks = new ConcurrentHashMap<>(); + private final ConcurrentHashMap<String, LockInfo> resourceLocks = new ConcurrentHashMap<>(); /** - * List of the inheritable collection locks. + * Map of all shared locks, keyed by lock token. */ - private final CopyOnWriteArrayList<LockInfo> collectionLocks = new CopyOnWriteArrayList<>(); + private final ConcurrentHashMap<String, LockInfo> sharedLocks = new ConcurrentHashMap<>(); /** @@ -284,16 +279,25 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen @Override public void periodicEvent() { // Check expiration of all locks - for (LockInfo currentLock : resourceLocks.values()) { + for (LockInfo currentLock : sharedLocks.values()) { if (currentLock.hasExpired()) { - resourceLocks.remove(currentLock.path); + sharedLocks.remove(currentLock.path); } } - Iterator<LockInfo> collectionLocksIterator = collectionLocks.iterator(); - while (collectionLocksIterator.hasNext()) { - LockInfo currentLock = collectionLocksIterator.next(); - if (currentLock.hasExpired()) { - collectionLocksIterator.remove(); + for (LockInfo currentLock : resourceLocks.values()) { + if (currentLock.isExclusive()) { + if (currentLock.hasExpired()) { + resourceLocks.remove(currentLock.path); + } + } else { + for (String token : currentLock.sharedTokens) { + if (sharedLocks.get(token) == null) { + currentLock.sharedTokens.remove(token); + } + } + if (currentLock.sharedTokens.isEmpty()) { + resourceLocks.remove(currentLock.path); + } } } } @@ -431,8 +435,11 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen if (result.length() == 0) { result.append('/'); } - - return result.toString(); + String resultString = result.toString(); + if (resultString.length() > 1 && resultString.endsWith("/")) { + resultString = resultString.substring(0, resultString.length() - 1); + } + return resultString; } @@ -454,7 +461,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen } else { href += path; } - if (resource.isDirectory() && (!href.endsWith("/"))) { + if (resource != null && resource.isDirectory() && (!href.endsWith("/"))) { href += "/"; } @@ -486,9 +493,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen } String path = getRelativePath(req); - if (path.length() > 1 && path.endsWith("/")) { - path = path.substring(0, path.length() - 1); - } // Exclude any resource in the /WEB-INF and /META-INF subdirectories if (isSpecialPath(path)) { @@ -664,9 +668,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen protected void doProppatch(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String path = getRelativePath(req); - if (path.length() > 1 && path.endsWith("/")) { - path = path.substring(0, path.length() - 1); - } WebResource resource = resources.getResource(path); if (!checkIfHeaders(req, resp, resource)) { @@ -1021,16 +1022,13 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen String path = getRelativePath(req); - if (isLocked(path, req)) { - resp.sendError(WebdavStatus.SC_LOCKED); - return; - } WebResource resource = resources.getResource(path); if (!checkIfHeaders(req, resp, resource)) { return; } LockInfo lock = new LockInfo(maxDepth); + lock.principal = req.getRemoteUser(); // Parsing lock request @@ -1211,27 +1209,33 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen if (lockRequestType == LOCK_CREATION) { - // Check if a parent is already locked - Iterator<LockInfo> collectionLocksIterator = collectionLocks.iterator(); - while (collectionLocksIterator.hasNext()) { - LockInfo currentLock = collectionLocksIterator.next(); - if (currentLock.hasExpired()) { - collectionLocksIterator.remove(); - continue; + // Check if the resource or a parent is already locked + String parentPath = path; + do { + LockInfo parentLock = resourceLocks.get(parentPath); + if (parentLock != null) { + if (parentLock.hasExpired()) { + resourceLocks.remove(parentPath); + } else if (parentLock.isExclusive() || lock.isExclusive()) { + // A parent collection of this collection is locked + resp.setStatus(WebdavStatus.SC_LOCKED); + return; + } } - if (lock.path.startsWith(currentLock.path + "/") && (currentLock.isExclusive() || lock.isExclusive())) { - // A parent collection of this collection is locked - resp.setStatus(WebdavStatus.SC_LOCKED); - return; + int slash = parentPath.lastIndexOf('/'); + if (slash < 0) { + break; } - } + parentPath = parentPath.substring(0, slash); + } while (true); // Generating lock id String lockTokenStr = req.getServletPath() + "-" + lock.type + "-" + lock.scope + "-" + - req.getUserPrincipal() + "-" + lock.depth + "-" + lock.owner + "-" + lock.tokens + "-" + + req.getUserPrincipal() + "-" + lock.depth + "-" + lock.owner + "-" + lock.token + "-" + lock.expiresAt + "-" + System.currentTimeMillis() + "-" + secret; String lockToken = HexUtils .toHexString(ConcurrentMessageDigest.digestMD5(lockTokenStr.getBytes(StandardCharsets.ISO_8859_1))); + lock.token = lockToken; if (resource.isDirectory() && lock.depth == maxDepth) { @@ -1239,27 +1243,16 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen // Checking if a child resource of this collection is // already locked + // Note: it is likely faster in many cases to go over the full lock list than trying to go over all the children (recursively) List<String> lockPaths = new ArrayList<>(); - 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())) { + if ((currentLock.isExclusive() || lock.isExclusive()) && currentLock.path.startsWith(lock.path + "/")) { // A child resource of this collection is locked - lockPaths.add(currentLock.path); + lockPaths.add(currentLock.lockroot); } } @@ -1275,6 +1268,14 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen generatedXML.writeElement("D", DEFAULT_NAMESPACE, "multistatus", XMLWriter.OPENING); + generatedXML.writeElement("D", "response", XMLWriter.OPENING); + generatedXML.writeElement("D", "href", XMLWriter.OPENING); + generatedXML.writeText(getEncodedPath(path, resource, req)); + generatedXML.writeElement("D", "href", XMLWriter.CLOSING); + generatedXML.writeElement("D", "status", XMLWriter.OPENING); + generatedXML.writeText("HTTP/1.1 " + WebdavStatus.SC_FAILED_DEPENDENCY + " "); + generatedXML.writeElement("D", "status", XMLWriter.CLOSING); + generatedXML.writeElement("D", "response", XMLWriter.CLOSING); for (String lockPath : lockPaths) { generatedXML.writeElement("D", "response", XMLWriter.OPENING); generatedXML.writeElement("D", "href", XMLWriter.OPENING); @@ -1295,73 +1296,38 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen 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); - // Add the Lock-Token header as by RFC 2518 8.10.1 - // - only do this for newly created locks - resp.addHeader("Lock-Token", "<opaquelocktoken:" + lockToken + ">"); - } - } 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); + // Checking if a resource exists at this path + if (!resource.exists()) { + // RFC 4918 removes lock null, instead an empty file is created + if (!resources.write(path, new ByteArrayInputStream(new byte[0]), false)) { + resp.sendError(WebdavStatus.SC_CONFLICT); return; - } else { - presentLock.tokens.add(lockToken); - lock = presentLock; - } - - } else { - - // Checking if a resource exists at this path - if (!resource.exists()) { - // RFC 4918 removes lock null, instead an empty file is created - if (!resources.write(path, new ByteArrayInputStream(new byte[0]), false)) { - resp.sendError(WebdavStatus.SC_CONFLICT); - return; - } } + } - lock.tokens.add(lockToken); - resourceLocks.put(lock.path, lock); + } - // Add the Lock-Token header as by RFC 2518 8.10.1 - // - only do this for newly created locks - resp.addHeader("Lock-Token", "<opaquelocktoken:" + lockToken + ">"); + lock.lockroot = getEncodedPath(lock.path, resource, req); + if (lock.isExclusive()) { + resourceLocks.put(path, lock); + } else { + // Checking if there is already a shared lock on this path + LockInfo sharedLock = resourceLocks.get(path); + if (sharedLock == null) { + resourceLocks.put(path, lock); + sharedLock = lock; } + sharedLock.sharedTokens.add(lockToken); + sharedLocks.put(lockToken, lock); } + + // Add the Lock-Token header as by RFC 2518 8.10.1 + resp.addHeader("Lock-Token", "<opaquelocktoken:" + lockToken + ">"); + } if (lockRequestType == LOCK_REFRESH) { @@ -1377,22 +1343,19 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen 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)) { + if (toRenew.isExclusive()) { + if (ifHeader.contains(toRenew.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) { + } else { + for (String token : toRenew.sharedTokens) { if (ifHeader.contains(token)) { - collecionLock.expiresAt = lock.expiresAt; - lock = collecionLock; + toRenew = sharedLocks.get(token); + if (toRenew != null) { + toRenew.expiresAt = lock.expiresAt; + lock = toRenew; + } } } } @@ -1436,6 +1399,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen } String path = getRelativePath(req); + WebResource resource = resources.getResource(path); if (!checkIfHeaders(req, resp, resource)) { return; @@ -1446,50 +1410,55 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen 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<String> tokenList = lock.tokens.iterator(); - while (tokenList.hasNext()) { - String token = tokenList.next(); - if (lockTokenHeader.contains(token)) { - tokenList.remove(); + boolean unlocked = false; + String parentPath = path; + do { + LockInfo parentLock = resourceLocks.get(parentPath); + if (parentLock != null) { + if (parentLock.hasExpired()) { + resourceLocks.remove(parentPath); + } else { + if ((parentPath != path && parentLock.depth > 0) || parentPath == path) { + if (parentLock.isExclusive()) { + if (lockTokenHeader.contains(parentLock.token) + && (parentLock.principal == null || parentLock.principal.equals(req.getRemoteUser()))) { + resourceLocks.remove(parentPath); + unlocked = true; + break; + } + } else { + for (String token : parentLock.sharedTokens) { + if (lockTokenHeader.contains(token)) { + LockInfo lock = sharedLocks.get(token); + if (lock == null || lock.principal == null || lock.principal.equals(req.getRemoteUser())) { + parentLock.sharedTokens.remove(token); + if (parentLock.sharedTokens.isEmpty()) { + resourceLocks.remove(parentPath); + } + sharedLocks.remove(token); + unlocked = true; + break; + } + } + } + } + } } } - - if (lock.tokens.isEmpty()) { - resourceLocks.remove(path); + int slash = parentPath.lastIndexOf('/'); + if (slash < 0) { + break; } + parentPath = parentPath.substring(0, slash); + } while (true); + if (unlocked) { + resp.setStatus(WebdavStatus.SC_NO_CONTENT); + } else { + sendReport(req, resp, parentPath, WebdavStatus.SC_CONFLICT, "lock-token-matches-request-uri"); } - - // Checking inheritable collection locks - Iterator<LockInfo> collectionLocksList = collectionLocks.iterator(); - while (collectionLocksList.hasNext()) { - lock = collectionLocksList.next(); - if (path.equals(lock.path)) { - Iterator<String> tokenList = lock.tokens.iterator(); - while (tokenList.hasNext()) { - String token = tokenList.next(); - if (lockTokenHeader.contains(token)) { - tokenList.remove(); - break; - } - } - if (lock.tokens.isEmpty()) { - collectionLocks.remove(lock); - break; - } - } - } - - resp.setStatus(WebdavStatus.SC_NO_CONTENT); } - // -------------------------------------------------------- Private Methods /** @@ -1519,7 +1488,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen lockTokenHeader = ""; } - return isLocked(path, ifHeader + lockTokenHeader); + return isLocked(path, req.getRemoteUser(), ifHeader + lockTokenHeader); } @@ -1527,53 +1496,52 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen * Check to see if a resource is currently write locked. * * @param path Path of the resource + * @param principal The authenticated principal name * @param ifHeader "If" HTTP header which was included in the request * * @return <code>true</code> if the resource is locked (and no appropriate lock token has been found for at least * one of the non-shared locks which are present on the resource). */ - private boolean isLocked(String path, String 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; - } - } + private boolean isLocked(String path, String principal, String ifHeader) { - // Checking inheritable collection locks - Iterator<LockInfo> 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; + // Check if the resource or a parent is already locked + String parentPath = path; + do { + LockInfo parentLock = resourceLocks.get(parentPath); + if (parentLock != null) { + if (parentLock.hasExpired()) { + resourceLocks.remove(parentPath); + } else { + if ((parentPath != path && parentLock.depth > 0) || parentPath == path) { + if (parentLock.isExclusive()) { + if (ifHeader.contains(parentLock.token) + && (parentLock.principal == null || parentLock.principal.equals(principal))) { + return false; + } + } else { + for (String token : parentLock.sharedTokens) { + if (ifHeader.contains(token)) { + if (principal == null) { + return false; + } else { + LockInfo lock = sharedLocks.get(token); + if (lock == null || lock.principal == null || lock.principal.equals(principal)) { + return false; + } + } + } + } + } } - } - if (!tokenMatch) { return true; } } - } + int slash = parentPath.lastIndexOf('/'); + if (slash < 0) { + break; + } + parentPath = parentPath.substring(0, slash); + } while (true); return false; } @@ -1647,6 +1615,10 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen } } + if (destinationPath.length() > 1 && destinationPath.endsWith("/")) { + destinationPath = destinationPath.substring(0, destinationPath.length() - 1); + } + // Cross-context operations aren't supported String reqContextPath = getPathPrefix(req); if (!destinationPath.startsWith(reqContextPath + "/")) { @@ -1885,7 +1857,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen sendNotAllowed(req, resp); return false; } - resourceLocks.remove(path); + unlockResource(path, null); } else { Map<String,Integer> errorList = new LinkedHashMap<>(); @@ -1908,15 +1880,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen errorList.put(path, Integer.valueOf(WebdavStatus.SC_METHOD_NOT_ALLOWED)); } } else { - Iterator<LockInfo> collectionLocksList = collectionLocks.iterator(); - while (collectionLocksList.hasNext()) { - LockInfo lock = collectionLocksList.next(); - if (path.equals(lock.path)) { - collectionLocks.remove(lock); - break; - } - } - resourceLocks.remove(path); + unlockResource(path, null); } if (!errorList.isEmpty()) { @@ -1930,6 +1894,27 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen return true; } + private void unlockResource(String path, String lockToken) { + LockInfo lock = resourceLocks.get(path); + if (lock != null) { + if (lock.isExclusive()) { + if (lockToken == null || lockToken.contains(lock.token)) { + resourceLocks.remove(path); + } + } else { + for (String token : lock.sharedTokens) { + if (lockToken == null || lockToken.contains(token)) { + lock.sharedTokens.remove(token); + if (lock.sharedTokens.isEmpty()) { + resourceLocks.remove(lock.path); + } + sharedLocks.remove(token); + break; + } + } + } + } + } /** * Deletes a collection. @@ -1969,7 +1954,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen } childName += entry; - if (isLocked(childName, lockHeader)) { + if (isLocked(childName, req.getRemoteUser(), lockHeader)) { errorList.put(childName, Integer.valueOf(WebdavStatus.SC_LOCKED)); @@ -1996,17 +1981,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen errorList.put(childName, Integer.valueOf(WebdavStatus.SC_METHOD_NOT_ALLOWED)); } } else { - if (childResource.isDirectory()) { - Iterator<LockInfo> collectionLocksList = collectionLocks.iterator(); - while (collectionLocksList.hasNext()) { - LockInfo lock = collectionLocksList.next(); - if (childName.equals(lock.path)) { - collectionLocks.remove(lock); - break; - } - } - } - resourceLocks.remove(childName); + unlockResource(childName, null); } } } @@ -2039,7 +2014,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen generatedXML.writeElement("D", "response", XMLWriter.OPENING); generatedXML.writeElement("D", "href", XMLWriter.OPENING); - generatedXML.writeText(getPathPrefix(req) + errorPath); + generatedXML.writeText(getEncodedPath(errorPath, null, req)); generatedXML.writeElement("D", "href", XMLWriter.CLOSING); generatedXML.writeElement("D", "status", XMLWriter.OPENING); @@ -2057,6 +2032,33 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen } + private void sendReport(HttpServletRequest req, HttpServletResponse resp, String errorPath, int errorCode, String error) + throws IOException { + resp.setStatus(errorCode); + + XMLWriter generatedXML = new XMLWriter(); + generatedXML.writeXMLHeader(); + + generatedXML.writeElement("D", DEFAULT_NAMESPACE, "error", XMLWriter.OPENING); + + if (errorPath != null && errorPath.length() > 0) { + generatedXML.writeElement("D", error, XMLWriter.OPENING); + generatedXML.writeElement("D", "href", XMLWriter.OPENING); + generatedXML.writeText(getEncodedPath(errorPath, null, req)); + generatedXML.writeElement("D", "href", XMLWriter.CLOSING); + generatedXML.writeElement("D", error, XMLWriter.CLOSING); + } else { + generatedXML.writeElement("D", error, XMLWriter.NO_CONTENT); + } + + generatedXML.writeElement("D", "error", XMLWriter.CLOSING); + + Writer writer = resp.getWriter(); + writer.write(generatedXML.toString()); + writer.close(); + } + + private void propfindResource(XMLWriter generatedXML, String rewrittenUrl, String path, int propFindType, List<Node> properties, boolean isFile, long created, long lastModified, long contentLength, String contentType, String eTag) { @@ -2285,25 +2287,47 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen */ 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); + String parentPath = path; + do { + LockInfo parentLock = resourceLocks.get(parentPath); + if (parentLock != null) { + if (parentLock.hasExpired()) { + resourceLocks.remove(parentPath); + } else { + if ((parentPath != path && parentLock.depth > 0) || parentPath == path) { + if (parentLock.isExclusive()) { + if (!wroteStart) { + wroteStart = true; + generatedXML.writeElement("D", "lockdiscovery", XMLWriter.OPENING); + } + parentLock.toXML(generatedXML); + } else { + for (String lockToken : parentLock.sharedTokens) { + parentLock = sharedLocks.get(lockToken); + if (parentLock != null) { + if (parentLock.hasExpired()) { + sharedLocks.remove(lockToken); + } else { + if (!wroteStart) { + wroteStart = true; + generatedXML.writeElement("D", "lockdiscovery", XMLWriter.OPENING); + } + parentLock.toXML(generatedXML); + } + } + } + } + } } - currentLock.toXML(generatedXML); } - } + int slash = parentPath.lastIndexOf('/'); + if (slash < 0) { + break; + } + parentPath = parentPath.substring(0, slash); + } while (true); if (wroteStart) { generatedXML.writeElement("D", "lockdiscovery", XMLWriter.CLOSING); @@ -2387,12 +2411,15 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen private final int maxDepth; String path = "/"; + String lockroot = "/"; String type = "write"; String scope = "exclusive"; int depth = 0; String owner = ""; - List<String> tokens = Collections.synchronizedList(new ArrayList<>()); + String token = ""; + List<String> sharedTokens = new CopyOnWriteArrayList<>(); long expiresAt = 0; + String principal = null; // ----------------------------------------------------- Public Methods @@ -2410,10 +2437,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen result.append(owner); result.append("\nExpiration:"); result.append(FastHttpDateFormat.formatDate(expiresAt)); - for (String token : tokens) { - result.append("\nToken:"); - result.append(token); - } + result.append("\nToken:"); + result.append(token); result.append("\n"); return result.toString(); } @@ -2469,12 +2494,16 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen generatedXML.writeText("Second-" + timeout); generatedXML.writeElement("D", "timeout", XMLWriter.CLOSING); + generatedXML.writeElement("D", "lockroot", XMLWriter.OPENING); + generatedXML.writeElement("D", "href", XMLWriter.OPENING); + generatedXML.writeText(lockroot); + generatedXML.writeElement("D", "href", XMLWriter.CLOSING); + generatedXML.writeElement("D", "lockroot", XMLWriter.CLOSING); + generatedXML.writeElement("D", "locktoken", XMLWriter.OPENING); - for (String token : tokens) { - generatedXML.writeElement("D", "href", XMLWriter.OPENING); - generatedXML.writeText("opaquelocktoken:" + token); - generatedXML.writeElement("D", "href", XMLWriter.CLOSING); - } + 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); diff --git a/test/org/apache/catalina/servlets/TestWebdavServlet.java b/test/org/apache/catalina/servlets/TestWebdavServlet.java index 45ed9ce9db..3b09c4cbdf 100644 --- a/test/org/apache/catalina/servlets/TestWebdavServlet.java +++ b/test/org/apache/catalina/servlets/TestWebdavServlet.java @@ -199,6 +199,14 @@ public class TestWebdavServlet extends TomcatBaseTest { " <D:owner>someone</D:owner>\n" + "</D:lockinfo>"; + private static final String LOCK_SHARED_BODY = + "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" + + "<D:lockinfo xmlns:D='DAV:'>\n" + + " <D:lockscope><D:shared/></D:lockscope>\n" + + " <D:locktype><D:write/></D:locktype>\n" + + " <D:owner>someone</D:owner>\n" + + "</D:lockinfo>"; + private static final String PROPFIND_PROP = "<?xml version=\"1.0\" encoding=\"utf-8\" ?> \n" + "<D:propfind xmlns:D=\"DAV:\">\n" + @@ -554,7 +562,7 @@ public class TestWebdavServlet extends TomcatBaseTest { Assert.assertTrue(client.getResponseBody().contains("/file7.txt")); // Unlock /myfolder again - client.setRequest(new String[] { "UNLOCK /myfolder HTTP/1.1" + SimpleHttpClient.CRLF + + client.setRequest(new String[] { "UNLOCK /myfolder/ HTTP/1.1" + SimpleHttpClient.CRLF + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + "Lock-Token: " + lockToken + SimpleHttpClient.CRLF + "Connection: Close" + SimpleHttpClient.CRLF + @@ -583,6 +591,241 @@ public class TestWebdavServlet extends TomcatBaseTest { } + @Test + public void testSharedLocks() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // Create a temp webapp that can be safely written to + File tempWebapp = new File(getTemporaryDirectory(), "webdav-lock"); + Assert.assertTrue(tempWebapp.mkdirs()); + Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath()); + Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new WebdavServlet()); + webdavServlet.addInitParameter("listings", "true"); + webdavServlet.addInitParameter("secret", "foo"); + webdavServlet.addInitParameter("readonly", "false"); + ctxt.addServletMappingDecoded("/*", "webdav"); + tomcat.start(); + + Client client = new Client(); + client.setPort(getPort()); + + // Create a few folders and files + client.setRequest(new String[] { "MKCOL /myfolder HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode()); + + client.setRequest(new String[] { "MKCOL /myfolder/myfolder2/ HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode()); + + client.setRequest(new String[] { "MKCOL /myfolder/myfolder3 HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode()); + + client.setRequest(new String[] { "MKCOL /myfolder/myfolder2/myfolder4 HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode()); + + client.setRequest(new String[] { "MKCOL /myfolder/myfolder2/myfolder4/myfolder5 HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode()); + + client.setRequest(new String[] { "PUT /file1.txt HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Content-Length: 6" + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + CONTENT }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode()); + + client.setRequest(new String[] { "PUT /myfolder/myfolder3/file2.txt HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Content-Length: 6" + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + CONTENT }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode()); + + client.setRequest(new String[] { "PUT /myfolder/myfolder2/myfolder4/file3.txt HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Content-Length: 6" + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + CONTENT }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode()); + + // Lock /myfolder + client.setRequest(new String[] { "LOCK /myfolder HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Content-Length: " + LOCK_SHARED_BODY.length() + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + LOCK_SHARED_BODY }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode()); + Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:")); + String lockToken = null; + for (String header : client.getResponseHeaders()) { + if (header.startsWith("Lock-Token: ")) { + lockToken = header.substring("Lock-Token: ".length()); + } + } + Assert.assertNotNull(lockToken); + + client.setRequest(new String[] { "LOCK /myfolder/myfolder2/myfolder4/myfolder5 HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "If: " + lockToken + SimpleHttpClient.CRLF + + "Content-Length: " + LOCK_BODY.length() + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + LOCK_BODY }); + client.connect(); + client.processRequest(true); + // This should conflict, submitting a token does not help + Assert.assertEquals(WebdavStatus.SC_LOCKED, client.getStatusCode()); + + client.setRequest(new String[] { "LOCK /myfolder/myfolder2/myfolder4 HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Content-Length: " + LOCK_SHARED_BODY.length() + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + LOCK_SHARED_BODY }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode()); + Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:")); + String lockToken2 = null; + for (String header : client.getResponseHeaders()) { + if (header.startsWith("Lock-Token: ")) { + lockToken2 = header.substring("Lock-Token: ".length()); + } + } + Assert.assertNotNull(lockToken2); + + // Take a second different lock on the same collection + client.setRequest(new String[] { "LOCK /myfolder/myfolder2/myfolder4 HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Content-Length: " + LOCK_SHARED_BODY.length() + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + LOCK_SHARED_BODY }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode()); + Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:")); + String lockToken3 = null; + for (String header : client.getResponseHeaders()) { + if (header.startsWith("Lock-Token: ")) { + lockToken3 = header.substring("Lock-Token: ".length()); + } + } + Assert.assertNotNull(lockToken3); + + client.setRequest(new String[] { "PROPFIND / HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode()); + Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:")); + + client.setRequest(new String[] { "PUT /myfolder/myfolder2/myfolder4/myfolder5/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Content-Length: 6" + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + CONTENT }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(WebdavStatus.SC_LOCKED, client.getStatusCode()); + + client.setRequest(new String[] { "PUT /myfolder/myfolder2/myfolder4/myfolder5/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "If: " + lockToken2 + SimpleHttpClient.CRLF + + "Content-Length: 6" + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + CONTENT }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode()); + + client.setRequest(new String[] { "UNLOCK /myfolder/myfolder2/myfolder4/myfolder5 HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Lock-Token: " + lockToken2 + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(HttpServletResponse.SC_NO_CONTENT, client.getStatusCode()); + + client.setRequest(new String[] { "UNLOCK /myfolder/myfolder2/myfolder4/myfolder5/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Lock-Token: " + lockToken3 + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(HttpServletResponse.SC_NO_CONTENT, client.getStatusCode()); + + client.setRequest(new String[] { "PUT /myfolder/myfolder2/myfolder4/myfolder5/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Content-Length: 6" + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + CONTENT }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(WebdavStatus.SC_LOCKED, client.getStatusCode()); + + client.setRequest(new String[] { "UNLOCK /myfolder/myfolder2/myfolder4/myfolder5/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Lock-Token: " + lockToken + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(HttpServletResponse.SC_NO_CONTENT, client.getStatusCode()); + + client.setRequest(new String[] { "PUT /myfolder/myfolder2/myfolder4/myfolder5/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Content-Length: 12" + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + CONTENT + CONTENT}); + client.connect(); + client.processRequest(true); + Assert.assertEquals(WebdavStatus.SC_NO_CONTENT, client.getStatusCode()); + + client.setRequest(new String[] { "PROPFIND / HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode()); + // Verify all the shared locks are cleared + Assert.assertFalse(client.getResponseBody().contains("opaquelocktoken:")); + + } + private static final class Client extends SimpleHttpClient { @Override diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 6e276efb75..74453cbb91 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -131,6 +131,10 @@ and annex D. Instead a lock on a non existing resource will create an empty file locked with a regular lock. (remm) </update> + <update> + Rewrite implementation of WebDAV shared locks to comply with RFC 4918. + (remm) + </update> <!-- Entries for backport and removal before 12.0.0-M1 below this line --> <update> <bug>69374</bug>: Properly separate between table header and body --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org