1 # -*- encoding: utf-8 -*-
2 ############################################################################9
4 # Copyright P. Christeas <p_christ@hol.gr> 2008-2010
5 # Copyright OpenERP SA, 2010 (http://www.openerp.com )
7 # Disclaimer: Many of the functions below borrow code from the
8 # python-webdav library (http://code.google.com/p/pywebdav/ ),
9 # which they import and override to suit OpenERP functionality.
10 # python-webdav was written by: Simon Pamies <s.pamies@banality.de>
11 # Christian Scholz <mrtopf@webdav.de>
12 # Vince Spicer <vince@vince.ca>
14 # WARNING: This program as such is intended to be used by professional
15 # programmers who take the whole responsability of assessing all potential
16 # consequences resulting from its eventual inadequacies and bugs
17 # End users who are looking for a ready-to-use solution with commercial
18 # garantees and support are strongly adviced to contract a Free Software
21 # This program is Free Software; you can redistribute it and/or
22 # modify it under the terms of the GNU General Public License
23 # as published by the Free Software Foundation; either version 3
24 # of the License, or (at your option) any later version.
26 # This program is distributed in the hope that it will be useful,
27 # but WITHOUT ANY WARRANTY; without even the implied warranty of
28 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 # GNU General Public License for more details.
31 # You should have received a copy of the GNU General Public License
32 # along with this program; if not, write to the Free Software
33 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
34 ###############################################################################
39 from dav_fs import openerp_dav_handler
40 from openerp.tools.config import config
42 from pywebdav.lib.WebDAVServer import DAVRequestHandler
43 from pywebdav.lib.utils import IfParser, TagList
44 from pywebdav.lib.errors import DAV_Error, DAV_Forbidden, DAV_NotFound
45 from pywebdav.lib.propfind import PROPFIND
47 from DAV.WebDAVServer import DAVRequestHandler
48 from DAV.utils import IfParser, TagList
49 from DAV.errors import DAV_Error, DAV_Forbidden, DAV_NotFound
50 from DAV.propfind import PROPFIND
51 from openerp.service import http_server
52 from openerp.service.websrv_lib import FixSendError, HttpOptions
53 from BaseHTTPServer import BaseHTTPRequestHandler
58 from string import atoi
60 # from DAV.constants import DAV_VERSION_1, DAV_VERSION_2
61 from xml.dom import minidom
62 from redirect import RedirectHTTPHandler
63 _logger = logging.getLogger(__name__)
64 khtml_re = re.compile(r' KHTML/([0-9\.]+) ')
66 def OpenDAVConfig(**kw):
68 def __init__(self, **kw):
69 self.__dict__.update(**kw)
71 def getboolean(self, word):
72 return self.__dict__.get(word, False)
80 class DAVHandler(DAVRequestHandler, HttpOptions, FixSendError):
83 protocol_version = 'HTTP/1.1'
84 _HTTP_OPTIONS= { 'DAV' : ['1', '2'],
85 'Allow' : [ 'GET', 'HEAD', 'COPY', 'MOVE', 'POST', 'PUT',
86 'PROPFIND', 'PROPPATCH', 'OPTIONS', 'MKCOL',
87 'DELETE', 'TRACE', 'REPORT', ]
90 def __init__(self, request, client_address, server):
91 self.request = request
92 self.client_address = client_address
96 def get_userinfo(self, user, pw):
99 def _log(self, message):
100 self._logger.debug(message)
103 """Handle multiple requests if necessary."""
104 self.close_connection = 1
106 self.handle_one_request()
107 while not self.close_connection:
108 self.handle_one_request()
109 except Exception as e:
111 self.log_error("Request timed out: %r \n Trying old version of HTTPServer", e)
113 except Exception as e:
114 #a read or a write timed out. Discard this connection
115 self.log_error("Not working neither, closing connection\n %r", e)
116 self.close_connection = 1
121 def get_db_from_path(self, uri):
122 # interface class will handle all cases.
123 res = self.IFACE_CLASS.get_db(uri, allow_last=True)
127 self.davpath = '/'+config.get_misc('webdav','vdir','webdav')
128 addr, port = self.server.server_name, self.server.server_port
129 server_proto = getattr(self.server,'proto', 'http').lower()
130 # Too early here to use self.headers
131 self.baseuri = "%s://%s:%d/"% (server_proto, addr, port)
132 self.IFACE_CLASS = openerp_dav_handler(self, self.verbose)
134 def copymove(self, CLASS):
135 """ Our uri scheme removes the /webdav/ component from there, so we
136 need to mangle the header, too.
138 up = urlparse.urlparse(urllib.unquote(self.headers['Destination']))
139 if up.path.startswith(self.davpath):
140 self.headers['Destination'] = up.path[len(self.davpath):]
142 raise DAV_Forbidden("Not allowed to copy/move outside webdav path.")
144 DAVRequestHandler.copymove(self, CLASS)
146 def get_davpath(self):
149 def log_message(self, format, *args):
150 _logger.debug(format % args)
152 def log_error(self, format, *args):
153 _logger.warning(format % args)
155 def _prep_OPTIONS(self, opts):
158 uri=urlparse.urljoin(self.get_baseuri(dc), self.path)
159 uri=urllib.unquote(uri)
161 ret = dc.prep_http_options(uri, opts)
162 except DAV_Error, (ec,dd):
165 self.log_error("Error at options: %s", str(e))
169 def send_response(self, code, message=None):
170 # the BufferingHttpServer will send Connection: close , while
171 # the BaseHTTPRequestHandler will only accept int code.
172 # workaround both of them.
173 if self.command == 'PROPFIND' and int(code) == 404:
174 kh = khtml_re.search(self.headers.get('User-Agent',''))
175 if kh and (kh.group(1) < '4.5'):
176 # There is an ugly bug in all khtml < 4.5.x, where the 404
177 # response is treated as an immediate error, which would even
178 # break the flow of a subsequent PUT request. At the same time,
179 # the 200 response (rather than 207 with content) is treated
180 # as "path not exist", so we send this instead
181 # https://bugs.kde.org/show_bug.cgi?id=166081
183 BaseHTTPRequestHandler.send_response(self, int(code), message)
185 def send_header(self, key, value):
186 if key == 'Connection' and value == 'close':
187 self.close_connection = 1
188 DAVRequestHandler.send_header(self, key, value)
190 def send_body(self, DATA, code=None, msg=None, desc=None, ctype='application/octet-stream', headers=None):
191 if headers and 'Connection' in headers:
193 elif self.request_version in ('HTTP/1.0', 'HTTP/0.9'):
195 elif self.close_connection == 1: # close header already sent
197 elif headers and self.headers.get('Connection',False) == 'Keep-Alive':
198 headers['Connection'] = 'keep-alive'
203 DAVRequestHandler.send_body(self, DATA, code=code, msg=msg, desc=desc,
204 ctype=ctype, headers=headers)
208 uri=urlparse.urljoin(self.get_baseuri(dc), self.path)
209 uri=urllib.unquote(uri)
211 if self.headers.has_key('If-Match'):
215 for match in self.headers['If-Match'].split(','):
221 if dc.match_prop(uri, match, "DAV:", "getetag"):
226 self.send_status(412)
229 # Handle If-None-Match
230 if self.headers.has_key('If-None-Match'):
233 for match in self.headers['If-None-Match'].split(','):
239 if dc.match_prop(uri, match, "DAV:", "getetag"):
244 self.send_status(412)
248 expect = self.headers.get('Expect', '')
249 if (expect.lower() == '100-continue' and
250 self.protocol_version >= 'HTTP/1.1' and
251 self.request_version >= 'HTTP/1.1'):
252 self.send_status(100)
256 body=self._get_body()
258 # locked resources are not allowed to be overwritten
259 if self._l_isLocked(uri):
260 return self.send_body(None, '423', 'Locked', 'Locked')
263 if self.headers.has_key("Content-Type"):
264 ct=self.headers['Content-Type']
266 location = dc.put(uri, body, ct)
267 except DAV_Error, (ec,dd):
268 self.log_error("Cannot PUT to %s: %s", uri, dd)
269 return self.send_status(ec)
273 if location and isinstance(location, tuple):
275 location = location[0]
276 # note that we have allowed for > 2 elems
278 headers['Location'] = location
282 etag = dc.get_prop(location or uri, "DAV:", "getetag")
284 headers['ETag'] = str(etag)
288 self.send_body(None, '201', 'Created', '', headers=headers)
292 if self.headers.has_key("Content-Length"):
293 l=self.headers['Content-Length']
294 body=self.rfile.read(atoi(l))
299 DAVRequestHandler.do_DELETE(self)
300 except DAV_Error, (ec, dd):
301 return self.send_status(ec)
304 """ Unlocks given resource """
306 dc = self.IFACE_CLASS
307 self.log_message('UNLOCKing resource %s' % self.headers)
309 uri = urlparse.urljoin(self.get_baseuri(dc), self.path)
310 uri = urllib.unquote(uri)
312 token = self.headers.get('Lock-Token', False)
314 token = token.strip()
315 if token[0] == '<' and token[-1] == '>':
321 return self.send_status(400, 'Bad lock token')
324 res = dc.unlock(uri, token)
325 except DAV_Error, (ec, dd):
326 return self.send_status(ec, dd)
329 self.send_body(None, '204', 'OK', 'Resource unlocked.')
331 # We just differentiate the description, for debugging purposes
332 self.send_body(None, '204', 'OK', 'Resource not locked.')
335 """ Attempt to place a lock on the given resource.
338 dc = self.IFACE_CLASS
341 self.log_message('LOCKing resource %s' % self.headers)
344 if self.headers.has_key('Content-Length'):
345 l = self.headers['Content-Length']
346 body = self.rfile.read(atoi(l))
348 depth = self.headers.get('Depth', 'infinity')
350 uri = urlparse.urljoin(self.get_baseuri(dc), self.path)
351 uri = urllib.unquote(uri)
352 self.log_message('do_LOCK: uri = %s' % uri)
354 ifheader = self.headers.get('If')
357 ldif = IfParser(ifheader)
358 if isinstance(ldif, list):
359 if len(ldif) !=1 or (not isinstance(ldif[0], TagList)) \
360 or len(ldif[0].list) != 1:
361 raise DAV_Error(400, "Cannot accept multiple tokens.")
362 ldif = ldif[0].list[0]
363 if ldif[0] == '<' and ldif[-1] == '>':
366 lock_data['token'] = ldif
369 lock_data['refresh'] = True
371 lock_data['refresh'] = False
372 lock_data.update(self._lock_unlock_parse(body))
374 if lock_data['refresh'] and not lock_data.get('token', False):
375 raise DAV_Error(400, 'Lock refresh must specify token.')
377 lock_data['depth'] = depth
380 created, data, lock_token = dc.lock(uri, lock_data)
381 except DAV_Error, (ec, dd):
382 return self.send_status(ec, dd)
385 if not lock_data['refresh']:
386 headers['Lock-Token'] = '<%s>' % lock_token
389 self.send_body(data, '201', 'Created', ctype='text/xml', headers=headers)
391 self.send_body(data, '200', 'OK', ctype='text/xml', headers=headers)
393 def _lock_unlock_parse(self, body):
394 # Override the python-webdav function, with some improvements
395 # Unlike the py-webdav one, we also parse the owner minidom elements into
396 # pure pythonic struct.
397 doc = minidom.parseString(body)
401 for info in doc.getElementsByTagNameNS('DAV:', 'lockinfo'):
402 for scope in info.getElementsByTagNameNS('DAV:', 'lockscope'):
403 for scc in scope.childNodes:
404 if scc.nodeType == info.ELEMENT_NODE \
405 and scc.namespaceURI == 'DAV:':
406 data['lockscope'] = scc.localName
408 for ltype in info.getElementsByTagNameNS('DAV:', 'locktype'):
409 for ltc in ltype.childNodes:
410 if ltc.nodeType == info.ELEMENT_NODE \
411 and ltc.namespaceURI == 'DAV:':
412 data['locktype'] = ltc.localName
414 for own in info.getElementsByTagNameNS('DAV:', 'owner'):
415 for ono in own.childNodes:
416 if ono.nodeType == info.TEXT_NODE:
418 owners.append(ono.data)
419 elif ono.nodeType == info.ELEMENT_NODE \
420 and ono.namespaceURI == 'DAV:' \
421 and ono.localName == 'href':
423 for hno in ono.childNodes:
424 if hno.nodeType == info.TEXT_NODE:
426 owners.append(('href','DAV:', href))
429 data['lockowner'] = owners[0]
433 data['lockowner'] = owners
436 from openerp.service.http_server import reg_http_service,OpenERPAuthProvider
438 class DAVAuthProvider(OpenERPAuthProvider):
439 def authenticate(self, db, user, passwd, client_address):
440 """ authenticate, but also allow the False db, meaning to skip
441 authentication when no db is specified.
445 return OpenERPAuthProvider.authenticate(self, db, user, passwd, client_address)
448 class dummy_dav_interface(object):
449 """ Dummy dav interface """
452 PROPS={"DAV:" : ('creationdate',
459 M_NS={"DAV:" : "_get_dav", }
461 def __init__(self, parent):
464 def get_propnames(self, uri):
467 def get_prop(self, uri, ns, propname):
468 if self.M_NS.has_key(ns):
472 mname=prefix+"_"+propname.replace('-', '_')
474 m=getattr(self,mname)
477 except AttributeError:
480 def get_data(self, uri, range=None):
483 def _get_dav_creationdate(self, uri):
484 return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
486 def _get_dav_getlastmodified(self, uri):
487 return time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime())
489 def _get_dav_displayname(self, uri):
492 def _get_dav_resourcetype(self, uri):
493 return ('collection', 'DAV:')
495 def exists(self, uri):
496 """ return 1 or None depending on if a resource exists """
497 uri2 = uri.split('/')
500 _logger.debug("Requested uri: %s", uri)
503 def is_collection(self, uri):
504 """ return 1 or None depending on if a resource is a collection """
507 class DAVStaticHandler(http_server.StaticHTTPHandler):
508 """ A variant of the Static handler, which will serve dummy DAV requests
512 protocol_version = 'HTTP/1.1'
513 _HTTP_OPTIONS= { 'DAV' : ['1', '2'],
514 'Allow' : [ 'GET', 'HEAD',
515 'PROPFIND', 'OPTIONS', 'REPORT', ]
518 def send_body(self, content, code, message='OK', content_type='text/xml'):
519 self.send_response(int(code), message)
520 self.send_header("Content-Type", content_type)
521 # self.send_header('Connection', 'close')
522 self.send_header('Content-Length', len(content) or 0)
524 if hasattr(self, '_flush'):
527 if self.command != 'HEAD':
528 self.wfile.write(content)
530 def do_PROPFIND(self):
531 """Answer to PROPFIND with generic data.
533 A rough copy of python-webdav's do_PROPFIND, but hacked to work
537 dc = dummy_dav_interface(self)
539 # read the body containing the xml request
540 # iff there is no body then this is an ALLPROP request
542 if self.headers.has_key('Content-Length'):
543 l = self.headers['Content-Length']
544 body = self.rfile.read(atoi(l))
546 path = self.path.rstrip('/')
547 uri = urllib.unquote(path)
549 pf = PROPFIND(uri, dc, self.headers.get('Depth', 'infinity'), body)
552 DATA = '%s\n' % pf.createResponse()
553 except DAV_Error, (ec,dd):
554 return self.send_error(ec,dd)
556 self.log_exception("Cannot PROPFIND")
559 # work around MSIE DAV bug for creation and modified date
560 # taken from Resource.py @ Zope webdav
561 if (self.headers.get('User-Agent') ==
562 'Microsoft Data Access Internet Publishing Provider DAV 1.1'):
563 DATA = DATA.replace('<ns0:getlastmodified xmlns:ns0="DAV:">',
564 '<ns0:getlastmodified xmlns:n="DAV:" xmlns:b="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/" b:dt="dateTime.rfc1123">')
565 DATA = DATA.replace('<ns0:creationdate xmlns:ns0="DAV:">',
566 '<ns0:creationdate xmlns:n="DAV:" xmlns:b="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/" b:dt="dateTime.tz">')
568 self.send_body(DATA, '207','Multi-Status','Multiple responses')
570 def not_get_baseuri(self):
572 if self.headers.has_key('Host'):
573 uparts = list(urlparse.urlparse('/'))
574 uparts[1] = self.headers['Host']
575 baseuri = urlparse.urlunparse(uparts)
578 def get_davpath(self):
584 if (config.get_misc('webdav','enable',True)):
585 directory = '/'+config.get_misc('webdav','vdir','webdav')
587 verbose = config.get_misc('webdav','verbose',True)
588 handler.debug = config.get_misc('webdav','debug',True)
589 _dc = { 'verbose' : verbose,
590 'directory' : directory,
591 'lockemulation' : True,
594 conf = OpenDAVConfig(**_dc)
595 handler._config = conf
596 reg_http_service(directory, DAVHandler, DAVAuthProvider)
597 _logger.info("WebDAV service registered at path: %s/ "% directory)
599 if not (config.get_misc('webdav', 'no_root_hack', False)):
600 # Now, replace the static http handler with the dav-enabled one.
601 # If a static-http service has been specified for our server, then
602 # read its configuration and use that dir_path.
603 # NOTE: this will _break_ any other service that would be registered
604 # at the root path in future.
606 if config.get_misc('static-http','enable', False):
607 base_path = config.get_misc('static-http', 'base_path', '/')
608 if base_path and base_path == '/':
609 dir_path = config.get_misc('static-http', 'dir_path', False)
611 dir_path = openerp.modules.module.get_module_resource('document_webdav','public_html')
612 # an _ugly_ hack: we put that dir back in tools.config.misc, so that
613 # the StaticHttpHandler can find its dir_path.
614 config.misc.setdefault('static-http',{})['dir_path'] = dir_path
616 reg_http_service('/', DAVStaticHandler)
619 _logger.error('Cannot launch webdav: %s' % e)
622 def init_well_known():
623 reps = RedirectHTTPHandler.redirect_paths
625 num_svcs = config.get_misc('http-well-known', 'num_services', '0')
627 for nsv in range(1, int(num_svcs)+1):
628 uri = config.get_misc('http-well-known', 'service_%d' % nsv, False)
629 path = config.get_misc('http-well-known', 'path_%d' % nsv, False)
630 if not (uri and path):
635 reg_http_service('/.well-known', RedirectHTTPHandler)
639 class PrincipalsRedirect(RedirectHTTPHandler):
644 def _find_redirect(self):
645 for b, r in self.redirect_paths.items():
646 if self.path.startswith(b):
647 return r + self.path[len(b):]
650 def init_principals_redirect():
651 """ Some devices like the iPhone will look under /principals/users/xxx for
652 the user's properties. In OpenERP we _cannot_ have a stray /principals/...
653 working path, since we have a database path and the /webdav/ component. So,
654 the best solution is to redirect the url with 301. Luckily, it does work in
655 the device. The trick is that we need to hard-code the database to use, either
656 the one centrally defined in the config, or a "forced" one in the webdav
659 dbname = config.get_misc('webdav', 'principal_dbname', False)
660 if (not dbname) and not config.get_misc('webdav', 'no_principals_redirect', False):
661 dbname = config.get('db_name', False)
663 PrincipalsRedirect.redirect_paths[''] = '/webdav/%s/principals' % dbname
664 reg_http_service('/principals', PrincipalsRedirect)
666 "Registered HTTP redirect handler for /principals to the %s db.",
669 init_principals_redirect()
676 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: