# -*- encoding: utf-8 -*- ############################################################################9 # # Copyright P. Christeas 2008-2010 # Copyright OpenERP SA, 2010 (http://www.openerp.com ) # # Disclaimer: Many of the functions below borrow code from the # python-webdav library (http://code.google.com/p/pywebdav/ ), # which they import and override to suit OpenERP functionality. # python-webdav was written by: Simon Pamies # Christian Scholz # Vince Spicer # # WARNING: This program as such is intended to be used by professional # programmers who take the whole responsability of assessing all potential # consequences resulting from its eventual inadequacies and bugs # End users who are looking for a ready-to-use solution with commercial # garantees and support are strongly adviced to contract a Free Software # Service Company # # This program is Free Software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 3 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. ############################################################################### import logging import netsvc from dav_fs import openerp_dav_handler from tools.config import config from DAV.WebDAVServer import DAVRequestHandler from service import http_server from service.websrv_lib import FixSendError, HttpOptions from BaseHTTPServer import BaseHTTPRequestHandler import urlparse import urllib import re import time from string import atoi import addons from DAV.utils import IfParser, TagList from DAV.errors import DAV_Error, DAV_Forbidden, DAV_NotFound from DAV.propfind import PROPFIND # from DAV.constants import DAV_VERSION_1, DAV_VERSION_2 from xml.dom import minidom from redirect import RedirectHTTPHandler _logger = logging.getLogger(__name__) khtml_re = re.compile(r' KHTML/([0-9\.]+) ') def OpenDAVConfig(**kw): class OpenDAV: def __init__(self, **kw): self.__dict__.update(**kw) def getboolean(self, word): return self.__dict__.get(word, False) class Config: DAV = OpenDAV(**kw) return Config() class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler): verbose = False protocol_version = 'HTTP/1.1' _HTTP_OPTIONS= { 'DAV' : ['1', '2'], 'Allow' : [ 'GET', 'HEAD', 'COPY', 'MOVE', 'POST', 'PUT', 'PROPFIND', 'PROPPATCH', 'OPTIONS', 'MKCOL', 'DELETE', 'TRACE', 'REPORT', ] } def get_userinfo(self,user,pw): return False def _log(self, message): self._logger.debug(message) def handle(self): self._init_buffer() def finish(self): pass def get_db_from_path(self, uri): # interface class will handle all cases. res = self.IFACE_CLASS.get_db(uri, allow_last=True) return res def setup(self): self.davpath = '/'+config.get_misc('webdav','vdir','webdav') addr, port = self.server.server_name, self.server.server_port server_proto = getattr(self.server,'proto', 'http').lower() try: if hasattr(self.request, 'getsockname'): addr, port = self.request.getsockname() except Exception, e: self.log_error("Cannot calculate own address: %s" , e) # Too early here to use self.headers self.baseuri = "%s://%s:%d/"% (server_proto, addr, port) self.IFACE_CLASS = openerp_dav_handler(self, self.verbose) def copymove(self, CLASS): """ Our uri scheme removes the /webdav/ component from there, so we need to mangle the header, too. """ up = urlparse.urlparse(urllib.unquote(self.headers['Destination'])) if up.path.startswith(self.davpath): self.headers['Destination'] = up.path[len(self.davpath):] else: raise DAV_Forbidden("Not allowed to copy/move outside webdav path.") # TODO: locks DAVRequestHandler.copymove(self, CLASS) def get_davpath(self): return self.davpath def log_message(self, format, *args): _logger.debug(format % args) def log_error(self, format, *args): _logger.warning(format % args) def _prep_OPTIONS(self, opts): ret = opts dc=self.IFACE_CLASS uri=urlparse.urljoin(self.get_baseuri(dc), self.path) uri=urllib.unquote(uri) try: ret = dc.prep_http_options(uri, opts) except DAV_Error, (ec,dd): pass except Exception,e: self.log_error("Error at options: %s", str(e)) raise return ret def send_response(self, code, message=None): # the BufferingHttpServer will send Connection: close , while # the BaseHTTPRequestHandler will only accept int code. # workaround both of them. if self.command == 'PROPFIND' and int(code) == 404: kh = khtml_re.search(self.headers.get('User-Agent','')) if kh and (kh.group(1) < '4.5'): # There is an ugly bug in all khtml < 4.5.x, where the 404 # response is treated as an immediate error, which would even # break the flow of a subsequent PUT request. At the same time, # the 200 response (rather than 207 with content) is treated # as "path not exist", so we send this instead # https://bugs.kde.org/show_bug.cgi?id=166081 code = 200 BaseHTTPRequestHandler.send_response(self, int(code), message) def send_header(self, key, value): if key == 'Connection' and value == 'close': self.close_connection = 1 DAVRequestHandler.send_header(self, key, value) def send_body(self, DATA, code = None, msg = None, desc = None, ctype='application/octet-stream', headers=None): if headers and 'Connection' in headers: pass elif self.request_version in ('HTTP/1.0', 'HTTP/0.9'): pass elif self.close_connection == 1: # close header already sent pass elif headers and self.headers.get('Connection',False) == 'Keep-Alive': headers['Connection'] = 'keep-alive' if headers is None: headers = {} DAVRequestHandler.send_body(self, DATA, code=code, msg=msg, desc=desc, ctype=ctype, headers=headers) def do_PUT(self): dc=self.IFACE_CLASS uri=urlparse.urljoin(self.get_baseuri(dc), self.path) uri=urllib.unquote(uri) # Handle If-Match if self.headers.has_key('If-Match'): test = False etag = None for match in self.headers['If-Match'].split(','): if match == '*': if dc.exists(uri): test = True break else: if dc.match_prop(uri, match, "DAV:", "getetag"): test = True break if not test: self._get_body() self.send_status(412) return # Handle If-None-Match if self.headers.has_key('If-None-Match'): test = True etag = None for match in self.headers['If-None-Match'].split(','): if match == '*': if dc.exists(uri): test = False break else: if dc.match_prop(uri, match, "DAV:", "getetag"): test = False break if not test: self._get_body() self.send_status(412) return # Handle expect expect = self.headers.get('Expect', '') if (expect.lower() == '100-continue' and self.protocol_version >= 'HTTP/1.1' and self.request_version >= 'HTTP/1.1'): self.send_status(100) self._flush() # read the body body=self._get_body() # locked resources are not allowed to be overwritten if self._l_isLocked(uri): return self.send_body(None, '423', 'Locked', 'Locked') ct=None if self.headers.has_key("Content-Type"): ct=self.headers['Content-Type'] try: location = dc.put(uri, body, ct) except DAV_Error, (ec,dd): self.log_error("Cannot PUT to %s: %s", uri, dd) return self.send_status(ec) headers = {} etag = None if location and isinstance(location, tuple): etag = location[1] location = location[0] # note that we have allowed for > 2 elems if location: headers['Location'] = location else: try: if not etag: etag = dc.get_prop(location or uri, "DAV:", "getetag") if etag: headers['ETag'] = str(etag) except Exception: pass self.send_body(None, '201', 'Created', '', headers=headers) def _get_body(self): body = None if self.headers.has_key("Content-Length"): l=self.headers['Content-Length'] body=self.rfile.read(atoi(l)) return body def do_DELETE(self): try: DAVRequestHandler.do_DELETE(self) except DAV_Error, (ec, dd): return self.send_status(ec) def do_UNLOCK(self): """ Unlocks given resource """ dc = self.IFACE_CLASS self.log_message('UNLOCKing resource %s' % self.headers) uri = urlparse.urljoin(self.get_baseuri(dc), self.path) uri = urllib.unquote(uri) token = self.headers.get('Lock-Token', False) if token: token = token.strip() if token[0] == '<' and token[-1] == '>': token = token[1:-1] else: token = False if not token: return self.send_status(400, 'Bad lock token') try: res = dc.unlock(uri, token) except DAV_Error, (ec, dd): return self.send_status(ec, dd) if res == True: self.send_body(None, '204', 'OK', 'Resource unlocked.') else: # We just differentiate the description, for debugging purposes self.send_body(None, '204', 'OK', 'Resource not locked.') def do_LOCK(self): """ Attempt to place a lock on the given resource. """ dc = self.IFACE_CLASS lock_data = {} self.log_message('LOCKing resource %s' % self.headers) body = None if self.headers.has_key('Content-Length'): l = self.headers['Content-Length'] body = self.rfile.read(atoi(l)) depth = self.headers.get('Depth', 'infinity') uri = urlparse.urljoin(self.get_baseuri(dc), self.path) uri = urllib.unquote(uri) self.log_message('do_LOCK: uri = %s' % uri) ifheader = self.headers.get('If') if ifheader: ldif = IfParser(ifheader) if isinstance(ldif, list): if len(ldif) !=1 or (not isinstance(ldif[0], TagList)) \ or len(ldif[0].list) != 1: raise DAV_Error(400, "Cannot accept multiple tokens.") ldif = ldif[0].list[0] if ldif[0] == '<' and ldif[-1] == '>': ldif = ldif[1:-1] lock_data['token'] = ldif if not body: lock_data['refresh'] = True else: lock_data['refresh'] = False lock_data.update(self._lock_unlock_parse(body)) if lock_data['refresh'] and not lock_data.get('token', False): raise DAV_Error(400, 'Lock refresh must specify token.') lock_data['depth'] = depth try: created, data, lock_token = dc.lock(uri, lock_data) except DAV_Error, (ec, dd): return self.send_status(ec, dd) headers = {} if not lock_data['refresh']: headers['Lock-Token'] = '<%s>' % lock_token if created: self.send_body(data, '201', 'Created', ctype='text/xml', headers=headers) else: self.send_body(data, '200', 'OK', ctype='text/xml', headers=headers) def _lock_unlock_parse(self, body): # Override the python-webdav function, with some improvements # Unlike the py-webdav one, we also parse the owner minidom elements into # pure pythonic struct. doc = minidom.parseString(body) data = {} owners = [] for info in doc.getElementsByTagNameNS('DAV:', 'lockinfo'): for scope in info.getElementsByTagNameNS('DAV:', 'lockscope'): for scc in scope.childNodes: if scc.nodeType == info.ELEMENT_NODE \ and scc.namespaceURI == 'DAV:': data['lockscope'] = scc.localName break for ltype in info.getElementsByTagNameNS('DAV:', 'locktype'): for ltc in ltype.childNodes: if ltc.nodeType == info.ELEMENT_NODE \ and ltc.namespaceURI == 'DAV:': data['locktype'] = ltc.localName break for own in info.getElementsByTagNameNS('DAV:', 'owner'): for ono in own.childNodes: if ono.nodeType == info.TEXT_NODE: if ono.data: owners.append(ono.data) elif ono.nodeType == info.ELEMENT_NODE \ and ono.namespaceURI == 'DAV:' \ and ono.localName == 'href': href = '' for hno in ono.childNodes: if hno.nodeType == info.TEXT_NODE: href += hno.data owners.append(('href','DAV:', href)) if len(owners) == 1: data['lockowner'] = owners[0] elif not owners: pass else: data['lockowner'] = owners return data from service.http_server import reg_http_service,OpenERPAuthProvider class DAVAuthProvider(OpenERPAuthProvider): def authenticate(self, db, user, passwd, client_address): """ authenticate, but also allow the False db, meaning to skip authentication when no db is specified. """ if db is False: return True return OpenERPAuthProvider.authenticate(self, db, user, passwd, client_address) class dummy_dav_interface(object): """ Dummy dav interface """ verbose = True PROPS={"DAV:" : ('creationdate', 'displayname', 'getlastmodified', 'resourcetype', ), } M_NS={"DAV:" : "_get_dav", } def __init__(self, parent): self.parent = parent def get_propnames(self,uri): return self.PROPS def get_prop(self,uri,ns,propname): if self.M_NS.has_key(ns): prefix=self.M_NS[ns] else: raise DAV_NotFound mname=prefix+"_"+propname.replace('-', '_') try: m=getattr(self,mname) r=m(uri) return r except AttributeError: raise DAV_NotFound def get_data(self, uri, range=None): raise DAV_NotFound def _get_dav_creationdate(self,uri): return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) def _get_dav_getlastmodified(self,uri): return time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) def _get_dav_displayname(self, uri): return uri def _get_dav_resourcetype(self, uri): return ('collection', 'DAV:') def exists(self, uri): """ return 1 or None depending on if a resource exists """ uri2 = uri.split('/') if len(uri2) < 3: return True _logger.debug("Requested uri: %s", uri) return None # no def is_collection(self, uri): """ return 1 or None depending on if a resource is a collection """ return None # no class DAVStaticHandler(http_server.StaticHTTPHandler): """ A variant of the Static handler, which will serve dummy DAV requests """ verbose = False protocol_version = 'HTTP/1.1' _HTTP_OPTIONS= { 'DAV' : ['1', '2'], 'Allow' : [ 'GET', 'HEAD', 'PROPFIND', 'OPTIONS', 'REPORT', ] } def send_body(self, content, code, message='OK', content_type='text/xml'): self.send_response(int(code), message) self.send_header("Content-Type", content_type) # self.send_header('Connection', 'close') self.send_header('Content-Length', len(content) or 0) self.end_headers() if hasattr(self, '_flush'): self._flush() if self.command != 'HEAD': self.wfile.write(content) def do_PROPFIND(self): """Answer to PROPFIND with generic data. A rough copy of python-webdav's do_PROPFIND, but hacked to work statically. """ dc = dummy_dav_interface(self) # read the body containing the xml request # iff there is no body then this is an ALLPROP request body = None if self.headers.has_key('Content-Length'): l = self.headers['Content-Length'] body = self.rfile.read(atoi(l)) path = self.path.rstrip('/') uri = urllib.unquote(path) pf = PROPFIND(uri, dc, self.headers.get('Depth', 'infinity'), body) try: DATA = '%s\n' % pf.createResponse() except DAV_Error, (ec,dd): return self.send_error(ec,dd) except Exception: self.log_exception("Cannot PROPFIND") raise # work around MSIE DAV bug for creation and modified date # taken from Resource.py @ Zope webdav if (self.headers.get('User-Agent') == 'Microsoft Data Access Internet Publishing Provider DAV 1.1'): DATA = DATA.replace('', '') DATA = DATA.replace('', '') self.send_body(DATA, '207','Multi-Status','Multiple responses') def not_get_baseuri(self): baseuri = '/' if self.headers.has_key('Host'): uparts = list(urlparse.urlparse('/')) uparts[1] = self.headers['Host'] baseuri = urlparse.urlunparse(uparts) return baseuri def get_davpath(self): return '' try: if (config.get_misc('webdav','enable',True)): directory = '/'+config.get_misc('webdav','vdir','webdav') handler = DAVHandler verbose = config.get_misc('webdav','verbose',True) handler.debug = config.get_misc('webdav','debug',True) _dc = { 'verbose' : verbose, 'directory' : directory, 'lockemulation' : True, } conf = OpenDAVConfig(**_dc) handler._config = conf reg_http_service(directory, DAVHandler, DAVAuthProvider) _logger.info("WebDAV service registered at path: %s/ "% directory) if not (config.get_misc('webdav', 'no_root_hack', False)): # Now, replace the static http handler with the dav-enabled one. # If a static-http service has been specified for our server, then # read its configuration and use that dir_path. # NOTE: this will _break_ any other service that would be registered # at the root path in future. base_path = False if config.get_misc('static-http','enable', False): base_path = config.get_misc('static-http', 'base_path', '/') if base_path and base_path == '/': dir_path = config.get_misc('static-http', 'dir_path', False) else: dir_path = addons.get_module_resource('document_webdav','public_html') # an _ugly_ hack: we put that dir back in tools.config.misc, so that # the StaticHttpHandler can find its dir_path. config.misc.setdefault('static-http',{})['dir_path'] = dir_path reg_http_service('/', DAVStaticHandler) except Exception, e: _logger.error('Cannot launch webdav: %s' % e) def init_well_known(): reps = RedirectHTTPHandler.redirect_paths num_svcs = config.get_misc('http-well-known', 'num_services', '0') for nsv in range(1, int(num_svcs)+1): uri = config.get_misc('http-well-known', 'service_%d' % nsv, False) path = config.get_misc('http-well-known', 'path_%d' % nsv, False) if not (uri and path): continue reps['/'+uri] = path if int(num_svcs): reg_http_service('/.well-known', RedirectHTTPHandler) init_well_known() class PrincipalsRedirect(RedirectHTTPHandler): redirect_paths = {} def _find_redirect(self): for b, r in self.redirect_paths.items(): if self.path.startswith(b): return r + self.path[len(b):] return False def init_principals_redirect(): """ Some devices like the iPhone will look under /principals/users/xxx for the user's properties. In OpenERP we _cannot_ have a stray /principals/... working path, since we have a database path and the /webdav/ component. So, the best solution is to redirect the url with 301. Luckily, it does work in the device. The trick is that we need to hard-code the database to use, either the one centrally defined in the config, or a "forced" one in the webdav section. """ dbname = config.get_misc('webdav', 'principal_dbname', False) if (not dbname) and not config.get_misc('webdav', 'no_principals_redirect', False): dbname = config.get('db_name', False) if dbname: PrincipalsRedirect.redirect_paths[''] = '/webdav/%s/principals' % dbname reg_http_service('/principals', PrincipalsRedirect) _logger.info( "Registered HTTP redirect handler for /principals to the %s db.", dbname) init_principals_redirect() #eof # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: