diff --git a/pywebdav/lib/AuthServer.py b/pywebdav/lib/AuthServer.py index 26a26a1..d9092c7 100644 --- a/pywebdav/lib/AuthServer.py +++ b/pywebdav/lib/AuthServer.py @@ -10,9 +10,9 @@ import six.moves.BaseHTTPServer -DEFAULT_AUTH_ERROR_MESSAGE = b""" +DEFAULT_AUTH_ERROR_MESSAGE = """ -%(code)d - %(message)b +%(code)d - %(message)s

Authorization Required

@@ -26,7 +26,7 @@ def _quote_html(html): - return html.replace(b"&", b"&").replace(b"<", b"<").replace(b">", b">") + return html.replace("&", "&").replace("<", "<").replace(">", ">") class AuthRequestHandler(six.moves.BaseHTTPServer.BaseHTTPRequestHandler): @@ -48,7 +48,7 @@ def parse_request(self): if self.DO_AUTH: authorization = self.headers.get('Authorization', '') if not authorization: - self.send_autherror(401, b"Authorization Required") + self.send_autherror(401, "Authorization Required") return False scheme, credentials = authorization.split() if scheme != 'Basic': @@ -57,7 +57,7 @@ def parse_request(self): credentials = base64.decodebytes(credentials.encode()).decode() user, password = credentials.split(':') if not self.get_userinfo(user, password, self.command): - self.send_autherror(401, b"Authorization Required") + self.send_autherror(401, "Authorization Required") return False return True @@ -76,7 +76,7 @@ def send_autherror(self, code, message=None): try: short, long = self.responses[code] except KeyError: - short, long = b'???', b'???' + short, long = '???', '???' if message is None: message = short explain = long @@ -84,14 +84,14 @@ def send_autherror(self, code, message=None): # using _quote_html to prevent Cross Site Scripting attacks (see bug # #1100201) - content = (self.error_auth_message_format % {b'code': code, b'message': - _quote_html(message), b'explain': explain}) + content = (self.error_auth_message_format % {'code': code, 'message': + _quote_html(message), 'explain': explain}) self.send_response(code, message) self.send_header('Content-Type', self.error_content_type) self.send_header('WWW-Authenticate', 'Basic realm="PyWebDAV"') self.send_header('Connection', 'close') self.end_headers() - self.wfile.write(content) + self.wfile.write(content.encode('utf-8')) error_auth_message_format = DEFAULT_AUTH_ERROR_MESSAGE diff --git a/pywebdav/lib/WebDAVServer.py b/pywebdav/lib/WebDAVServer.py index 8e72296..092e125 100644 --- a/pywebdav/lib/WebDAVServer.py +++ b/pywebdav/lib/WebDAVServer.py @@ -69,7 +69,6 @@ def send_body(self, DATA, code=None, msg=None, desc=None, self._send_dav_version() for a, v in headers.items(): - v = v.encode() if isinstance(v, six.text_type) else v self.send_header(a, v) if DATA: @@ -97,7 +96,9 @@ def send_body(self, DATA, code=None, msg=None, desc=None, self.end_headers() if DATA: - if isinstance(DATA, str) or isinstance(DATA, six.text_type) or isinstance(DATA, bytes): + if isinstance(DATA, str): + DATA = DATA.encode('utf-8') + if isinstance(DATA, six.text_type) or isinstance(DATA, bytes): log.debug("Don't use iterator") self.wfile.write(DATA) else: @@ -224,8 +225,7 @@ def _HEAD_GET(self, with_body=False): """ Returns headers and body for given resource """ dc = self.IFACE_CLASS - uri = urllib.parse.urljoin(self.get_baseuri(dc), self.path) - uri = urllib.parse.unquote(uri).encode() + uri = urllib.parse.unquote(urllib.parse.urljoin(self.get_baseuri(dc), self.path)) headers = {} @@ -244,7 +244,7 @@ def _HEAD_GET(self, with_body=False): # get the content type try: - if uri.endswith(b'/'): + if uri.endswith('/'): # we could do away with this very non-local workaround for # _get_listing if the data could have a type attached content_type = 'text/html;charset=utf-8' @@ -331,8 +331,7 @@ def do_PROPFIND(self): l = self.headers['Content-Length'] body = self.rfile.read(int(l)) - uri = urllib.parse.urljoin(self.get_baseuri(dc), self.path) - uri = urllib.parse.unquote(uri).encode() + uri = urllib.parse.unquote(urllib.parse.urljoin(self.get_baseuri(dc), self.path)) try: pf = PROPFIND(uri, dc, self.headers.get('Depth', 'infinity'), body) @@ -341,7 +340,7 @@ def do_PROPFIND(self): return self.send_status(400) try: - DATA = b'%s\n' % pf.createResponse() + DATA = pf.createResponse() except DAV_Error as error: (ec, dd) = error.args return self.send_status(ec) @@ -376,8 +375,7 @@ def do_REPORT(self): l = self.headers['Content-Length'] body = self.rfile.read(int(l)) - uri = urllib.parse.urljoin(self.get_baseuri(dc), self.path) - uri = urllib.parse.unquote(uri).encode() + uri = urllib.parse.unquote(urllib.parse.urljoin(self.get_baseuri(dc), self.path)) rp = REPORT(uri, dc, self.headers.get('Depth', '0'), body) @@ -403,8 +401,7 @@ def do_MKCOL(self): return self.send_status(415) dc = self.IFACE_CLASS - uri = urllib.parse.urljoin(self.get_baseuri(dc), self.path) - uri = urllib.parse.unquote(uri).encode() + uri = urllib.parse.unquote(urllib.parse.urljoin(self.get_baseuri(dc), self.path)) try: dc.mkcol(uri) @@ -419,11 +416,10 @@ def do_DELETE(self): """ delete an resource """ dc = self.IFACE_CLASS - uri = urllib.parse.urljoin(self.get_baseuri(dc), self.path) - uri = urllib.parse.unquote(uri).encode() + uri = urllib.parse.unquote(urllib.parse.urljoin(self.get_baseuri(dc), self.path)) # hastags not allowed - if uri.find(b'#') >= 0: + if uri.find('#') >= 0: return self.send_status(404) # locked resources are not allowed to delete @@ -490,8 +486,7 @@ def do_DELETE(self): def do_PUT(self): dc = self.IFACE_CLASS - uri = urllib.parse.urljoin(self.get_baseuri(dc), self.path) - uri = urllib.parse.unquote(uri).encode() + uri = urllib.parse.unquote(urllib.parse.urljoin(self.get_baseuri(dc), self.path)) log.debug("do_PUT: uri = %s" % uri) log.debug('do_PUT: headers = %s' % self.headers) @@ -677,12 +672,11 @@ def copymove(self, CLASS): dc = self.IFACE_CLASS # get the source URI - source_uri = urllib.parse.urljoin(self.get_baseuri(dc), self.path) - source_uri = urllib.parse.unquote(source_uri).encode() + source_uri = urllib.parse.unquote(urllib.parse.urljoin(self.get_baseuri(dc), self.path)) # get the destination URI dest_uri = self.headers['Destination'] - dest_uri = urllib.parse.unquote(dest_uri).encode() + dest_uri = urllib.parse.unquote(dest_uri) # check locks on source and dest if self._l_isLocked(source_uri) or self._l_isLocked(dest_uri): diff --git a/pywebdav/lib/davcmd.py b/pywebdav/lib/davcmd.py index 094ad61..1387de0 100644 --- a/pywebdav/lib/davcmd.py +++ b/pywebdav/lib/davcmd.py @@ -14,6 +14,7 @@ from .utils import create_treelist, is_prefix from .errors import * from six.moves import range +import os def deltree(dc,uri,exclude={}): """ delete a tree of resources @@ -128,7 +129,7 @@ def copytree(dc,src,dst,overwrite=None): dc -- dataclass to use src -- src uri from where to copy dst -- dst uri - overwrite -- if 1 then delete dst uri before + overwrite -- if True then delete dst uri before returns dict of uri:error_code tuples from which another method can create a multistatus xml element. @@ -149,8 +150,14 @@ def copytree(dc,src,dst,overwrite=None): tlist = create_treelist(dc,src) result = {} - # prepare destination URIs (get the prefix) - dpath = urllib.parse.urlparse(dst)[2] + # Extract the path out of the source URI. + src_path = urllib.parse.urlparse(src).path + + # Parse the destination URI. + # We'll be using it to construct destination URIs, + # so we don't just retain the path, like we did with + # the source. + dst_parsed = urllib.parse.urlparse(dst) for element in tlist: problem_uris = list(result.keys()) @@ -160,24 +167,34 @@ def copytree(dc,src,dst,overwrite=None): # able to copy in problem_uris which is the prefix # of the actual element. If it is, then we cannot # copy this as well but do not generate another error. - ok=1 + ok=True for p in problem_uris: if is_prefix(p,element): - ok=None + ok=False break - if not ok: continue + if not ok: + continue + + # Find the element's path relative to the source. + element_path = urllib.parse.urlparse(element).path + element_path_rel = os.path.relpath(element_path, start=src_path) + # Append this relative path to the destination. + if element_path_rel == '.': + # os.path.relpath("/somedir", start="/somedir") returns + # a result of ".", which we don't want to append to the + # destination path. + dst_path = dst_parsed.path + else: + dst_path = os.path.join(dst_parsed.path, element_path_rel) + + # Generate destination URI using our derived destination path. + dst_uri = urllib.parse.urlunparse(dst_parsed._replace(path=os.path.join(dst_parsed.path, element_path_rel))) - # now create the destination URI which corresponds to - # the actual source URI. -> actual_dst - # ("subtract" the base src from the URI and prepend the - # dst prefix to it.) - esrc=element.replace(src,b"") - actual_dst=dpath+esrc # now copy stuff try: - copy(dc,element,actual_dst) + copy(dc,element,dst_uri) except DAV_Error as error: (ec,dd) = error.args result[element]=ec @@ -216,6 +233,11 @@ def movetree(dc,src,dst,overwrite=None): # first copy it res = copytree(dc,src,dst,overwrite) + # TODO: shouldn't we check res for errors and bail out before + # the delete if we find any? + # TODO: or, better yet, is there anything preventing us from + # reimplementing this using `shutil.move()`? + # then delete it res = deltree(dc,src,exclude=res) diff --git a/pywebdav/lib/davcopy.py b/pywebdav/lib/davcopy.py index 3ee6c6a..ceda983 100644 --- a/pywebdav/lib/davcopy.py +++ b/pywebdav/lib/davcopy.py @@ -99,4 +99,4 @@ def tree_action(self): re.appendChild(st) ms.appendChild(re) - return doc.toxml(encoding="utf-8") + return doc.toxml(encoding="utf-8") + b"\n" diff --git a/pywebdav/lib/davmove.py b/pywebdav/lib/davmove.py index 155208e..fd5a80a 100644 --- a/pywebdav/lib/davmove.py +++ b/pywebdav/lib/davmove.py @@ -5,7 +5,7 @@ from .constants import COLLECTION, OBJECT, DAV_PROPS from .constants import RT_ALLPROP, RT_PROPNAME, RT_PROP from .errors import * -from .utils import create_treelist, quote_uri, gen_estring, make_xmlresponse +from .utils import create_treelist, quote_uri, gen_estring, make_xmlresponse, is_prefix from .davcmd import moveone, movetree class MOVE: @@ -65,7 +65,8 @@ def tree_action(self): # (we assume that both uris are on the same server!) ps=urllib.parse.urlparse(self.__src)[2] pd=urllib.parse.urlparse(self.__dst)[2] - if ps==pd: raise DAV_Error(403) + if is_prefix(ps, pd): + raise DAV_Error(403) result=dc.movetree(self.__src,self.__dst,self.__overwrite) if not result: return None diff --git a/pywebdav/lib/locks.py b/pywebdav/lib/locks.py index 7924b48..5988c97 100644 --- a/pywebdav/lib/locks.py +++ b/pywebdav/lib/locks.py @@ -1,7 +1,7 @@ from __future__ import absolute_import import time from six.moves import urllib -import random +import uuid import logging @@ -95,7 +95,7 @@ def do_UNLOCK(self): if self._l_isLocked(uri): self._l_delLock(token) - self.send_body(None, 204, 'Ok', 'Ok') + self.send_body(None, 204, 'OK', 'OK') def do_LOCK(self): """ Locking is implemented via in-memory caches. No data is written to disk. """ @@ -194,9 +194,7 @@ def isValid(self): return (modified + timeout) > now def generateToken(self): - _randGen = random.Random(time.time()) - return '%s-%s-00105A989226:%.03f' % \ - (_randGen.random(),_randGen.random(),time.time()) + return str(uuid.uuid4()) def getTimeoutString(self): t = str(self.timeout) @@ -211,7 +209,7 @@ def asXML(self, namespace='d', discover=False): owner_str = '' if isinstance(self.owner, str): owner_str = self.owner - elif isinstance(self.owner, xml.dom.minicompat.NodeList): + elif isinstance(self.owner, xml.dom.minicompat.NodeList) and len(self.owner) > 0: owner_str = "".join([node.toxml() for node in self.owner[0].childNodes]) token = self.token diff --git a/pywebdav/lib/propfind.py b/pywebdav/lib/propfind.py index 5596af0..2cd08d5 100644 --- a/pywebdav/lib/propfind.py +++ b/pywebdav/lib/propfind.py @@ -34,7 +34,7 @@ def __init__(self, uri, dataclass, depth, body): self.default_ns = None self._dataclass = dataclass self._depth = str(depth) - self._uri = uri.rstrip(b'/') + self._uri = uri.rstrip('/') self._has_body = None # did we parse a body? if dataclass.verbose: @@ -116,7 +116,7 @@ def create_propname(self): if uri_childs: uri_list.extend(uri_childs) - return doc.toxml(encoding="utf-8") + return doc.toxml(encoding="utf-8") + b"\n" def create_allprop(self): """ return a list of all properties """ @@ -180,7 +180,7 @@ def create_prop(self): if uri_childs: uri_list.extend(uri_childs) - return doc.toxml(encoding="utf-8") + return doc.toxml(encoding="utf-8") + b"\n" def mk_propname_response(self, uri, propnames, doc): """ make a new result element for a PROPNAME request @@ -245,7 +245,7 @@ def mk_prop_response(self, uri, good_props, bad_props, doc): uri = self._dataclass.baseurl + '/' + '/'.join(uri.split('/')[3:]) # write href information - uparts = urllib.parse.urlparse(uri.decode()) + uparts = urllib.parse.urlparse(uri) fileloc = uparts[2] href = doc.createElement("D:href") diff --git a/pywebdav/lib/report.py b/pywebdav/lib/report.py index db6f398..7d589d4 100644 --- a/pywebdav/lib/report.py +++ b/pywebdav/lib/report.py @@ -55,7 +55,7 @@ def create_propname(self): if uri_childs: uri_list.extend(uri_childs) - return doc.toxml(encoding="utf-8") + return doc.toxml(encoding="utf-8") + b"\n" def create_prop(self): """ handle a request @@ -117,5 +117,5 @@ def create_prop(self): if uri_childs: uri_list.extend(uri_childs) - return doc.toxml(encoding="utf-8") + return doc.toxml(encoding="utf-8") + b"\n" diff --git a/pywebdav/lib/utils.py b/pywebdav/lib/utils.py index bbc6098..7698a9e 100755 --- a/pywebdav/lib/utils.py +++ b/pywebdav/lib/utils.py @@ -1,6 +1,7 @@ from __future__ import absolute_import import time import re +import os from xml.dom import minidom from six.moves import urllib @@ -74,11 +75,10 @@ def create_treelist(dataclass,uri): return list def is_prefix(uri1,uri2): - """ returns 1 of uri1 is a prefix of uri2 """ - if uri2[:len(uri1)]==uri1: - return 1 - else: - return None + """ returns 1 if uri1 is a prefix of uri2 """ + path1 = urllib.parse.urlparse(uri1).path + path2 = urllib.parse.urlparse(uri2).path + return os.path.commonpath([path1, path2]) == path1 def quote_uri(uri): """ quote an URL but not the protocol part """ @@ -122,7 +122,7 @@ def make_xmlresponse(result): re.appendChild(st) doc.documentElement.appendChild(re) - return doc.toxml(encoding="utf-8") + return doc.toxml(encoding="utf-8") + b"\n" # taken from App.Common diff --git a/pywebdav/server/config.ini b/pywebdav/server/config.ini index c46187f..fa57207 100644 --- a/pywebdav/server/config.ini +++ b/pywebdav/server/config.ini @@ -59,6 +59,9 @@ mimecheck = 1 # webdav level (1 = webdav level 2) lockemulation = 1 +# dav server base url +baseurl = + # internal features #chunked_http_response = 1 #http_request_use_iterator = 0 diff --git a/pywebdav/server/fshandler.py b/pywebdav/server/fshandler.py index 7b9b0a6..4b059dd 100644 --- a/pywebdav/server/fshandler.py +++ b/pywebdav/server/fshandler.py @@ -92,7 +92,7 @@ def setBaseURI(self, uri): def uri2local(self,uri): """ map uri in baseuri and local part """ - uparts=urllib.parse.urlparse(uri.decode()) + uparts=urllib.parse.urlparse(uri) fileloc=uparts[2][1:] filename=os.path.join(self.directory, fileloc) filename=os.path.normpath(filename) @@ -105,7 +105,7 @@ def local2uri(self,filename): parts=filename.replace("\\","/").split("/")[pnum:] sparts="/"+"/".join(parts) uri=urllib.parse.urljoin(self.baseuri,sparts) - return uri.encode() if isinstance(uri, six.text_type) else uri + return uri def get_childs(self, uri, filter=None):