diff options
Diffstat (limited to 'src/webdav/Connection.py')
-rw-r--r-- | src/webdav/Connection.py | 321 |
1 files changed, 321 insertions, 0 deletions
diff --git a/src/webdav/Connection.py b/src/webdav/Connection.py new file mode 100644 index 0000000..66f7833 --- /dev/null +++ b/src/webdav/Connection.py @@ -0,0 +1,321 @@ +# pylint: disable-msg=W0142,W0102,R0901,R0904,E0203,E1101,C0103 +# +# Copyright 2008 German Aerospace Center (DLR) +# +# Licensed 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. + + +""" +The contained class extends the HTTPConnection class for WebDAV support. +""" + + +from httplib import HTTPConnection, CannotSendRequest, BadStatusLine, ResponseNotReady +from copy import copy +import base64 # for basic authentication +try: + import hashlib +except ImportError: # for Python 2.4 compatibility + import md5 + hashlib = md5 +import mimetypes +import os # file handling +import urllib +import types +import socket # to "catch" socket.error +from threading import RLock +try: + from uuid import uuid4 +except ImportError: # for Python 2.4 compatibility + from uuid_ import uuid4 +from xml.parsers.expat import ExpatError + +from davlib import DAV +from qp_xml import Parser + +from webdav.WebdavResponse import MultiStatusResponse, ResponseFormatError +from webdav import Constants +from webdav.logger import getDefaultLogger + + +__version__ = "$LastChangedRevision$" + + +class Connection(DAV): + """ + This class handles a connection to a WebDAV server. + This class is used internally. Client code should prefer classes + L{WebdavClient.ResourceStorer} and L{WebdavClient.CollectionStorer}. + + @author: Roland Betz + """ + + # Constants + # The following switch activates a workaround for the Tamino webdav server: + # Tamino expects URLs which are passed in a HTTP header to be Latin-1 encoded + # instead of Utf-8 encoded. + # Set this switch to zero in order to communicate with conformant servers. + blockSize = 30000 + MaxRetries = 10 + + def __init__(self, *args, **kwArgs): + DAV.__init__(self, *args, **kwArgs) + self.__authorizationInfo = None + self.logger = getDefaultLogger() + self.isConnectedToCatacomb = True + self.serverTypeChecked = False + self._lock = RLock() + + def _request(self, method, url, body=None, extra_hdrs={}): + + self._lock.acquire() + try: + # add the authorization header + extraHeaders = copy(extra_hdrs) + if self.__authorizationInfo: + + # update (digest) authorization data + if hasattr(self.__authorizationInfo, "update"): + self.__authorizationInfo.update(method=method, uri=url) + + extraHeaders["AUTHORIZATION"] = self.__authorizationInfo.authorization + + # encode message parts + body = _toUtf8(body) + url = _urlEncode(url) + for key, value in extraHeaders.items(): + extraHeaders[key] = _toUtf8(value) + if key == "Destination": # copy/move header + if self.isConnectedToCatacomb: + extraHeaders[key] = _toUtf8(value.replace(Constants.SHARP, Constants.QUOTED_SHARP)) + + else: # in case of TAMINO 4.4 + extraHeaders[key] = _urlEncode(value) + # pass message to httplib class + for retry in range(0, Connection.MaxRetries): # retry loop + try: + self.logger.debug("REQUEST Send %s for %s" % (method, url)) + self.logger.debug("REQUEST Body: " + repr(body)) + for hdr in extraHeaders.items(): + self.logger.debug("REQUEST Header: " + repr(hdr)) + self.request(method, url, body, extraHeaders) + response = self.getresponse() + break # no retry needed + except (CannotSendRequest, socket.error, BadStatusLine, ResponseNotReady), exc: + # Workaround, start: reconnect and retry... + self.logger.debug("Exception: " + str(exc) + " Retry ... ") + self.close() + try: + self.connect() + except (CannotSendRequest, socket.error, BadStatusLine, ResponseNotReady), exc: + raise WebdavError("Cannot perform request. Connection failed.") + if retry == Connection.MaxRetries - 1: + raise WebdavError("Cannot perform request.") + return self.__evaluateResponse(method, response) + finally: + self._lock.release() + + def __evaluateResponse(self, method, response): + """ Evaluates the response of the WebDAV server. """ + + status, reason = response.status, response.reason + self.logger.debug("Method: " + method + " Status %d: " % status + reason) + + if status >= Constants.CODE_LOWEST_ERROR: # error has occured ? + self.logger.debug("ERROR Response: " + response.read()) + + # identify authentication CODE_UNAUTHORIZED, throw appropriate exception + if status == Constants.CODE_UNAUTHORIZED: + raise AuthorizationError(reason, status, response.msg["www-authenticate"]) + + response.close() + raise WebdavError(reason, status) + + if status == Constants.CODE_MULTISTATUS: + content = response.read() + ## check for UTF-8 encoding + try: + response.root = Parser().parse(content) + except ExpatError, error: + errorMessage = "Invalid XML document has been returned.\nReason: '%s'" % str(error.args) + raise WebdavError(errorMessage) + try: + response.msr = MultiStatusResponse(response.root) + except ResponseFormatError: + raise WebdavError("Invalid WebDAV response.") + response.close() + self.logger.debug("RESPONSE (Multi-Status): " + unicode(response.msr)) + elif method == 'LOCK' and status == Constants.CODE_SUCCEEDED: + response.parse_lock_response() + response.close() + elif method != 'GET' and method != 'PUT': + self.logger.debug("RESPONSE Body: " + response.read()) + response.close() + return response + + def addBasicAuthorization(self, user, password, realm=None): + if user and len(user) > 0: + self.__authorizationInfo = _BasicAuthenticationInfo(realm=realm, user=user, password=password) + + def addDigestAuthorization(self, user, password, realm, qop, nonce, uri = None, method = None): + if user and len(user) > 0: + # username, realm, password, uri, method, qop are required + self.__authorizationInfo = _DigestAuthenticationInfo(realm=realm, user=user, password=password, uri=uri, method=method, qop=qop, nonce=nonce) + + def putFile(self, path, srcfile, header={}): + self._lock.acquire() + try: + # Assemble header + try: + size = os.path.getsize(srcfile.name) + except os.error, error: + raise WebdavError("Cannot determine file size.\nReason: ''" % str(error.args)) + header["Content-length"] = str(size) + + contentType, contentEnc = mimetypes.guess_type(path) + if contentType: + header['Content-Type'] = contentType + if contentEnc: + header['Content-Encoding'] = contentEnc + if self.__authorizationInfo: + # update (digest) authorization data + if hasattr(self.__authorizationInfo, "update"): + self.__authorizationInfo.update(method="PUT", uri=path) + header["AUTHORIZATION"] = self.__authorizationInfo.authorization + + # send first request + path = _urlEncode(path) + try: + HTTPConnection.request(self, 'PUT', path, "", header) + self._blockCopySocket(srcfile, self, Connection.blockSize) + srcfile.close() + response = self.getresponse() + except (CannotSendRequest, socket.error, BadStatusLine, ResponseNotReady), exc: + self.logger.debug("Exception: " + str(exc) + " Retry ... ") + raise WebdavError("Cannot perform request.") + status, reason = (response.status, response.reason) + self.logger.debug("Status %d: %s" % (status, reason)) + try: + if status >= Constants.CODE_LOWEST_ERROR: # error has occured ? + raise WebdavError(reason, status) + finally: + self.logger.debug("RESPONSE Body: " + response.read()) + response.close() + return response + finally: + self._lock.release() + + def _blockCopySocket(self, source, toSocket, blockSize): + transferedBytes = 0 + block = source.read(blockSize) + #while source.readinto(block, blockSize): + while len(block): + toSocket.send(block) + self.logger.debug("Wrote %d bytes." % len(block)) + transferedBytes += len(block) + block = source.read(blockSize) + self.logger.info("Transfered %d bytes." % transferedBytes) + + def __str__(self): + return self.protocol + "://" + self.host + ':' + str(self.port) + + +class _BasicAuthenticationInfo(object): + def __init__(self, **kwArgs): + self.__dict__.update(kwArgs) + self.cookie = base64.encodestring("%s:%s" % (self.user, self.password) ).strip() + self.authorization = "Basic " + self.cookie + self.password = None # protect password security + +class _DigestAuthenticationInfo(object): + + __nc = "0000000" # in hexadecimal without leading 0x + + def __init__(self, **kwArgs): + + self.__dict__.update(kwArgs) + + if self.qop is None: + raise WebdavError("Digest without qop is not implemented.") + if self.qop == "auth-int": + raise WebdavError("Digest with qop-int is not implemented.") + + def update(self, **kwArgs): + """ Update input data between requests""" + + self.__dict__.update(kwArgs) + + def _makeDigest(self): + """ Creates the digest information. """ + + # increment nonce count + self._incrementNc() + + # username, realm, password, uri, method, qop are required + + a1 = "%s:%s:%s" % (self.user, self.realm, self.password) + ha1 = hashlib.md5(a1).hexdigest() + + #qop == auth + a2 = "%s:%s" % (self.method, self.uri) + ha2 = hashlib.md5(a2).hexdigest() + + cnonce = str(uuid4()) + + responseData = "%s:%s:%s:%s:%s:%s" % (ha1, self.nonce, _DigestAuthenticationInfo.__nc, cnonce, self.qop, ha2) + digestResponse = hashlib.md5(responseData).hexdigest() + + authorization = "Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", algorithm=MD5, response=\"%s\", qop=auth, nc=%s, cnonce=\"%s\"" \ + % (self.user, self.realm, self.nonce, self.uri, digestResponse, _DigestAuthenticationInfo.__nc, cnonce) + return authorization + + authorization = property(_makeDigest) + + def _incrementNc(self): + _DigestAuthenticationInfo.__nc = self._dec2nc(self._nc2dec() + 1) + + def _nc2dec(self): + return int(_DigestAuthenticationInfo.__nc, 16) + + def _dec2nc(self, decimal): + return hex(decimal)[2:].zfill(8) + + +class WebdavError(IOError): + def __init__(self, reason, code=0): + IOError.__init__(self, code) + self.code = code + self.reason = reason + def __str__(self): + return self.reason + + +class AuthorizationError(WebdavError): + def __init__(self, reason, code, authHeader): + WebdavError.__init__(self, reason, code) + + self.authType = authHeader.split(" ")[0] + self.authInfo = authHeader + + +def _toUtf8(body): + if not body is None: + if type(body) == types.UnicodeType: + body = body.encode('utf-8') + return body + + +def _urlEncode(url): + if type(url) == types.UnicodeType: + url = url.encode('utf-8') + return urllib.quote(url) |