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 40c094b8ba Refine the dead properties handling 40c094b8ba is described below commit 40c094b8ba52513b1468a45db0d9ad0c36a64fbf Author: remm <r...@apache.org> AuthorDate: Sun Oct 20 23:46:05 2024 +0200 Refine the dead properties handling Add a proof of concept otherwise it is not possible to validate it. Add namespaces on elments to the DOMWriter. --- .../servlets/TransientPropertiesWebdavServlet.java | 186 +++++++++++++++++++++ .../apache/catalina/servlets/WebdavServlet.java | 71 ++++++-- java/org/apache/catalina/util/DOMWriter.java | 12 +- .../catalina/servlets/TestWebdavServlet.java | 84 ++++------ 4 files changed, 286 insertions(+), 67 deletions(-) diff --git a/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java b/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java new file mode 100644 index 0000000000..80c979c190 --- /dev/null +++ b/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java @@ -0,0 +1,186 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.catalina.servlets; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.util.DOMWriter; +import org.apache.catalina.util.XMLWriter; +import org.w3c.dom.Node; + +/** + * Extended WebDAV Servlet that implements dead properties using storage in memory. + */ +public class TransientPropertiesWebdavServlet extends WebdavServlet { + + private static final long serialVersionUID = 1L; + + private final ConcurrentHashMap<String,ArrayList<Node>> deadProperties = new ConcurrentHashMap<>(); + + @Override + protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.addHeader("DAV", "1,2,3"); + resp.addHeader("Allow", determineMethodsAllowed(req)); + resp.addHeader("MS-Author-Via", "DAV"); + } + + @Override + protected void proppatchResource(String path, ArrayList<ProppatchOperation> operations) { + boolean protectedProperty = false; + // Check for the protected properties + for (ProppatchOperation operation : operations) { + if (operation.getProtectedProperty()) { + protectedProperty = true; + operation.setStatusCode(HttpServletResponse.SC_FORBIDDEN); + } + } + if (protectedProperty) { + for (ProppatchOperation operation : operations) { + if (!operation.getProtectedProperty()) { + operation.setStatusCode(WebdavStatus.SC_FAILED_DEPENDENCY); + } + } + } else { + ArrayList<Node> properties = deadProperties.get(path); + if (properties == null) { + properties = new ArrayList<Node>(); + deadProperties.put(path, properties); + } + synchronized (properties) { + for (ProppatchOperation operation : operations) { + if (operation.getUpdateType() == PropertyUpdateType.SET) { + Node node = operation.getPropertyNode().cloneNode(true); + boolean found = false; + for (int i = 0; i < properties.size(); i++) { + Node propertyNode = properties.get(i); + if (propertyEquals(node, propertyNode)) { + found = true; + properties.set(i, node); + break; + } + } + if (!found) { + properties.add(node); + } + } if (operation.getUpdateType() == PropertyUpdateType.REMOVE) { + Node node = operation.getPropertyNode(); + for (int i = 0; i < properties.size(); i++) { + Node propertyNode = properties.get(i); + if (propertyEquals(node, propertyNode)) { + properties.remove(i); + break; + } + } + } + } + } + } + } + + @Override + protected boolean propfindResource(String path, Node property, boolean nameOnly, XMLWriter generatedXML) { + ArrayList<Node> properties = deadProperties.get(path); + if (properties != null) { + synchronized (properties) { + if (nameOnly) { + // Add the names of all properties + for (Node node : properties) { + generatedXML.writeElement(node.getPrefix(), + node.getNamespaceURI(), + node.getLocalName(), XMLWriter.NO_CONTENT); + } + } else if (property != null) { + // Add a single property + Node foundNode = null; + for (Node node : properties) { + if (propertyEquals(node, property)) { + foundNode = node; + } + } + if (foundNode != null) { + StringWriter strWriter = new StringWriter(); + DOMWriter domWriter = new DOMWriter(strWriter); + domWriter.print(foundNode); + generatedXML.writeRaw(strWriter.toString()); + return true; + } + } else { + StringWriter strWriter = new StringWriter(); + DOMWriter domWriter = new DOMWriter(strWriter); + // Add all properties + for (Node node : properties) { + domWriter.print(node); + } + generatedXML.writeRaw(strWriter.toString()); + } + } + } + return false; + } + + @Override + protected void copyResource(String source, String dest) { + ArrayList<Node> properties = deadProperties.get(source); + ArrayList<Node> propertiesDest = deadProperties.get(dest); + if (properties != null) { + if (propertiesDest == null) { + propertiesDest = new ArrayList<Node>(); + deadProperties.put(dest, propertiesDest); + } + synchronized (properties) { + synchronized (propertiesDest) { + for (Node node : properties) { + node = node.cloneNode(true); + boolean found = false; + for (int i = 0; i < propertiesDest.size(); i++) { + Node propertyNode = propertiesDest.get(i); + if (propertyEquals(node, propertyNode)) { + found = true; + propertiesDest.set(i, node); + break; + } + } + if (!found) { + propertiesDest.add(node); + } + } + } + } + } + } + + @Override + protected void deleteResource(String path) { + deadProperties.remove(path); + } + + private boolean propertyEquals(Node node1, Node node2) { + if (node1.getLocalName().equals(node2.getLocalName()) + && ((node1.getNamespaceURI() == null && node2.getNamespaceURI() == null) + || (node1.getNamespaceURI() != null && node1.getNamespaceURI().equals(node2.getNamespaceURI())))) { + return true; + } + return false; + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/servlets/WebdavServlet.java b/java/org/apache/catalina/servlets/WebdavServlet.java index 15f5d398d6..4f1df32636 100644 --- a/java/org/apache/catalina/servlets/WebdavServlet.java +++ b/java/org/apache/catalina/servlets/WebdavServlet.java @@ -998,7 +998,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen /** - * Apply proppatch to the specified path. This should be overriden by subclasses to provide + * Apply proppatch to the specified path. This should be overridden by subclasses to provide * useful behavior. The default implementation prevents setting protected properties * (anything from the DAV: namespace), and sets 507 for a set attempt on dead properties. * @@ -1964,6 +1964,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen errorList.put(dest, Integer.valueOf(WebdavStatus.SC_CONFLICT)); return false; } + } else { + copyResource(source, dest); } if (infiniteCopy) { @@ -2006,6 +2008,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen if (!resources.write(dest, is, false)) { errorList.put(source, Integer.valueOf(WebdavStatus.SC_INTERNAL_SERVER_ERROR)); return false; + } else { + copyResource(source, dest); } } catch (IOException e) { log(sm.getString("webdavservlet.inputstreamclosefail", source), e); @@ -2017,6 +2021,16 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen return true; } + /** + * Copy resource. This should be overridden by subclasses to provide + * useful behavior. The default implementation prevents setting protected properties + * (anything from the DAV: namespace), and sets 507 for a set attempt on dead properties. + * + * @param source the copy source path + * @param dest the copy destination path + */ + protected void copyResource(String source, String dest) { + } /** * Delete a resource. @@ -2071,7 +2085,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen sendNotAllowed(req, resp); return false; } - unlockResource(path, null); + deleteResource(path); } else { Map<String,Integer> errorList = new LinkedHashMap<>(); @@ -2094,7 +2108,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen errorList.put(path, Integer.valueOf(WebdavStatus.SC_METHOD_NOT_ALLOWED)); } } else { - unlockResource(path, null); + deleteResource(path); } if (!errorList.isEmpty()) { @@ -2108,6 +2122,17 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen return true; } + /** + * Delete specified resource. This should be overridden by subclasses to provide + * useful behavior. The default implementation prevents setting protected properties + * (anything from the DAV: namespace), and sets 507 for a set attempt on dead properties. + * + * @param path the path of the resource to delete + */ + protected void deleteResource(String path) { + unlockResource(path, null); + } + private void unlockResource(String path, String lockToken) { LockInfo lock = resourceLocks.get(path); if (lock != null) { @@ -2195,7 +2220,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen errorList.put(childName, Integer.valueOf(WebdavStatus.SC_METHOD_NOT_ALLOWED)); } } else { - unlockResource(childName, null); + deleteResource(childName); } } } @@ -2299,9 +2324,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen 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)); @@ -2338,7 +2360,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen 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); @@ -2468,7 +2489,9 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen /** - * Generate propfind XML fragments for dead properties. + * Generate propfind XML fragments for dead properties. This should be overridden by subclasses to provide + * useful behavior. The default implementation prevents setting protected properties + * (anything from the DAV: namespace), and sets 507 for a set attempt on dead properties. * * @param path the resource path * @param property the dead property, if null then all dead properties must be written @@ -2477,6 +2500,30 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen * @return true if property was specified and a corresponding dead property was found on the resource, false otherwise */ protected boolean propfindResource(String path, Node property, boolean nameOnly, XMLWriter generatedXML) { + if (nameOnly) { + generatedXML.writeElement("D", "displayname", XMLWriter.NO_CONTENT); + } else if (property == null) { + String resourceName = path; + int lastSlash = path.lastIndexOf('/'); + if (lastSlash != -1) { + resourceName = resourceName.substring(lastSlash + 1); + } + generatedXML.writeElement("D", "displayname", XMLWriter.OPENING); + generatedXML.writeData(resourceName); + generatedXML.writeElement("D", "displayname", XMLWriter.CLOSING); + } else { + String davName = getDAVNode(property); + if ("displayname".equals(davName)) { + String resourceName = path; + int lastSlash = path.lastIndexOf('/'); + if (lastSlash != -1) { + resourceName = resourceName.substring(lastSlash + 1); + } + generatedXML.writeElement("D", "displayname", XMLWriter.OPENING); + generatedXML.writeData(resourceName); + generatedXML.writeElement("D", "displayname", XMLWriter.CLOSING); + } + } return false; } @@ -2575,7 +2622,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen private static String getDAVNode(Node node) { - if (node.getNamespaceURI().equals(DEFAULT_NAMESPACE)) { + if (DEFAULT_NAMESPACE.equals(node.getNamespaceURI())) { return node.getLocalName(); } return null; @@ -2731,7 +2778,9 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen public ProppatchOperation(PropertyUpdateType updateType, Node propertyNode) { this.updateType = updateType; this.propertyNode = propertyNode; - protectedProperty = getDAVNode(propertyNode) != null; + String davName = getDAVNode(propertyNode); + protectedProperty = davName != null + && (!(davName.equals("displayname") || davName.equals("getcontentlanguage"))); } /** * @return the updateType diff --git a/java/org/apache/catalina/util/DOMWriter.java b/java/org/apache/catalina/util/DOMWriter.java index 0cd3532526..7ef95b1ffa 100644 --- a/java/org/apache/catalina/util/DOMWriter.java +++ b/java/org/apache/catalina/util/DOMWriter.java @@ -64,14 +64,22 @@ public class DOMWriter { out.print('<'); out.print(node.getLocalName()); Attr attrs[] = sortAttributes(node.getAttributes()); + boolean xmlns = false; for (Attr attr : attrs) { out.print(' '); out.print(attr.getLocalName()); - + if ("xmlns".equals(attr.getLocalName())) { + xmlns = true; + } out.print("=\""); out.print(Escape.xml("", true, attr.getNodeValue())); out.print('"'); } + if (!xmlns && node.getNamespaceURI() != null) { + out.print(" xmlns=\""); + out.print(Escape.xml(node.getNamespaceURI())); + out.print('"'); + } out.print('>'); printChildren(node); break; @@ -88,7 +96,7 @@ public class DOMWriter { // print text case Node.TEXT_NODE: - out.print(Escape.xml("", true, node.getNodeValue())); + out.print(Escape.xml("", false, node.getNodeValue())); break; // print processing instruction diff --git a/test/org/apache/catalina/servlets/TestWebdavServlet.java b/test/org/apache/catalina/servlets/TestWebdavServlet.java index 8272381a56..71f7977ae4 100644 --- a/test/org/apache/catalina/servlets/TestWebdavServlet.java +++ b/test/org/apache/catalina/servlets/TestWebdavServlet.java @@ -20,7 +20,6 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.StringReader; -import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -36,7 +35,6 @@ import org.apache.catalina.Wrapper; import org.apache.catalina.startup.SimpleHttpClient; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.TomcatBaseTest; -import org.apache.catalina.util.XMLWriter; import org.apache.tomcat.util.buf.ByteChunk; import org.apache.tomcat.websocket.server.WsContextListener; import org.xml.sax.InputSource; @@ -230,7 +228,7 @@ public class TestWebdavServlet extends TomcatBaseTest { "<D:propertyupdate xmlns:D=\"DAV:\" xmlns:T=\"http://tomcat.apache.org/testsuite\">\n" + " <D:set>\n" + " <D:prop>\n" + - " <T:customprop xmlns:T=\"http://tomcat.apache.org/testsuite\">\n" + + " <T:customprop>\n" + " <T:myvalue/>\n" + " </T:customprop>\n" + " </D:prop>\n" + @@ -248,7 +246,7 @@ public class TestWebdavServlet extends TomcatBaseTest { File tempWebapp = new File(getTemporaryDirectory(), "webdav-properties"); Assert.assertTrue(tempWebapp.mkdirs()); Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath()); - Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new CustomWebdavServlet()); + Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new TransientPropertiesWebdavServlet()); webdavServlet.addInitParameter("listings", "true"); webdavServlet.addInitParameter("secret", "foo"); webdavServlet.addInitParameter("readonly", "false"); @@ -289,7 +287,17 @@ public class TestWebdavServlet extends TomcatBaseTest { Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode()); Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:")); - client.setRequest(new String[] { "PROPFIND / HTTP/1.1" + SimpleHttpClient.CRLF + + client.setRequest(new String[] { "PROPPATCH /file1.txt HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Content-Length: " + PROPPATCH_PROPNAME.length() + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + PROPPATCH_PROPNAME }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode()); + Assert.assertTrue(client.getResponseBody().contains("<T:othercustomprop")); + + client.setRequest(new String[] { "PROPFIND /file1.txt HTTP/1.1" + SimpleHttpClient.CRLF + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + "Content-Length: " + PROPFIND_PROPNAME.length() + SimpleHttpClient.CRLF + "Connection: Close" + SimpleHttpClient.CRLF + @@ -299,7 +307,7 @@ public class TestWebdavServlet extends TomcatBaseTest { Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode()); Assert.assertTrue(client.getResponseBody().contains("<D:getcontenttype/>")); - client.setRequest(new String[] { "PROPFIND / HTTP/1.1" + SimpleHttpClient.CRLF + + client.setRequest(new String[] { "PROPFIND /file1.txt HTTP/1.1" + SimpleHttpClient.CRLF + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + "Content-Length: " + PROPFIND_PROP.length() + SimpleHttpClient.CRLF + "Connection: Close" + SimpleHttpClient.CRLF + @@ -309,18 +317,28 @@ public class TestWebdavServlet extends TomcatBaseTest { Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode()); Assert.assertTrue(client.getResponseBody().contains("<D:getcontenttype>")); Assert.assertFalse(client.getResponseBody().contains("<D:getlastmodified>")); - Assert.assertTrue(client.getResponseBody().contains("<T:myvalue/>")); + Assert.assertTrue(client.getResponseBody().contains("<myvalue xmlns=\"http://tomcat.apache.org/testsuite\">")); - client.setRequest(new String[] { "PROPPATCH /file1.txt HTTP/1.1" + SimpleHttpClient.CRLF + + client.setRequest(new String[] { "MOVE /file1.txt HTTP/1.1" + SimpleHttpClient.CRLF + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + - "Content-Length: " + PROPPATCH_PROPNAME.length() + SimpleHttpClient.CRLF + + "Destination: /file3.txt" + SimpleHttpClient.CRLF + "Connection: Close" + SimpleHttpClient.CRLF + - SimpleHttpClient.CRLF + PROPPATCH_PROPNAME }); + SimpleHttpClient.CRLF }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode()); + + client.setRequest(new String[] { "PROPFIND /file3.txt HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Content-Length: " + PROPFIND_PROP.length() + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + PROPFIND_PROP }); client.connect(); client.processRequest(true); Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode()); - Assert.assertTrue(proppatchSuccess); - Assert.assertTrue(client.getResponseBody().contains("<T:othercustomprop")); + Assert.assertTrue(client.getResponseBody().contains("<D:getcontenttype>")); + Assert.assertFalse(client.getResponseBody().contains("<D:getlastmodified>")); + Assert.assertTrue(client.getResponseBody().contains("<myvalue xmlns=\"http://tomcat.apache.org/testsuite\">")); } @@ -979,46 +997,4 @@ public class TestWebdavServlet extends TomcatBaseTest { } } - private static boolean proppatchSuccess = false; - - private class CustomWebdavServlet extends WebdavServlet { - - private static final long serialVersionUID = 1L; - - @Override - protected void proppatchResource(String path, ArrayList<ProppatchOperation> operations) { - for (ProppatchOperation operation : operations) { - if (operation.getUpdateType().equals(PropertyUpdateType.SET) - && operation.getPropertyNode().getLocalName().equals("customprop")) { - proppatchSuccess = true; - } - operation.setStatusCode(HttpServletResponse.SC_OK); - } - } - - @Override - protected boolean propfindResource(String path, org.w3c.dom.Node property, boolean nameOnly, XMLWriter generatedXML) { - if (nameOnly) { - generatedXML.writeElement("T", "http://tomcat.apache.org/testsuite", "customprop", XMLWriter.NO_CONTENT); - generatedXML.writeElement("T", "http://tomcat.apache.org/testsuite", "othercustomprop", XMLWriter.NO_CONTENT); - } else if (property == null) { - generatedXML.writeElement("T", "http://tomcat.apache.org/testsuite", "customprop", XMLWriter.OPENING); - generatedXML.writeElement("T", "myvalue", XMLWriter.NO_CONTENT); - generatedXML.writeElement("T", "customprop", XMLWriter.CLOSING); - generatedXML.writeElement("T", "http://tomcat.apache.org/testsuite", "othercustomprop", XMLWriter.OPENING); - generatedXML.writeElement("T", "myothervalue", XMLWriter.NO_CONTENT); - generatedXML.writeElement("T", "othercustomprop", XMLWriter.CLOSING); - } else if (property.getLocalName().equals("customprop")) { - generatedXML.writeElement("T", "http://tomcat.apache.org/testsuite", "customprop", XMLWriter.OPENING); - generatedXML.writeElement("T", "myvalue", XMLWriter.NO_CONTENT); - generatedXML.writeElement("T", "customprop", XMLWriter.CLOSING); - return true; - } else if (property.getLocalName().equals("othercustomprop")) { - return false; - } - return false; - } - - } - } --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org