/* * 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;
/** * Servlet which adds support for <a href="https://tools.ietf.org/html/rfc4918">WebDAV</a> * <a href="https://tools.ietf.org/html/rfc4918#section-18">level 2</a>. 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. * <p> * Mapping a subpath (e.g. <code>/webdav/*</code> 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 <code>WEB-INF</code> and * <code>META-INF</code> directories are protected in this re-mounted resource tree. * <p> * To enable WebDAV for a context add the following to web.xml: * * <pre> * <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> * </pre> * * This will enable read only access. To enable read-write access add: * * <pre> * <init-param> * <param-name>readonly</param-name> * <param-value>false</param-value> * </init-param> * </pre> * * To make the content editable via a different URL, use the following mapping: * * <pre> * <servlet-mapping> * <servlet-name>webdav</servlet-name> * <url-pattern>/webdavedit/*</url-pattern> * </servlet-mapping> * </pre> * * By default access to /WEB-INF and META-INF are not available via WebDAV. To enable access to these URLs, use add: * * <pre> * <init-param> * <param-name>allowSpecialPaths</param-name> * <param-value>true</param-value> * </init-param> * </pre> * * Don't forget to secure access appropriately to the editing URLs, especially if allowSpecialPaths is used. With the * mapping configuration above, the context will be accessible to normal users as before. Those users with the necessary * access will be able to edit content available via http://host:port/context/content using * http://host:port/context/webdavedit/content * * @author Remy Maucherat * * @see <a href="https://tools.ietf.org/html/rfc4918">RFC 4918</a>
*/ publicclass WebdavServlet extends DefaultServlet {
/** * Simple date format for the creation date ISO representation (partial).
*/ protectedstaticfinal ConcurrentDateFormat creationDateFormat = new ConcurrentDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US, TimeZone.getTimeZone("GMT"));
/** * Repository of the locks put on single resources. * <p> * Key : path <br> * Value : LockInfo
*/ privatefinal Map<String,LockInfo> resourceLocks = new ConcurrentHashMap<>();
/** * Repository of the lock-null resources. * <p> * Key : path of the collection containing the lock-null resource<br> * 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.
*/ privatefinal Map<String,List<String>> lockNullResources = new ConcurrentHashMap<>();
/** * List of the inheritable collection locks.
*/ privatefinal List<LockInfo> 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.
*/ privateint maxDepth = 3;
/** * Is access allowed via WebDAV to the special paths (/WEB-INF and /META-INF)?
*/ privateboolean allowSpecialPaths = false;
// --------------------------------------------------------- Public Methods
/** * Handles the special WebDAV methods.
*/
@Override protectedvoid 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;
}
/** * Checks whether a given path refers to a resource under <code>WEB-INF</code> or <code>META-INF</code>. * * @param path the full path of the resource being accessed * * @return <code>true</code> if the resource specified is under a special path
*/ privateboolean isSpecialPath(final String path) { return !allowSpecialPaths && (path.toUpperCase(Locale.ENGLISH).startsWith("/WEB-INF") ||
path.toUpperCase(Locale.ENGLISH).startsWith("/META-INF"));
}
if (!super.checkIfHeaders(request, response, resource)) { returnfalse;
}
// TODO : Checking the WebDAV If header returntrue;
}
/** * 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);
}
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 protectedvoid 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
*/ protectedvoid doPropfind(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
if (!listings) {
sendNotAllowed(req, resp); return;
}
// Properties which are to be displayed.
List<String> properties = null; // Propfind depth int depth = maxDepth; // Propfind type int type = FIND_ALL_PROP;
// 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;
}
}
}
if (depth == 0) {
parseProperties(req, generatedXML, path, type, properties);
} else { // The stack always contains the object of the current level
Deque<String> stack = new ArrayDeque<>();
stack.addFirst(path);
// Stack of the objects one level below
Deque<String> stackBelow = new ArrayDeque<>();
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 protectedvoid 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 protectedvoid doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
if (isLocked(req)) {
resp.sendError(WebdavStatus.SC_LOCKED); return;
}
// Get the root element of the document
Element rootElement = document.getDocumentElement();
lockInfoNode = rootElement;
} catch (IOException | SAXException e) {
lockRequestType = LOCK_REFRESH;
}
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<String> lockPaths = new ArrayList<>();
Iterator<LockInfo> 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();
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;
}
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);
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();
}
}
if (lock.tokens.isEmpty()) {
resourceLocks.remove(path); // Removing any lock-null resource which would be present
lockNullResources.remove(path);
}
}
// 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()) {
collectionLocksList.remove(); // Removing any lock-null resource which would be present
lockNullResources.remove(path);
}
}
}
/** * 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 <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).
*/ privateboolean isLocked(HttpServletRequest req) {
/** * 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 <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).
*/ privateboolean isLocked(String path, String ifHeader) {
// Destination isn't allowed to use '.' or '..' segments if (!destinationPath.equals(RequestUtil.normalize(destinationPath))) {
resp.sendError(WebdavStatus.SC_BAD_REQUEST); returnfalse;
}
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); returnfalse;
} // Port needs to match too but handled separately as the logic is a // little more complicated if (req.getServerPort() != destinationUri.getPort()) { if (destinationUri.getPort() == -1 && ("http".equals(req.getScheme()) && req.getServerPort() == 80 || "https".equals(req.getScheme()) && req.getServerPort() == 443)) { // All good.
} else {
resp.sendError(WebdavStatus.SC_FORBIDDEN); returnfalse;
}
}
}
// Check destination path to protect special subdirectories if (isSpecialPath(destinationPath)) {
resp.sendError(WebdavStatus.SC_FORBIDDEN); returnfalse;
}
if (destinationPath.equals(path)) {
resp.sendError(WebdavStatus.SC_FORBIDDEN); returnfalse;
}
// 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); returnfalse;
}
// 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)) { returnfalse;
}
} 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); returnfalse;
}
}
// Copying source to destination
Map<String,Integer> 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);
} returnfalse;
}
// 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);
returntrue;
}
/** * 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 <code>true</code> if the copy was successful
*/ privateboolean copyResource(Map<String,Integer> errorList, String source, String dest) {
if (debug > 1) {
log("Copy: " + source + " To: " + dest);
}
if (sourceResource.isDirectory()) { if (!resources.mkdir(dest)) {
WebResource destResource = resources.getResource(dest); if (!destResource.isDirectory()) {
errorList.put(dest, Integer.valueOf(WebdavStatus.SC_CONFLICT)); returnfalse;
}
}
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);
}
} elseif (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)); returnfalse;
}
}
} // 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)); returnfalse;
}
} catch (IOException e) {
log(sm.getString("webdavservlet.inputstreamclosefail", source), e);
}
} else {
errorList.put(source, Integer.valueOf(WebdavStatus.SC_INTERNAL_SERVER_ERROR)); returnfalse;
} returntrue;
}
/** * Delete a resource. * * @param req Servlet request * @param resp Servlet response * * @return <code>true</code> if the delete is successful * * @throws IOException If an IO error occurs
*/ privateboolean 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 <code>true</code> if the delete is successful * * @throws IOException If an IO error occurs
*/ privateboolean deleteResource(String path, HttpServletRequest req, HttpServletResponse resp, boolean setStatus) throws IOException {
if (!resource.exists()) {
resp.sendError(WebdavStatus.SC_NOT_FOUND); returnfalse;
}
if (!resource.isDirectory()) { if (!resource.delete()) {
resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); returnfalse;
}
} else {
Map<String,Integer> 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); returnfalse;
}
} if (setStatus) {
resp.setStatus(WebdavStatus.SC_NO_CONTENT);
} returntrue;
}
/** * 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
*/ privatevoid deleteCollection(HttpServletRequest req, String path, Map<String,Integer> errorList) {
if (debug > 1) {
log("Delete:" + path);
}
// Prevent deletion of special subdirectories if (isSpecialPath(path)) {
errorList.put(path, Integer.valueOf(WebdavStatus.SC_FORBIDDEN)); return;
}
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
*/ privatevoid sendReport(HttpServletRequest req, HttpServletResponse resp, Map<String,Integer> errorList) throws IOException {
resp.setStatus(WebdavStatus.SC_MULTI_STATUS);
XMLWriter generatedXML = new XMLWriter();
generatedXML.writeXMLHeader();
/** * 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
*/ privatevoid parseProperties(HttpServletRequest req, XMLWriter generatedXML, String path, int type,
List<String> 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;
}
/** * 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
*/ privatevoid parseLockNullProperties(HttpServletRequest req, XMLWriter generatedXML, String path, int type,
List<String> 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);
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.