mirror of
https://github.com/YunoHost-Apps/seafile_ynh.git
synced 2024-09-03 20:26:01 +02:00
1432 lines
60 KiB
Python
1432 lines
60 KiB
Python
# (c) 2009-2011 Martin Wendt and contributors; see WsgiDAV http://wsgidav.googlecode.com/
|
|
# Original PyFileServer (c) 2005 Ho Chun Wei.
|
|
# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
|
|
"""
|
|
WSGI application that handles one single WebDAV request.
|
|
|
|
See `Developers info`_ for more information about the WsgiDAV architecture.
|
|
|
|
.. _`Developers info`: http://docs.wsgidav.googlecode.com/hg/html/develop.html
|
|
"""
|
|
from urlparse import urlparse
|
|
from wsgidav.dav_error import HTTP_OK, HTTP_LENGTH_REQUIRED
|
|
from wsgidav import xml_tools
|
|
import util
|
|
import urllib
|
|
try:
|
|
from cStringIO import StringIO
|
|
except ImportError:
|
|
from StringIO import StringIO #@UnusedImport
|
|
|
|
from dav_error import DAVError, asDAVError, \
|
|
HTTP_BAD_REQUEST,\
|
|
HTTP_NOT_IMPLEMENTED, HTTP_NOT_FOUND, HTTP_FORBIDDEN, HTTP_INTERNAL_ERROR,\
|
|
HTTP_FAILED_DEPENDENCY, HTTP_METHOD_NOT_ALLOWED,\
|
|
PRECONDITION_CODE_PropfindFiniteDepth, HTTP_MEDIATYPE_NOT_SUPPORTED,\
|
|
HTTP_CONFLICT, \
|
|
PRECONDITION_CODE_LockTokenMismatch, getHttpStatusString,\
|
|
HTTP_PRECONDITION_FAILED, HTTP_BAD_GATEWAY, HTTP_NO_CONTENT, HTTP_CREATED,\
|
|
HTTP_RANGE_NOT_SATISFIABLE
|
|
|
|
#import lock_manager
|
|
|
|
# Trick PyDev to do intellisense and don't produce warnings:
|
|
from util import etree #@UnusedImport
|
|
if False: from xml.etree import ElementTree as etree #@Reimport
|
|
|
|
__docformat__ = "reStructuredText"
|
|
|
|
_logger = util.getModuleLogger(__name__)
|
|
|
|
BLOCK_SIZE = 8192
|
|
|
|
|
|
|
|
#===============================================================================
|
|
# RequestServer
|
|
#===============================================================================
|
|
class RequestServer(object):
|
|
|
|
def __init__(self, davProvider):
|
|
self._davProvider = davProvider
|
|
self.allowPropfindInfinite = True
|
|
self._verbose = 2
|
|
util.debug("RequestServer: __init__", module="sc")
|
|
|
|
def __del__(self):
|
|
util.debug("RequestServer: __del__", module="sc")
|
|
|
|
def __call__(self, environ, start_response):
|
|
assert "wsgidav.verbose" in environ
|
|
# TODO: allow anonymous somehow: this should run, even if http_authenticator middleware is not installed
|
|
# assert "http_authenticator.username" in environ
|
|
if not "http_authenticator.username" in environ:
|
|
_logger.info("*** missing 'http_authenticator.username' in environ")
|
|
|
|
environ["wsgidav.username"] = environ.get("http_authenticator.username", "anonymous")
|
|
requestmethod = environ["REQUEST_METHOD"]
|
|
|
|
# Convert 'infinity' and 'T'/'F' to a common case
|
|
if environ.get("HTTP_DEPTH") is not None:
|
|
environ["HTTP_DEPTH"] = environ["HTTP_DEPTH"].lower()
|
|
if environ.get("HTTP_OVERWRITE") is not None:
|
|
environ["HTTP_OVERWRITE"] = environ["HTTP_OVERWRITE"].upper()
|
|
|
|
if "HTTP_EXPECT" in environ:
|
|
pass
|
|
|
|
# Dispatch HTTP request methods to 'doMETHOD()' handlers
|
|
method = getattr(self, "do%s" % requestmethod, None)
|
|
if not method:
|
|
self._fail(HTTP_METHOD_NOT_ALLOWED)
|
|
|
|
if environ.get("wsgidav.debug_break"):
|
|
pass # Set a break point here
|
|
|
|
if environ.get("wsgidav.debug_profile"):
|
|
from cProfile import Profile
|
|
profile = Profile()
|
|
res = profile.runcall(method, environ, start_response)
|
|
# sort: 0:"calls",1:"time", 2: "cumulative"
|
|
profile.print_stats(sort=2)
|
|
for v in res:
|
|
yield v
|
|
return
|
|
|
|
for v in method(environ, start_response):
|
|
yield v
|
|
return
|
|
# return method(environ, start_response)
|
|
|
|
|
|
def _fail(self, value, contextinfo=None, srcexception=None, errcondition=None):
|
|
"""Wrapper to raise (and log) DAVError."""
|
|
if isinstance(value, Exception):
|
|
e = asDAVError(value)
|
|
else:
|
|
e = DAVError(value, contextinfo, srcexception, errcondition)
|
|
util.log("Raising DAVError %s" % e.getUserInfo())
|
|
raise e
|
|
|
|
|
|
def _sendResponse(self, environ, start_response, rootRes, successCode, errorList):
|
|
"""Send WSGI response (single or multistatus).
|
|
|
|
- If errorList is None or [], then <successCode> is send as response.
|
|
- If errorList contains a single error with a URL that matches rootRes,
|
|
then this error is returned.
|
|
- If errorList contains more than one error, then '207 Multistatus' is
|
|
returned.
|
|
"""
|
|
assert successCode in (HTTP_CREATED, HTTP_NO_CONTENT, HTTP_OK)
|
|
if not errorList:
|
|
# Status OK
|
|
return util.sendStatusResponse(environ, start_response, successCode)
|
|
if len(errorList) == 1 and errorList[0][0] == rootRes.getHref():
|
|
# Only one error that occurred on the root resource
|
|
return util.sendStatusResponse(environ, start_response, errorList[0][1])
|
|
|
|
# Multiple errors, or error on one single child
|
|
multistatusEL = xml_tools.makeMultistatusEL()
|
|
|
|
for refurl, e in errorList:
|
|
# assert refurl.startswith("http:")
|
|
assert refurl.startswith("/")
|
|
assert isinstance(e, DAVError)
|
|
responseEL = etree.SubElement(multistatusEL, "{DAV:}response")
|
|
etree.SubElement(responseEL, "{DAV:}href").text = refurl
|
|
etree.SubElement(responseEL, "{DAV:}status").text = "HTTP/1.1 %s" % getHttpStatusString(e)
|
|
|
|
return util.sendMultiStatusResponse(environ, start_response, multistatusEL)
|
|
|
|
|
|
def _checkWritePermission(self, res, depth, environ):
|
|
"""Raise DAVError(HTTP_LOCKED), if res is locked.
|
|
|
|
If depth=='infinity', we also raise when child resources are locked.
|
|
"""
|
|
lockMan = self._davProvider.lockManager
|
|
if lockMan is None or res is None:
|
|
return True
|
|
|
|
refUrl = res.getRefUrl()
|
|
|
|
if "wsgidav.conditions.if" not in environ:
|
|
util.parseIfHeaderDict(environ)
|
|
|
|
# raise HTTP_LOCKED if conflict exists
|
|
lockMan.checkWritePermission(refUrl, depth,
|
|
environ["wsgidav.ifLockTokenList"],
|
|
environ["wsgidav.username"])
|
|
|
|
|
|
def _evaluateIfHeaders(self, res, environ):
|
|
"""Apply HTTP headers on <path>, raising DAVError if conditions fail.
|
|
|
|
Add environ['wsgidav.conditions.if'] and environ['wsgidav.ifLockTokenList'].
|
|
Handle these headers:
|
|
|
|
- If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since:
|
|
Raising HTTP_PRECONDITION_FAILED or HTTP_NOT_MODIFIED
|
|
- If:
|
|
Raising HTTP_PRECONDITION_FAILED
|
|
|
|
@see http://www.webdav.org/specs/rfc4918.html#HEADER_If
|
|
@see util.evaluateHTTPConditionals
|
|
"""
|
|
# Add parsed If header to environ
|
|
if "wsgidav.conditions.if" not in environ:
|
|
util.parseIfHeaderDict(environ)
|
|
|
|
# Bail out, if res does not exist
|
|
if res is None:
|
|
return
|
|
|
|
ifDict = environ["wsgidav.conditions.if"]
|
|
|
|
# Raise HTTP_PRECONDITION_FAILED or HTTP_NOT_MODIFIED, if standard
|
|
# HTTP condition fails
|
|
lastmodified = -1 # nonvalid modified time
|
|
entitytag = "[]" # Non-valid entity tag
|
|
if res.getLastModified() is not None:
|
|
lastmodified = res.getLastModified()
|
|
if res.getEtag() is not None:
|
|
entitytag = res.getEtag()
|
|
|
|
if ("HTTP_IF_MODIFIED_SINCE" in environ
|
|
or "HTTP_IF_UNMODIFIED_SINCE" in environ
|
|
or "HTTP_IF_MATCH" in environ
|
|
or "HTTP_IF_NONE_MATCH" in environ):
|
|
util.evaluateHTTPConditionals(res, lastmodified, entitytag, environ)
|
|
|
|
if not "HTTP_IF" in environ:
|
|
return
|
|
|
|
# Raise HTTP_PRECONDITION_FAILED, if DAV 'If' condition fails
|
|
# TODO: handle empty locked resources
|
|
# TODO: handle unmapped locked resources
|
|
# isnewfile = not provider.exists(mappedpath)
|
|
|
|
refUrl = res.getRefUrl()
|
|
lockMan = self._davProvider.lockManager
|
|
locktokenlist = []
|
|
if lockMan:
|
|
lockList = lockMan.getIndirectUrlLockList(refUrl, environ["wsgidav.username"])
|
|
for lock in lockList:
|
|
locktokenlist.append(lock["token"])
|
|
|
|
if not util.testIfHeaderDict(res, ifDict, refUrl, locktokenlist, entitytag):
|
|
self._fail(HTTP_PRECONDITION_FAILED, "'If' header condition failed.")
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def doPROPFIND(self, environ, start_response):
|
|
"""
|
|
TODO: does not yet support If and If HTTP Conditions
|
|
@see http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
|
|
"""
|
|
path = environ["PATH_INFO"]
|
|
res = self._davProvider.getResourceInst(path, environ)
|
|
|
|
# RFC: By default, the PROPFIND method without a Depth header MUST act
|
|
# as if a "Depth: infinity" header was included.
|
|
environ.setdefault("HTTP_DEPTH", "infinity")
|
|
if not environ["HTTP_DEPTH"] in ("0", "1", "infinity"):
|
|
self._fail(HTTP_BAD_REQUEST,
|
|
"Invalid Depth header: '%s'." % environ["HTTP_DEPTH"])
|
|
|
|
if environ["HTTP_DEPTH"] == "infinity" and not self.allowPropfindInfinite:
|
|
self._fail(HTTP_FORBIDDEN,
|
|
"PROPFIND 'infinite' was disabled for security reasons.",
|
|
errcondition=PRECONDITION_CODE_PropfindFiniteDepth)
|
|
|
|
if res is None:
|
|
self._fail(HTTP_NOT_FOUND)
|
|
|
|
if environ.get("wsgidav.debug_break"):
|
|
pass # break point
|
|
|
|
self._evaluateIfHeaders(res, environ)
|
|
|
|
# Parse PROPFIND request
|
|
requestEL = util.parseXmlBody(environ, allowEmpty=True)
|
|
if requestEL is None:
|
|
# An empty PROPFIND request body MUST be treated as a request for
|
|
# the names and values of all properties.
|
|
requestEL = etree.XML("<D:propfind xmlns:D='DAV:'><D:allprop/></D:propfind>")
|
|
|
|
if requestEL.tag != "{DAV:}propfind":
|
|
self._fail(HTTP_BAD_REQUEST)
|
|
|
|
propNameList = []
|
|
propFindMode = None
|
|
for pfnode in requestEL:
|
|
if pfnode.tag == "{DAV:}allprop":
|
|
if propFindMode:
|
|
# RFC: allprop and propname are mutually exclusive
|
|
self._fail(HTTP_BAD_REQUEST)
|
|
propFindMode = "allprop"
|
|
# TODO: implement <include> option
|
|
# elif pfnode.tag == "{DAV:}include":
|
|
# if not propFindMode in (None, "allprop"):
|
|
# self._fail(HTTP_BAD_REQUEST, "<include> element is only valid with 'allprop'.")
|
|
# for pfpnode in pfnode:
|
|
# propNameList.append(pfpnode.tag)
|
|
elif pfnode.tag == "{DAV:}propname":
|
|
if propFindMode: # RFC: allprop and propname are mutually exclusive
|
|
self._fail(HTTP_BAD_REQUEST)
|
|
propFindMode = "propname"
|
|
elif pfnode.tag == "{DAV:}prop":
|
|
if propFindMode not in (None, "named"): # RFC: allprop and propname are mutually exclusive
|
|
self._fail(HTTP_BAD_REQUEST)
|
|
propFindMode = "named"
|
|
for pfpnode in pfnode:
|
|
propNameList.append(pfpnode.tag)
|
|
|
|
# --- Build list of resource URIs
|
|
|
|
reslist = res.getDescendants(depth=environ["HTTP_DEPTH"], addSelf=True)
|
|
# if environ["wsgidav.verbose"] >= 3:
|
|
# pprint(reslist, indent=4)
|
|
|
|
multistatusEL = xml_tools.makeMultistatusEL()
|
|
responsedescription = []
|
|
|
|
for child in reslist:
|
|
|
|
if propFindMode == "allprop":
|
|
propList = child.getProperties("allprop")
|
|
elif propFindMode == "propname":
|
|
propList = child.getProperties("propname")
|
|
else:
|
|
propList = child.getProperties("named", nameList=propNameList)
|
|
|
|
href = child.getHref()
|
|
util.addPropertyResponse(multistatusEL, href, propList)
|
|
|
|
if responsedescription:
|
|
etree.SubElement(multistatusEL, "{DAV:}responsedescription").text = "\n".join(responsedescription)
|
|
|
|
return util.sendMultiStatusResponse(environ, start_response, multistatusEL)
|
|
|
|
|
|
|
|
|
|
def doPROPPATCH(self, environ, start_response):
|
|
"""Handle PROPPATCH request to set or remove a property.
|
|
|
|
@see http://www.webdav.org/specs/rfc4918.html#METHOD_PROPPATCH
|
|
"""
|
|
path = environ["PATH_INFO"]
|
|
res = self._davProvider.getResourceInst(path, environ)
|
|
|
|
# Only accept Depth: 0 (but assume this, if omitted)
|
|
environ.setdefault("HTTP_DEPTH", "0")
|
|
if environ["HTTP_DEPTH"] != "0":
|
|
self._fail(HTTP_BAD_REQUEST, "Depth must be '0'.")
|
|
|
|
if res is None:
|
|
self._fail(HTTP_NOT_FOUND)
|
|
|
|
self._evaluateIfHeaders(res, environ)
|
|
self._checkWritePermission(res, "0", environ)
|
|
|
|
# Parse request
|
|
requestEL = util.parseXmlBody(environ)
|
|
|
|
if requestEL.tag != "{DAV:}propertyupdate":
|
|
self._fail(HTTP_BAD_REQUEST)
|
|
|
|
# Create a list of update request tuples: (propname, value)
|
|
propupdatelist = []
|
|
|
|
for ppnode in requestEL:
|
|
propupdatemethod = None
|
|
if ppnode.tag == "{DAV:}remove":
|
|
propupdatemethod = "remove"
|
|
elif ppnode.tag == "{DAV:}set":
|
|
propupdatemethod = "set"
|
|
else:
|
|
self._fail(HTTP_BAD_REQUEST, "Unknown tag (expected 'set' or 'remove').")
|
|
|
|
for propnode in ppnode:
|
|
if propnode.tag != "{DAV:}prop":
|
|
self._fail(HTTP_BAD_REQUEST, "Unknown tag (expected 'prop').")
|
|
|
|
for propertynode in propnode:
|
|
propvalue = None
|
|
if propupdatemethod == "remove":
|
|
propvalue = None # Mark as 'remove'
|
|
if len(propertynode) > 0:
|
|
# 14.23: All the XML elements in a 'prop' XML element inside of a 'remove' XML element MUST be empty
|
|
self._fail(HTTP_BAD_REQUEST, "prop element must be empty for 'remove'.")
|
|
else:
|
|
propvalue = propertynode
|
|
|
|
propupdatelist.append( (propertynode.tag, propvalue) )
|
|
|
|
# Apply updates in SIMULATION MODE and create a result list (propname, result)
|
|
successflag = True
|
|
writeresultlist = []
|
|
|
|
for (propname, propvalue) in propupdatelist:
|
|
try:
|
|
res.setPropertyValue(propname, propvalue, dryRun=True)
|
|
except Exception, e:
|
|
writeresult = asDAVError(e)
|
|
else:
|
|
writeresult = "200 OK"
|
|
writeresultlist.append( (propname, writeresult) )
|
|
successflag = successflag and writeresult == "200 OK"
|
|
|
|
# Generate response list of 2-tuples (name, value)
|
|
# <value> is None on success, or an instance of DAVError
|
|
propResponseList = []
|
|
responsedescription = []
|
|
|
|
if not successflag:
|
|
# If dry run failed: convert all OK to FAILED_DEPENDENCY.
|
|
for (propname, result) in writeresultlist:
|
|
if result == "200 OK":
|
|
result = DAVError(HTTP_FAILED_DEPENDENCY)
|
|
elif isinstance(result, DAVError):
|
|
responsedescription.append(result.getUserInfo())
|
|
propResponseList.append( (propname, result) )
|
|
|
|
else:
|
|
# Dry-run succeeded: set properties again, this time in 'real' mode
|
|
# In theory, there should be no exceptions thrown here, but this is real live...
|
|
for (propname, propvalue) in propupdatelist:
|
|
try:
|
|
res.setPropertyValue(propname, propvalue, dryRun=False)
|
|
# Set value to None, so the response xml contains empty tags
|
|
propResponseList.append( (propname, None) )
|
|
except Exception, e:
|
|
e = asDAVError(e)
|
|
propResponseList.append( (propname, e) )
|
|
responsedescription.append(e.getUserInfo())
|
|
|
|
# Generate response XML
|
|
multistatusEL = xml_tools.makeMultistatusEL()
|
|
# href = util.makeCompleteUrl(environ, path)
|
|
href = res.getHref()
|
|
util.addPropertyResponse(multistatusEL, href, propResponseList)
|
|
if responsedescription:
|
|
etree.SubElement(multistatusEL, "{DAV:}responsedescription").text = "\n".join(responsedescription)
|
|
|
|
# Send response
|
|
return util.sendMultiStatusResponse(environ, start_response, multistatusEL)
|
|
|
|
|
|
|
|
|
|
def doMKCOL(self, environ, start_response):
|
|
"""Handle MKCOL request to create a new collection.
|
|
|
|
@see http://www.webdav.org/specs/rfc4918.html#METHOD_MKCOL
|
|
"""
|
|
path = environ["PATH_INFO"]
|
|
provider = self._davProvider
|
|
# res = provider.getResourceInst(path, environ)
|
|
|
|
# Do not understand ANY request body entities
|
|
if util.getContentLength(environ) != 0:
|
|
self._fail(HTTP_MEDIATYPE_NOT_SUPPORTED,
|
|
"The server does not handle any body content.")
|
|
|
|
# Only accept Depth: 0 (but assume this, if omitted)
|
|
if environ.setdefault("HTTP_DEPTH", "0") != "0":
|
|
self._fail(HTTP_BAD_REQUEST, "Depth must be '0'.")
|
|
|
|
if provider.exists(path, environ):
|
|
self._fail(HTTP_METHOD_NOT_ALLOWED,
|
|
"MKCOL can only be executed on an unmapped URL.")
|
|
|
|
parentRes = provider.getResourceInst(util.getUriParent(path), environ)
|
|
if not parentRes or not parentRes.isCollection:
|
|
self._fail(HTTP_CONFLICT, "Parent must be an existing collection.")
|
|
|
|
# TODO: should we check If headers here?
|
|
# self._evaluateIfHeaders(res, environ)
|
|
# Check for write permissions on the PARENT
|
|
self._checkWritePermission(parentRes, "0", environ)
|
|
|
|
parentRes.createCollection(util.getUriName(path))
|
|
|
|
return util.sendStatusResponse(environ, start_response, HTTP_CREATED)
|
|
|
|
|
|
|
|
|
|
def doPOST(self, environ, start_response):
|
|
"""
|
|
@see http://www.webdav.org/specs/rfc4918.html#METHOD_POST
|
|
"""
|
|
self._fail(HTTP_METHOD_NOT_ALLOWED)
|
|
|
|
|
|
|
|
|
|
def doDELETE(self, environ, start_response):
|
|
"""
|
|
@see: http://www.webdav.org/specs/rfc4918.html#METHOD_DELETE
|
|
"""
|
|
path = environ["PATH_INFO"]
|
|
provider = self._davProvider
|
|
res = provider.getResourceInst(path, environ)
|
|
|
|
# --- Check request preconditions --------------------------------------
|
|
|
|
if util.getContentLength(environ) != 0:
|
|
self._fail(HTTP_MEDIATYPE_NOT_SUPPORTED,
|
|
"The server does not handle any body content.")
|
|
if res is None:
|
|
self._fail(HTTP_NOT_FOUND)
|
|
|
|
if res.isCollection:
|
|
# Delete over collection
|
|
# "The DELETE method on a collection MUST act as if a
|
|
# 'Depth: infinity' header was used on it. A client MUST NOT submit
|
|
# a Depth header with a DELETE on a collection with any value but
|
|
# infinity."
|
|
if environ.setdefault("HTTP_DEPTH", "infinity") != "infinity":
|
|
self._fail(HTTP_BAD_REQUEST,
|
|
"Only Depth: infinity is supported for collections.")
|
|
else:
|
|
if not environ.setdefault("HTTP_DEPTH", "0") in ("0", "infinity"):
|
|
self._fail(HTTP_BAD_REQUEST,
|
|
"Only Depth: 0 or infinity are supported for non-collections.")
|
|
|
|
self._evaluateIfHeaders(res, environ)
|
|
# We need write access on the parent collection. Also we check for
|
|
# locked children
|
|
parentRes = provider.getResourceInst(util.getUriParent(path), environ)
|
|
if parentRes:
|
|
# self._checkWritePermission(parentRes, environ["HTTP_DEPTH"], environ)
|
|
self._checkWritePermission(parentRes, "0", environ)
|
|
else:
|
|
# self._checkWritePermission(res, environ["HTTP_DEPTH"], environ)
|
|
self._checkWritePermission(res, "0", environ)
|
|
|
|
# --- Let provider handle the request natively -------------------------
|
|
|
|
# Errors in deletion; [ (<ref-url>, <DAVError>), ... ]
|
|
errorList = []
|
|
|
|
try:
|
|
handled = res.handleDelete()
|
|
assert handled in (True, False) or type(handled) is list
|
|
if type(handled) is list:
|
|
errorList = handled
|
|
handled = True
|
|
except Exception, e:
|
|
errorList = [ (res.getHref(), asDAVError(e)) ]
|
|
handled = True
|
|
if handled:
|
|
return self._sendResponse(environ, start_response,
|
|
res, HTTP_NO_CONTENT, errorList)
|
|
|
|
|
|
# --- Let provider implement own recursion -----------------------------
|
|
|
|
# Get a list of all resources (parents after children, so we can remove
|
|
# them in that order)
|
|
reverseChildList = res.getDescendants(depthFirst=True,
|
|
depth=environ["HTTP_DEPTH"],
|
|
addSelf=True)
|
|
|
|
if res.isCollection and res.supportRecursiveDelete():
|
|
hasConflicts = False
|
|
for childRes in reverseChildList:
|
|
try:
|
|
self._evaluateIfHeaders(childRes, environ)
|
|
self._checkWritePermission(childRes, "0", environ)
|
|
except:
|
|
hasConflicts = True
|
|
break
|
|
|
|
if not hasConflicts:
|
|
try:
|
|
errorList = res.delete()
|
|
except Exception, e:
|
|
errorList = [ (res.getHref(), asDAVError(e)) ]
|
|
return self._sendResponse(environ, start_response,
|
|
res, HTTP_NO_CONTENT, errorList)
|
|
|
|
# --- Implement file-by-file processing --------------------------------
|
|
|
|
# Hidden paths (ancestors of failed deletes) {<path>: True, ...}
|
|
ignoreDict = {}
|
|
for childRes in reverseChildList:
|
|
if childRes.path in ignoreDict:
|
|
_logger.debug("Skipping %s (contains error child)" % childRes.path)
|
|
ignoreDict[util.getUriParent(childRes.path)] = ""
|
|
continue
|
|
|
|
try:
|
|
# 9.6.1.: Any headers included with delete must be applied in
|
|
# processing every resource to be deleted
|
|
self._evaluateIfHeaders(childRes, environ)
|
|
self._checkWritePermission(childRes, "0", environ)
|
|
childRes.delete()
|
|
# Double-check, if deletion succeeded
|
|
if provider.exists(childRes.path, environ):
|
|
raise DAVError(HTTP_INTERNAL_ERROR,
|
|
"Resource could not be deleted.")
|
|
except Exception, e:
|
|
errorList.append( (childRes.getHref(), asDAVError(e)) )
|
|
ignoreDict[util.getUriParent(childRes.path)] = True
|
|
|
|
# --- Send response ----------------------------------------------------
|
|
|
|
return self._sendResponse(environ, start_response,
|
|
res, HTTP_NO_CONTENT, errorList)
|
|
|
|
|
|
|
|
|
|
def doPUT(self, environ, start_response):
|
|
"""
|
|
@see: http://www.webdav.org/specs/rfc4918.html#METHOD_PUT
|
|
"""
|
|
path = environ["PATH_INFO"]
|
|
provider = self._davProvider
|
|
res = provider.getResourceInst(path, environ)
|
|
parentRes = provider.getResourceInst(util.getUriParent(path), environ)
|
|
|
|
isnewfile = res is None
|
|
|
|
## Test for unsupported stuff
|
|
if "HTTP_CONTENT_ENCODING" in environ:
|
|
self._fail(HTTP_NOT_IMPLEMENTED,
|
|
"Content-encoding header is not supported.")
|
|
if "HTTP_CONTENT_RANGE" in environ:
|
|
self._fail(HTTP_NOT_IMPLEMENTED,
|
|
"Content-range header is not supported.")
|
|
|
|
if res and res.isCollection:
|
|
self._fail(HTTP_METHOD_NOT_ALLOWED, "Cannot PUT to a collection")
|
|
elif not parentRes.isCollection: # TODO: allow parentRes==None?
|
|
self._fail(HTTP_CONFLICT, "PUT parent must be a collection")
|
|
|
|
self._evaluateIfHeaders(res, environ)
|
|
|
|
if isnewfile:
|
|
self._checkWritePermission(parentRes, "0", environ)
|
|
res = parentRes.createEmptyResource(util.getUriName(path))
|
|
else:
|
|
self._checkWritePermission(res, "0", environ)
|
|
|
|
## Start Content Processing
|
|
# Content-Length may be 0 or greater. (Set to -1 if missing or invalid.)
|
|
# WORKAROUND_BAD_LENGTH = True
|
|
try:
|
|
contentlength = max(-1, long(environ.get("CONTENT_LENGTH", -1)))
|
|
except ValueError:
|
|
contentlength = -1
|
|
|
|
# if contentlength < 0 and not WORKAROUND_BAD_LENGTH:
|
|
if (
|
|
(contentlength < 0) and
|
|
(environ.get("HTTP_TRANSFER_ENCODING", "").lower() != "chunked")
|
|
):
|
|
# HOTFIX: not fully understood, but MS sends PUT without content-length,
|
|
# when creating new files
|
|
# if "Microsoft-WebDAV-MiniRedir" in environ.get("HTTP_USER_AGENT", ""):
|
|
# _logger.warning("Setting misssing Content-Length to 0 for MS client")
|
|
# contentlength = 0
|
|
# else:
|
|
# self._fail(HTTP_LENGTH_REQUIRED,
|
|
# "PUT request with invalid Content-Length: (%s)" % environ.get("CONTENT_LENGTH"))
|
|
contentlength = 0
|
|
|
|
hasErrors = False
|
|
try:
|
|
fileobj = res.beginWrite(contentType=environ.get("CONTENT_TYPE"))
|
|
|
|
if environ.get("HTTP_TRANSFER_ENCODING", "").lower() == "chunked":
|
|
buf = environ["wsgi.input"].readline()
|
|
if buf == '':
|
|
l = 0
|
|
else:
|
|
l = int(buf, 16)
|
|
|
|
environ["wsgidav.some_input_read"] = 1
|
|
while l > 0:
|
|
buf = environ["wsgi.input"].read(l)
|
|
fileobj.write(buf)
|
|
environ["wsgi.input"].readline()
|
|
buf = environ["wsgi.input"].readline()
|
|
if buf == '':
|
|
l = 0
|
|
else:
|
|
l = int(buf, 16)
|
|
environ["wsgidav.all_input_read"] = 1
|
|
|
|
elif contentlength == 0:
|
|
# TODO: review this
|
|
# XP and Vista MiniRedir submit PUT with Content-Length 0,
|
|
# before LOCK and the real PUT. So we have to accept this.
|
|
_logger.info("PUT: Content-Length == 0. Creating empty file...")
|
|
|
|
# elif contentlength < 0:
|
|
# # TODO: review this
|
|
# # If CONTENT_LENGTH is invalid, we may try to workaround this
|
|
# # by reading until the end of the stream. This may block however!
|
|
# # The iterator produced small chunks of varying size, but not
|
|
# # sure, if we always get everything before it times out.
|
|
# _logger.warning("PUT with invalid Content-Length (%s). Trying to read all (this may timeout)..." % environ.get("CONTENT_LENGTH"))
|
|
# nb = 0
|
|
# try:
|
|
# for s in environ["wsgi.input"]:
|
|
# environ["wsgidav.some_input_read"] = 1
|
|
# _logger.debug("PUT: read from wsgi.input.__iter__, len=%s" % len(s))
|
|
# fileobj.write(s)
|
|
# nb += len (s)
|
|
# except socket.timeout:
|
|
# _logger.warning("PUT: input timed out after writing %s bytes" % nb)
|
|
# hasErrors = True
|
|
else:
|
|
assert contentlength > 0
|
|
contentremain = contentlength
|
|
while contentremain > 0:
|
|
n = min(contentremain, BLOCK_SIZE)
|
|
readbuffer = environ["wsgi.input"].read(n)
|
|
# This happens with litmus expect-100 test:
|
|
# assert len(readbuffer) > 0, "input.read(%s) returned %s bytes" % (n, len(readbuffer))
|
|
if not len(readbuffer) > 0:
|
|
util.warn("input.read(%s) returned 0 bytes" % n)
|
|
break
|
|
environ["wsgidav.some_input_read"] = 1
|
|
fileobj.write(readbuffer)
|
|
contentremain -= len(readbuffer)
|
|
|
|
if contentremain == 0:
|
|
environ["wsgidav.all_input_read"] = 1
|
|
|
|
fileobj.close()
|
|
|
|
except Exception, e:
|
|
res.endWrite(withErrors=True)
|
|
_logger.exception("PUT: byte copy failed")
|
|
self._fail(e)
|
|
|
|
res.endWrite(hasErrors)
|
|
|
|
if isnewfile:
|
|
return util.sendStatusResponse(environ, start_response, HTTP_CREATED)
|
|
return util.sendStatusResponse(environ, start_response, HTTP_NO_CONTENT)
|
|
|
|
|
|
|
|
|
|
def doCOPY(self, environ, start_response):
|
|
return self._copyOrMove(environ, start_response, False)
|
|
|
|
|
|
|
|
|
|
def doMOVE(self, environ, start_response):
|
|
return self._copyOrMove(environ, start_response, True)
|
|
|
|
|
|
|
|
|
|
def _copyOrMove(self, environ, start_response, isMove):
|
|
"""
|
|
@see: http://www.webdav.org/specs/rfc4918.html#METHOD_COPY
|
|
@see: http://www.webdav.org/specs/rfc4918.html#METHOD_MOVE
|
|
"""
|
|
srcPath = environ["PATH_INFO"]
|
|
provider = self._davProvider
|
|
srcRes = provider.getResourceInst(srcPath, environ)
|
|
srcParentRes = provider.getResourceInst(util.getUriParent(srcPath), environ)
|
|
|
|
# --- Check source -----------------------------------------------------
|
|
|
|
if srcRes is None:
|
|
self._fail(HTTP_NOT_FOUND)
|
|
if "HTTP_DESTINATION" not in environ:
|
|
self._fail(HTTP_BAD_REQUEST, "Missing required Destination header.")
|
|
if not environ.setdefault("HTTP_OVERWRITE", "T") in ("T", "F"):
|
|
# Overwrite defaults to 'T'
|
|
self._fail(HTTP_BAD_REQUEST, "Invalid Overwrite header.")
|
|
if util.getContentLength(environ) != 0:
|
|
# RFC 2518 defined support for <propertybehavior>.
|
|
# This was dropped with RFC 4918.
|
|
# Still clients may send it (e.g. DAVExplorer 0.9.1 File-Copy) sends
|
|
# <A:propertybehavior xmlns:A="DAV:"> <A:keepalive>*</A:keepalive>
|
|
body = environ["wsgi.input"].read(util.getContentLength(environ))
|
|
environ["wsgidav.all_input_read"] = 1
|
|
_logger.info("Ignored copy/move body: '%s'..." % body[:50])
|
|
|
|
if srcRes.isCollection:
|
|
# The COPY method on a collection without a Depth header MUST act as
|
|
# if a Depth header with value "infinity" was included.
|
|
# A client may submit a Depth header on a COPY on a collection with
|
|
# a value of "0" or "infinity".
|
|
environ.setdefault("HTTP_DEPTH", "infinity")
|
|
if not environ["HTTP_DEPTH"] in ("0", "infinity"):
|
|
self._fail(HTTP_BAD_REQUEST, "Invalid Depth header.")
|
|
if isMove and environ["HTTP_DEPTH"] != "infinity":
|
|
self._fail(HTTP_BAD_REQUEST, "Depth header for MOVE collection must be 'infinity'.")
|
|
else:
|
|
# It's an existing non-collection: assume Depth 0
|
|
# Note: litmus 'copymove: 3 (copy_simple)' sends 'infinity' for a
|
|
# non-collection resource, so we accept that too
|
|
environ.setdefault("HTTP_DEPTH", "0")
|
|
if not environ["HTTP_DEPTH"] in ("0", "infinity"):
|
|
self._fail(HTTP_BAD_REQUEST, "Invalid Depth header.")
|
|
environ["HTTP_DEPTH"] = "0"
|
|
|
|
# --- Get destination path and check for cross-realm access ------------
|
|
|
|
# Destination header may be quoted (e.g. DAV Explorer sends unquoted,
|
|
# Windows quoted)
|
|
destinationHeader = urllib.unquote(environ["HTTP_DESTINATION"])
|
|
|
|
# Return fragments as part of <path>
|
|
# Fixes litmus -> running `basic': 9. delete_fragment....... WARNING: DELETE removed collection resource withRequest-URI including fragment; unsafe
|
|
destScheme, destNetloc, destPath, \
|
|
_destParams, _destQuery, _destFrag = urlparse(destinationHeader,
|
|
allow_fragments=False)
|
|
|
|
if srcRes.isCollection:
|
|
destPath = destPath.rstrip("/") + "/"
|
|
|
|
# Remove username
|
|
atLoc = destNetloc.rfind("@")
|
|
if atLoc != -1:
|
|
destNetloc = destNetloc[atLoc+1:]
|
|
|
|
# if destScheme and destScheme.lower() != environ["wsgi.url_scheme"].lower():
|
|
# self._fail(HTTP_BAD_GATEWAY,
|
|
# "Source and destination must have the same scheme.")
|
|
# elif destNetloc and destNetloc.lower() != environ["HTTP_HOST"].lower():
|
|
# # TODO: this should consider environ["SERVER_PORT"] also
|
|
# self._fail(HTTP_BAD_GATEWAY,
|
|
# "Source and destination must have the same host name.")
|
|
# elif not destPath.startswith(provider.mountPath + provider.sharePath):
|
|
# # Inter-realm copying not supported, since its not possible to
|
|
# # authentication-wise
|
|
# self._fail(HTTP_BAD_GATEWAY,
|
|
# "Inter-realm copy/move is not supported.")
|
|
|
|
destPath = destPath[len(provider.mountPath + provider.sharePath):]
|
|
assert destPath.startswith("/")
|
|
|
|
# destPath is now relative to current mount/share starting with '/'
|
|
|
|
destRes = provider.getResourceInst(destPath, environ)
|
|
destExists = destRes is not None
|
|
|
|
destParentRes = provider.getResourceInst(util.getUriParent(destPath), environ)
|
|
|
|
if not destParentRes or not destParentRes.isCollection:
|
|
self._fail(HTTP_CONFLICT, "Destination parent must be a collection.")
|
|
|
|
self._evaluateIfHeaders(srcRes, environ)
|
|
self._evaluateIfHeaders(destRes, environ)
|
|
# Check permissions
|
|
# http://www.webdav.org/specs/rfc4918.html#rfc.section.7.4
|
|
if isMove:
|
|
self._checkWritePermission(srcRes, "infinity", environ)
|
|
# Cannot remove members from locked-0 collections
|
|
if srcParentRes:
|
|
self._checkWritePermission(srcParentRes, "0", environ)
|
|
|
|
# Cannot create or new members in locked-0 collections
|
|
if not destExists:
|
|
self._checkWritePermission(destParentRes, "0", environ)
|
|
# If target exists, it must not be locked
|
|
self._checkWritePermission(destRes, "infinity", environ)
|
|
|
|
if srcPath == destPath:
|
|
self._fail(HTTP_FORBIDDEN, "Cannot copy/move source onto itself")
|
|
elif util.isEqualOrChildUri(srcPath, destPath):
|
|
self._fail(HTTP_FORBIDDEN, "Cannot copy/move source below itself")
|
|
|
|
if destExists and environ["HTTP_OVERWRITE"] != "T":
|
|
self._fail(HTTP_PRECONDITION_FAILED,
|
|
"Destination already exists and Overwrite is set to false")
|
|
|
|
# --- Let provider handle the request natively -------------------------
|
|
|
|
# Errors in copy/move; [ (<ref-url>, <DAVError>), ... ]
|
|
errorList = []
|
|
successCode = HTTP_CREATED
|
|
if destExists:
|
|
successCode = HTTP_NO_CONTENT
|
|
|
|
try:
|
|
if isMove:
|
|
handled = srcRes.handleMove(destPath)
|
|
else:
|
|
isInfinity = environ["HTTP_DEPTH"] == "infinity"
|
|
handled = srcRes.handleCopy(destPath, isInfinity)
|
|
assert handled in (True, False) or type(handled) is list
|
|
if type(handled) is list:
|
|
errorList = handled
|
|
handled = True
|
|
except Exception, e:
|
|
errorList = [ (srcRes.getHref(), asDAVError(e)) ]
|
|
handled = True
|
|
if handled:
|
|
return self._sendResponse(environ, start_response,
|
|
srcRes, HTTP_NO_CONTENT, errorList)
|
|
|
|
# --- Cleanup destination before copy/move -----------------------------
|
|
|
|
srcList = srcRes.getDescendants(addSelf=True)
|
|
|
|
srcRootLen = len(srcPath)
|
|
destRootLen = len(destPath)
|
|
|
|
if destExists:
|
|
if (isMove
|
|
or not destRes.isCollection
|
|
or not srcRes.isCollection ):
|
|
# MOVE:
|
|
# If a resource exists at the destination and the Overwrite
|
|
# header is "T", then prior to performing the move, the server
|
|
# MUST perform a DELETE with "Depth: infinity" on the
|
|
# destination resource.
|
|
_logger.debug("Remove dest before move: '%s'" % destRes)
|
|
destRes.delete()
|
|
destRes = None
|
|
else:
|
|
# COPY collection over collection:
|
|
# Remove destination files, that are not part of source, because
|
|
# source and dest collections must not be merged (9.8.4).
|
|
# This is not the same as deleting the complete dest collection
|
|
# before copying, because that would also discard the history of
|
|
# existing resources.
|
|
reverseDestList = destRes.getDescendants(depthFirst=True, addSelf=False)
|
|
srcPathList = [ s.path for s in srcList ]
|
|
_logger.debug("check srcPathList: %s" % srcPathList)
|
|
for dRes in reverseDestList:
|
|
_logger.debug("check unmatched dest before copy: %s" % dRes)
|
|
relUrl = dRes.path[destRootLen:]
|
|
sp = srcPath + relUrl
|
|
if not sp in srcPathList:
|
|
_logger.debug("Remove unmatched dest before copy: %s" % dRes)
|
|
dRes.delete()
|
|
|
|
# --- Let provider implement recursive move ----------------------------
|
|
# We do this only, if the provider supports it, and no conflicts exist.
|
|
# A provider can implement this very efficiently, without allocating
|
|
# double memory as a copy/delete approach would.
|
|
|
|
if isMove and srcRes.supportRecursiveMove(destPath):
|
|
hasConflicts = False
|
|
for s in srcList:
|
|
try:
|
|
self._evaluateIfHeaders(s, environ)
|
|
except:
|
|
hasConflicts = True
|
|
break
|
|
|
|
if not hasConflicts:
|
|
try:
|
|
_logger.debug("Recursive move: %s -> '%s'" % (srcRes, destPath))
|
|
errorList = srcRes.moveRecursive(destPath)
|
|
except Exception, e:
|
|
errorList = [ (srcRes.getHref(), asDAVError(e)) ]
|
|
return self._sendResponse(environ, start_response,
|
|
srcRes, successCode, errorList)
|
|
|
|
# --- Copy/move file-by-file using copy/delete -------------------------
|
|
|
|
# We get here, if
|
|
# - the provider does not support recursive moves
|
|
# - this is a copy request
|
|
# In this case we would probably not win too much by a native provider
|
|
# implementation, since we had to handle single child errors anyway.
|
|
# - the source tree is partially locked
|
|
# We would have to pass this information to the native provider.
|
|
|
|
# Hidden paths (paths of failed copy/moves) {<src_path>: True, ...}
|
|
ignoreDict = {}
|
|
|
|
for sRes in srcList:
|
|
# Skip this resource, if there was a failure copying a parent
|
|
parentError = False
|
|
for ignorePath in ignoreDict.keys():
|
|
if util.isEqualOrChildUri(ignorePath, sRes.path):
|
|
parentError = True
|
|
break
|
|
if parentError:
|
|
_logger.debug("Copy: skipping '%s', because of parent error" % sRes.path)
|
|
continue
|
|
|
|
try:
|
|
relUrl = sRes.path[srcRootLen:]
|
|
dPath = destPath + relUrl
|
|
|
|
self._evaluateIfHeaders(sRes, environ)
|
|
|
|
# We copy resources and their properties top-down.
|
|
# Collections are simply created (without members), for
|
|
# non-collections bytes are copied (overwriting target)
|
|
sRes.copyMoveSingle(dPath, isMove)
|
|
|
|
# If copy succeeded, and it was a non-collection delete it now.
|
|
# So the source tree shrinks while the destination grows and we
|
|
# don't have to allocate the memory twice.
|
|
# We cannot remove collections here, because we have not yet
|
|
# copied all children.
|
|
if isMove and not sRes.isCollection:
|
|
sRes.delete()
|
|
|
|
except Exception, e:
|
|
ignoreDict[sRes.path] = True
|
|
# TODO: the error-href should be 'most appropriate of the source
|
|
# and destination URLs'. So maybe this should be the destination
|
|
# href sometimes.
|
|
# http://www.webdav.org/specs/rfc4918.html#rfc.section.9.8.5
|
|
errorList.append( (sRes.getHref(), asDAVError(e)) )
|
|
|
|
# MOVE: Remove source tree (bottom-up)
|
|
if isMove:
|
|
reverseSrcList = srcList[:]
|
|
reverseSrcList.reverse()
|
|
util.status("Delete after move, ignore=", var=ignoreDict)
|
|
for sRes in reverseSrcList:
|
|
# Non-collections have already been removed in the copy loop.
|
|
if not sRes.isCollection:
|
|
continue
|
|
# Skip collections that contain errors (unmoved resources)
|
|
childError = False
|
|
for ignorePath in ignoreDict.keys():
|
|
if util.isEqualOrChildUri(sRes.path, ignorePath):
|
|
childError = True
|
|
break
|
|
if childError:
|
|
util.status("Delete after move: skipping '%s', because of child error" % sRes.path)
|
|
continue
|
|
|
|
try:
|
|
# _logger.debug("Remove source after move: %s" % sRes)
|
|
util.status("Remove collection after move: %s" % sRes)
|
|
sRes.delete()
|
|
except Exception, e:
|
|
errorList.append( (srcRes.getHref(), asDAVError(e)) )
|
|
util.status("ErrorList", var=errorList)
|
|
|
|
# --- Return response --------------------------------------------------
|
|
|
|
return self._sendResponse(environ, start_response,
|
|
srcRes, successCode, errorList)
|
|
|
|
|
|
|
|
|
|
def doLOCK(self, environ, start_response):
|
|
"""
|
|
@see: http://www.webdav.org/specs/rfc4918.html#METHOD_LOCK
|
|
"""
|
|
path = environ["PATH_INFO"]
|
|
provider = self._davProvider
|
|
res = provider.getResourceInst(path, environ)
|
|
lockMan = provider.lockManager
|
|
|
|
if lockMan is None:
|
|
# http://www.webdav.org/specs/rfc4918.html#rfc.section.6.3
|
|
self._fail(HTTP_NOT_IMPLEMENTED,
|
|
"This realm does not support locking.")
|
|
if res and res.preventLocking():
|
|
self._fail(HTTP_FORBIDDEN,
|
|
"This resource does not support locking.")
|
|
|
|
if environ.setdefault("HTTP_DEPTH", "infinity") not in ("0", "infinity"):
|
|
self._fail(HTTP_BAD_REQUEST, "Expected Depth: 'infinity' or '0'.")
|
|
|
|
self._evaluateIfHeaders(res, environ)
|
|
|
|
timeoutsecs = util.readTimeoutValueHeader(environ.get("HTTP_TIMEOUT", ""))
|
|
submittedTokenList = environ["wsgidav.ifLockTokenList"]
|
|
|
|
lockinfoEL = util.parseXmlBody(environ, allowEmpty=True)
|
|
|
|
# --- Special case: empty request body ---------------------------------
|
|
|
|
if lockinfoEL is None:
|
|
# TODO: @see 9.10.2
|
|
# TODO: 'URL of a resource within the scope of the lock'
|
|
# Other (shared) locks are unaffected and don't prevent refreshing
|
|
# TODO: check for valid user
|
|
# TODO: check for If with single lock token
|
|
environ["HTTP_DEPTH"] = "0" # MUST ignore depth header on refresh
|
|
|
|
if res is None:
|
|
self._fail(HTTP_BAD_REQUEST,
|
|
"LOCK refresh must specify an existing resource.")
|
|
if len(submittedTokenList) != 1:
|
|
self._fail(HTTP_BAD_REQUEST,
|
|
"Expected a lock token (only one lock may be refreshed at a time).")
|
|
elif not lockMan.isUrlLockedByToken(res.getRefUrl(), submittedTokenList[0]):
|
|
self._fail(HTTP_PRECONDITION_FAILED,
|
|
"Lock token does not match URL.",
|
|
errcondition=PRECONDITION_CODE_LockTokenMismatch)
|
|
# TODO: test, if token is owned by user
|
|
|
|
lock = lockMan.refresh(submittedTokenList[0], timeoutsecs)
|
|
|
|
# The lock root may be <path>, or a parent of <path>.
|
|
lockPath = provider.refUrlToPath(lock["root"])
|
|
lockRes = provider.getResourceInst(lockPath, environ)
|
|
|
|
propEL = xml_tools.makePropEL()
|
|
# TODO: handle exceptions in getPropertyValue
|
|
lockdiscoveryEL = lockRes.getPropertyValue("{DAV:}lockdiscovery")
|
|
propEL.append(lockdiscoveryEL)
|
|
|
|
# Lock-Token header is not returned
|
|
xml = xml_tools.xmlToString(propEL)
|
|
start_response("200 OK", [("Content-Type", "application/xml"),
|
|
("Content-Length", str(len(xml))),
|
|
("Date", util.getRfc1123Time()),
|
|
])
|
|
return [ xml ]
|
|
|
|
# --- Standard case: parse xml body ------------------------------------
|
|
|
|
if lockinfoEL.tag != "{DAV:}lockinfo":
|
|
self._fail(HTTP_BAD_REQUEST)
|
|
|
|
locktype = None
|
|
lockscope = None
|
|
lockowner = ""
|
|
lockdepth = environ.setdefault("HTTP_DEPTH", "infinity")
|
|
|
|
for linode in lockinfoEL:
|
|
if linode.tag == "{DAV:}lockscope":
|
|
for lsnode in linode:
|
|
if lsnode.tag == "{DAV:}exclusive":
|
|
lockscope = "exclusive"
|
|
elif lsnode.tag == "{DAV:}shared":
|
|
lockscope = "shared"
|
|
break
|
|
elif linode.tag == "{DAV:}locktype":
|
|
for ltnode in linode:
|
|
if ltnode.tag == "{DAV:}write":
|
|
locktype = "write" # only type accepted
|
|
break
|
|
|
|
elif linode.tag == "{DAV:}owner":
|
|
# Store whole <owner> tag, so we can use etree.XML() later
|
|
lockowner = xml_tools.xmlToString(linode, pretty_print=False)
|
|
|
|
else:
|
|
self._fail(HTTP_BAD_REQUEST, "Invalid node '%s'." % linode.tag)
|
|
|
|
if not lockscope:
|
|
self._fail(HTTP_BAD_REQUEST, "Missing or invalid lockscope.")
|
|
if not locktype:
|
|
self._fail(HTTP_BAD_REQUEST, "Missing or invalid locktype.")
|
|
|
|
if environ.get("wsgidav.debug_break"):
|
|
pass # break point
|
|
|
|
# TODO: check for locked parents BEFORE creating an empty child
|
|
|
|
# http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10.4
|
|
# Locking unmapped URLs: must create an empty resource
|
|
createdNewResource = False
|
|
if res is None:
|
|
parentRes = provider.getResourceInst(util.getUriParent(path), environ)
|
|
if not parentRes or not parentRes.isCollection:
|
|
self._fail(HTTP_CONFLICT, "LOCK-0 parent must be a collection")
|
|
res = parentRes.createEmptyResource(util.getUriName(path))
|
|
createdNewResource = True
|
|
|
|
# --- Check, if path is already locked ---------------------------------
|
|
|
|
# May raise DAVError(HTTP_LOCKED):
|
|
lock = lockMan.acquire(res.getRefUrl(),
|
|
locktype, lockscope, lockdepth,
|
|
lockowner, timeoutsecs,
|
|
environ["wsgidav.username"],
|
|
submittedTokenList)
|
|
|
|
# Lock succeeded
|
|
propEL = xml_tools.makePropEL()
|
|
# TODO: handle exceptions in getPropertyValue
|
|
lockdiscoveryEL = res.getPropertyValue("{DAV:}lockdiscovery")
|
|
propEL.append(lockdiscoveryEL)
|
|
|
|
respcode = "200 OK"
|
|
if createdNewResource:
|
|
respcode = "201 Created"
|
|
|
|
xml = xml_tools.xmlToString(propEL)
|
|
start_response(respcode, [("Content-Type", "application/xml"),
|
|
("Content-Length", str(len(xml))),
|
|
("Lock-Token", lock["token"]),
|
|
("Date", util.getRfc1123Time()),
|
|
])
|
|
return [ xml ]
|
|
|
|
# TODO: LOCK may also fail with HTTP_FORBIDDEN.
|
|
# In this case we should return 207 multi-status.
|
|
# http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10.9
|
|
# Checking this would require to call res.preventLocking()
|
|
# recursively.
|
|
|
|
# # --- Locking FAILED: return fault response
|
|
# if len(conflictList) == 1 and conflictList[0][0]["root"] == res.getRefUrl():
|
|
# # If there is only one error for the root URL, send as simple error response
|
|
# return util.sendStatusResponse(environ, start_response, conflictList[0][1])
|
|
#
|
|
# dictStatus = {}
|
|
#
|
|
# for lockDict, e in conflictList:
|
|
# dictStatus[lockDict["root"]] = e
|
|
#
|
|
# if not res.getRefUrl() in dictStatus:
|
|
# dictStatus[res.getRefUrl()] = DAVError(HTTP_FAILED_DEPENDENCY)
|
|
#
|
|
# # Return multi-status fault response
|
|
# multistatusEL = xml_tools.makeMultistatusEL()
|
|
# for nu, e in dictStatus.items():
|
|
# responseEL = etree.SubElement(multistatusEL, "{DAV:}response")
|
|
# etree.SubElement(responseEL, "{DAV:}href").text = nu
|
|
# etree.SubElement(responseEL, "{DAV:}status").text = "HTTP/1.1 %s" % getHttpStatusString(e)
|
|
# # TODO: all responses should have this(?):
|
|
# if e.contextinfo:
|
|
# etree.SubElement(multistatusEL, "{DAV:}responsedescription").text = e.contextinfo
|
|
#
|
|
## if responsedescription:
|
|
## etree.SubElement(multistatusEL, "{DAV:}responsedescription").text = "\n".join(responsedescription)
|
|
#
|
|
# return util.sendMultiStatusResponse(environ, start_response, multistatusEL)
|
|
|
|
|
|
|
|
|
|
def doUNLOCK(self, environ, start_response):
|
|
"""
|
|
@see: http://www.webdav.org/specs/rfc4918.html#METHOD_UNLOCK
|
|
"""
|
|
path = environ["PATH_INFO"]
|
|
provider = self._davProvider
|
|
res = self._davProvider.getResourceInst(path, environ)
|
|
|
|
lockMan = provider.lockManager
|
|
if lockMan is None:
|
|
self._fail(HTTP_NOT_IMPLEMENTED, "This share does not support locking.")
|
|
elif util.getContentLength(environ) != 0:
|
|
self._fail(HTTP_MEDIATYPE_NOT_SUPPORTED,
|
|
"The server does not handle any body content.")
|
|
elif not "HTTP_LOCK_TOKEN" in environ:
|
|
self._fail(HTTP_BAD_REQUEST, "Missing lock token.")
|
|
|
|
self._evaluateIfHeaders(res, environ)
|
|
|
|
lockToken = environ["HTTP_LOCK_TOKEN"].strip("<>")
|
|
refUrl = res.getRefUrl()
|
|
|
|
if not lockMan.isUrlLockedByToken(refUrl, lockToken):
|
|
self._fail(HTTP_CONFLICT,
|
|
"Resource is not locked by token.",
|
|
errcondition=PRECONDITION_CODE_LockTokenMismatch)
|
|
|
|
if not lockMan.isTokenLockedByUser(lockToken, environ["wsgidav.username"]):
|
|
# TODO: there must be a way to allow this for admins.
|
|
# Maybe test for "remove_locks" in environ["wsgidav.roles"]
|
|
self._fail(HTTP_FORBIDDEN, "Token was created by another user.")
|
|
|
|
# TODO: Is this correct?: unlock(a/b/c) will remove Lock for 'a/b'
|
|
lockMan.release(lockToken)
|
|
|
|
return util.sendStatusResponse(environ, start_response, HTTP_NO_CONTENT)
|
|
|
|
|
|
|
|
|
|
def doOPTIONS(self, environ, start_response):
|
|
"""
|
|
@see http://www.webdav.org/specs/rfc4918.html#HEADER_DAV
|
|
"""
|
|
path = environ["PATH_INFO"]
|
|
provider = self._davProvider
|
|
res = provider.getResourceInst(path, environ)
|
|
|
|
headers = [("Content-Type", "text/html"),
|
|
("Content-Length", "0"),
|
|
("DAV", "1,2"), # TODO: 10.1: 'OPTIONS MUST return DAV header with compliance class "1"'
|
|
# TODO: 10.1: In cases where WebDAV is only supported in part of the server namespace, an OPTIONS request to non-WebDAV resources (including "/") SHOULD NOT advertise WebDAV support
|
|
("Server", "DAV/2"),
|
|
("Date", util.getRfc1123Time()),
|
|
]
|
|
|
|
if path == "/":
|
|
path = "*" # Hotfix for WinXP
|
|
|
|
if path == "*":
|
|
# Answer HTTP 'OPTIONS' method on server-level.
|
|
# From RFC 2616
|
|
# If the Request-URI is an asterisk ("*"), the OPTIONS request is
|
|
# intended to apply to the server in general rather than to a specific
|
|
# resource. Since a server's communication options typically depend on
|
|
# the resource, the "*" request is only useful as a "ping" or "no-op"
|
|
# type of method; it does nothing beyond allowing the client to test the
|
|
# capabilities of the server. For example, this can be used to test a
|
|
# proxy for HTTP/1.1 compliance (or lack thereof).
|
|
start_response("200 OK", headers)
|
|
return [""]
|
|
|
|
# TODO: should we have something like provider.isReadOnly() and then omit MKCOL PUT DELETE PROPPATCH COPY MOVE?
|
|
# TODO: LOCK UNLOCK is only available, if lockmanager not None
|
|
if res and res.isCollection:
|
|
# Existing collection
|
|
headers.append( ("Allow", "OPTIONS HEAD GET DELETE PROPFIND PROPPATCH COPY MOVE LOCK UNLOCK") )
|
|
elif res:
|
|
# Existing resource
|
|
headers.append( ("Allow", "OPTIONS HEAD GET PUT DELETE PROPFIND PROPPATCH COPY MOVE LOCK UNLOCK") )
|
|
if res.supportRanges():
|
|
headers.append( ("Allow-Ranges", "bytes") )
|
|
elif provider.isCollection(util.getUriParent(path), environ):
|
|
# A new resource below an existing collection
|
|
# TODO: should we allow LOCK here? I think it is allowed to lock an non-existing resource
|
|
headers.append( ("Allow", "OPTIONS PUT MKCOL") )
|
|
else:
|
|
self._fail(HTTP_NOT_FOUND)
|
|
|
|
start_response("200 OK", headers)
|
|
return [""]
|
|
|
|
|
|
|
|
|
|
def doGET(self, environ, start_response):
|
|
return self._sendResource(environ, start_response, isHeadMethod=False)
|
|
|
|
|
|
def doHEAD(self, environ, start_response):
|
|
return self._sendResource(environ, start_response, isHeadMethod=True)
|
|
|
|
|
|
def _sendResource(self, environ, start_response, isHeadMethod):
|
|
"""
|
|
If-Range
|
|
If the entity is unchanged, send me the part(s) that I am missing;
|
|
otherwise, send me the entire new entity
|
|
If-Range: "737060cd8c284d8af7ad3082f209582d"
|
|
|
|
@see: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.27
|
|
"""
|
|
path = environ["PATH_INFO"]
|
|
res = self._davProvider.getResourceInst(path, environ)
|
|
|
|
if util.getContentLength(environ) != 0:
|
|
self._fail(HTTP_MEDIATYPE_NOT_SUPPORTED,
|
|
"The server does not handle any body content.")
|
|
elif environ.setdefault("HTTP_DEPTH", "0") != "0":
|
|
self._fail(HTTP_BAD_REQUEST, "Only Depth: 0 supported.")
|
|
elif res is None:
|
|
self._fail(HTTP_NOT_FOUND)
|
|
elif res.isCollection:
|
|
self._fail(HTTP_FORBIDDEN,
|
|
"Directory browsing not enabled (WsgiDavDirBrowser middleware may be enabled using the dir_browser option).")
|
|
|
|
self._evaluateIfHeaders(res, environ)
|
|
|
|
filesize = res.getContentLength()
|
|
if filesize is None:
|
|
filesize = -1 # flag logic to read until EOF
|
|
|
|
lastmodified = res.getLastModified()
|
|
if lastmodified is None:
|
|
lastmodified = -1
|
|
|
|
entitytag = res.getEtag()
|
|
if entitytag is None:
|
|
entitytag = "[]"
|
|
|
|
## Ranges
|
|
doignoreranges = (not res.supportContentLength()
|
|
or not res.supportRanges()
|
|
or filesize == 0)
|
|
if "HTTP_RANGE" in environ and "HTTP_IF_RANGE" in environ and not doignoreranges:
|
|
ifrange = environ["HTTP_IF_RANGE"]
|
|
# Try as http-date first (Return None, if invalid date string)
|
|
secstime = util.parseTimeString(ifrange)
|
|
if secstime:
|
|
if lastmodified != secstime:
|
|
doignoreranges = True
|
|
else:
|
|
# Use as entity tag
|
|
ifrange = ifrange.strip("\" ")
|
|
if entitytag is None or ifrange != entitytag:
|
|
doignoreranges = True
|
|
|
|
ispartialranges = False
|
|
if "HTTP_RANGE" in environ and not doignoreranges:
|
|
ispartialranges = True
|
|
listRanges, _totallength = util.obtainContentRanges(environ["HTTP_RANGE"], filesize)
|
|
if len(listRanges) == 0:
|
|
#No valid ranges present
|
|
self._fail(HTTP_RANGE_NOT_SATISFIABLE)
|
|
|
|
# More than one range present -> take only the first range, since
|
|
# multiple range returns require multipart, which is not supported
|
|
# obtainContentRanges supports more than one range in case the above
|
|
# behaviour changes in future
|
|
(rangestart, rangeend, rangelength) = listRanges[0]
|
|
else:
|
|
(rangestart, rangeend, rangelength) = (0L, filesize - 1, filesize)
|
|
|
|
## Content Processing
|
|
mimetype = res.getContentType() #provider.getContentType(path)
|
|
|
|
responseHeaders = []
|
|
if res.supportContentLength():
|
|
# Content-length must be of type string (otherwise CherryPy server chokes)
|
|
responseHeaders.append(("Content-Length", str(rangelength)))
|
|
if res.supportModified():
|
|
responseHeaders.append(("Last-Modified", util.getRfc1123Time(lastmodified)))
|
|
responseHeaders.append(("Content-Type", mimetype))
|
|
responseHeaders.append(("Date", util.getRfc1123Time()))
|
|
if res.supportEtag():
|
|
responseHeaders.append(("ETag", '"%s"' % entitytag))
|
|
|
|
if ispartialranges:
|
|
# responseHeaders.append(("Content-Ranges", "bytes " + str(rangestart) + "-" + str(rangeend) + "/" + str(rangelength)))
|
|
responseHeaders.append(("Content-Range", "bytes %s-%s/%s" % (rangestart, rangeend, filesize)))
|
|
start_response("206 Partial Content", responseHeaders)
|
|
else:
|
|
start_response("200 OK", responseHeaders)
|
|
|
|
# Return empty body for HEAD requests
|
|
if isHeadMethod:
|
|
yield ""
|
|
return
|
|
|
|
fileobj = res.getContent()
|
|
|
|
if not doignoreranges:
|
|
fileobj.seek(rangestart)
|
|
|
|
contentlengthremaining = rangelength
|
|
while 1:
|
|
if contentlengthremaining < 0 or contentlengthremaining > BLOCK_SIZE:
|
|
readbuffer = fileobj.read(BLOCK_SIZE)
|
|
else:
|
|
readbuffer = fileobj.read(contentlengthremaining)
|
|
yield readbuffer
|
|
contentlengthremaining -= len(readbuffer)
|
|
if len(readbuffer) == 0 or contentlengthremaining == 0:
|
|
break
|
|
fileobj.close()
|
|
return
|
|
|
|
|
|
|
|
|
|
# def doTRACE(self, environ, start_response):
|
|
# """ TODO: TRACE pending, but not essential."""
|
|
# self._fail(HTTP_NOT_IMPLEMENTED)
|