1"""DAV HTTP Server 2 3This module builds on BaseHTTPServer and implements DAV commands 4 5""" 6import AuthServer 7import urlparse 8import urllib 9import logging 10 11from propfind import PROPFIND 12from report import REPORT 13from delete import DELETE 14from davcopy import COPY 15from davmove import MOVE 16 17from utils import rfc1123_date, IfParser, tokenFinder 18from string import atoi 19from errors import DAV_Error, DAV_NotFound 20 21from constants import DAV_VERSION_1, DAV_VERSION_2 22from locks import LockManager 23import gzip 24import StringIO 25 26from pywebdav.lib import VERSION 27 28from xml.parsers.expat import ExpatError 29 30log = logging.getLogger(__name__) 31 32BUFFER_SIZE = 128 * 1000 # 128 Ko 33 34 35class DAVRequestHandler(AuthServer.AuthRequestHandler, LockManager): 36 """Simple DAV request handler with 37 38 - GET 39 - HEAD 40 - PUT 41 - OPTIONS 42 - PROPFIND 43 - PROPPATCH 44 - MKCOL 45 - REPORT 46 47 experimental 48 - LOCK 49 - UNLOCK 50 51 It uses the resource/collection classes for serving and 52 storing content. 53 54 """ 55 56 server_version = "DAV/" + VERSION 57 encode_threshold = 1400 # common MTU 58 59 def send_body(self, DATA, code=None, msg=None, desc=None, 60 ctype='application/octet-stream', headers={}): 61 """ send a body in one part """ 62 log.debug("Use send_body method") 63 64 self.send_response(code, message=msg) 65 self.send_header("Connection", "close") 66 self.send_header("Accept-Ranges", "bytes") 67 self.send_header('Date', rfc1123_date()) 68 69 self._send_dav_version() 70 71 for a, v in headers.items(): 72 self.send_header(a, v) 73 74 if DATA: 75 if 'gzip' in self.headers.get('Accept-Encoding', '').split(',') \ 76 and len(DATA) > self.encode_threshold: 77 buffer = StringIO.StringIO() 78 output = gzip.GzipFile(mode='wb', fileobj=buffer) 79 if isinstance(DATA, str) or isinstance(DATA, unicode): 80 output.write(DATA) 81 else: 82 for buf in DATA: 83 output.write(buf) 84 output.close() 85 buffer.seek(0) 86 DATA = buffer.getvalue() 87 self.send_header('Content-Encoding', 'gzip') 88 89 self.send_header('Content-Length', len(DATA)) 90 self.send_header('Content-Type', ctype) 91 else: 92 self.send_header('Content-Length', 0) 93 94 self.end_headers() 95 if DATA: 96 if isinstance(DATA, str) or isinstance(DATA, unicode): 97 log.debug("Don't use iterator") 98 self.wfile.write(DATA) 99 else: 100 if self._config.DAV.getboolean('http_response_use_iterator'): 101 # Use iterator to reduce using memory 102 log.debug("Use iterator") 103 for buf in DATA: 104 self.wfile.write(buf) 105 self.wfile.flush() 106 else: 107 # Don't use iterator, it's a compatibility option 108 log.debug("Don't use iterator") 109 self.wfile.write(DATA.read()) 110 111 def send_body_chunks_if_http11(self, DATA, code, msg=None, desc=None, 112 ctype='text/xml; encoding="utf-8"', 113 headers={}): 114 if (self.request_version == 'HTTP/1.0' or 115 not self._config.DAV.getboolean('chunked_http_response')): 116 self.send_body(DATA, code, msg, desc, ctype, headers) 117 else: 118 self.send_body_chunks(DATA, code, msg, desc, ctype, headers) 119 120 def send_body_chunks(self, DATA, code, msg=None, desc=None, 121 ctype='text/xml"', headers={}): 122 """ send a body in chunks """ 123 124 self.responses[207] = (msg, desc) 125 self.send_response(code, message=msg) 126 self.send_header("Content-type", ctype) 127 self.send_header("Transfer-Encoding", "chunked") 128 self.send_header('Date', rfc1123_date()) 129 130 self._send_dav_version() 131 132 for a, v in headers.items(): 133 self.send_header(a, v) 134 135 if DATA: 136 if ('gzip' in self.headers.get('Accept-Encoding', '').split(',') 137 and len(DATA) > self.encode_threshold): 138 buffer = StringIO.StringIO() 139 output = gzip.GzipFile(mode='wb', fileobj=buffer) 140 if isinstance(DATA, str): 141 output.write(DATA) 142 else: 143 for buf in DATA: 144 output.write(buf) 145 output.close() 146 buffer.seek(0) 147 DATA = buffer.getvalue() 148 self.send_header('Content-Encoding', 'gzip') 149 150 self.send_header('Content-Length', len(DATA)) 151 self.send_header('Content-Type', ctype) 152 153 else: 154 self.send_header('Content-Length', 0) 155 156 self.end_headers() 157 158 if DATA: 159 if isinstance(DATA, str) or isinstance(DATA, unicode): 160 self.wfile.write(hex(len(DATA))[2:] + "\r\n") 161 self.wfile.write(DATA) 162 self.wfile.write("\r\n") 163 self.wfile.write("0\r\n") 164 self.wfile.write("\r\n") 165 else: 166 if self._config.DAV.getboolean('http_response_use_iterator'): 167 # Use iterator to reduce using memory 168 for buf in DATA: 169 self.wfile.write(hex(len(buf))[2:] + "\r\n") 170 self.wfile.write(buf) 171 self.wfile.write("\r\n") 172 173 self.wfile.write("0\r\n") 174 self.wfile.write("\r\n") 175 else: 176 # Don't use iterator, it's a compatibility option 177 self.wfile.write(hex(len(DATA))[2:] + "\r\n") 178 self.wfile.write(DATA.read()) 179 self.wfile.write("\r\n") 180 self.wfile.write("0\r\n") 181 self.wfile.write("\r\n") 182 183 def _send_dav_version(self): 184 if self._config.DAV.getboolean('lockemulation'): 185 self.send_header('DAV', DAV_VERSION_2['version']) 186 else: 187 self.send_header('DAV', DAV_VERSION_1['version']) 188 189 ### HTTP METHODS called by the server 190 191 def do_OPTIONS(self): 192 """return the list of capabilities """ 193 194 self.send_response(200) 195 self.send_header("Content-Length", 0) 196 197 if self._config.DAV.getboolean('lockemulation'): 198 self.send_header('Allow', DAV_VERSION_2['options']) 199 else: 200 self.send_header('Allow', DAV_VERSION_1['options']) 201 202 self._send_dav_version() 203 204 self.send_header('MS-Author-Via', 'DAV') # this is for M$ 205 self.end_headers() 206 207 def _HEAD_GET(self, with_body=False): 208 """ Returns headers and body for given resource """ 209 210 dc = self.IFACE_CLASS 211 uri = urlparse.urljoin(self.get_baseuri(dc), self.path) 212 uri = urllib.unquote(uri) 213 214 headers = {} 215 216 # get the last modified date (RFC 1123!) 217 try: 218 headers['Last-Modified'] = dc.get_prop( 219 uri, "DAV:", "getlastmodified") 220 except DAV_NotFound: 221 pass 222 223 # get the ETag if any 224 try: 225 headers['Etag'] = dc.get_prop(uri, "DAV:", "getetag") 226 except DAV_NotFound: 227 pass 228 229 # get the content type 230 try: 231 content_type = dc.get_prop(uri, "DAV:", "getcontenttype") 232 except DAV_NotFound: 233 content_type = "application/octet-stream" 234 235 range = None 236 status_code = 200 237 if 'Range' in self.headers: 238 p = self.headers['Range'].find("bytes=") 239 if p != -1: 240 range = self.headers['Range'][p + 6:].split("-") 241 status_code = 206 242 243 # get the data 244 try: 245 data = dc.get_data(uri, range) 246 except DAV_Error, (ec, dd): 247 self.send_status(ec) 248 return ec 249 250 # send the data 251 if with_body is False: 252 data = None 253 254 if isinstance(data, str) or isinstance(data, unicode): 255 self.send_body(data, status_code, None, None, content_type, 256 headers) 257 else: 258 headers['Keep-Alive'] = 'timeout=15, max=86' 259 headers['Connection'] = 'Keep-Alive' 260 self.send_body_chunks_if_http11(data, status_code, None, None, 261 content_type, headers) 262 263 return status_code 264 265 def do_HEAD(self): 266 """ Send a HEAD response: Retrieves resource information w/o body """ 267 268 return self._HEAD_GET(with_body=False) 269 270 def do_GET(self): 271 """Serve a GET request.""" 272 273 log.debug(self.headers) 274 275 try: 276 status_code = self._HEAD_GET(with_body=True) 277 self.log_request(status_code) 278 return status_code 279 except IOError, e: 280 if e.errno == 32: 281 self.log_request(206) 282 else: 283 raise 284 285 def do_TRACE(self): 286 """ This will always fail because we can not reproduce HTTP requests. 287 We send back a 405=Method Not Allowed. """ 288 289 self.send_body(None, 405, 'Method Not Allowed', 'Method Not Allowed') 290 291 def do_POST(self): 292 """ Replacement for GET response. Not implemented here. """ 293 294 self.send_body(None, 405, 'Method Not Allowed', 'Method Not Allowed') 295 296 def do_PROPPATCH(self): 297 # currently unsupported 298 return self.send_status(423) 299 300 def do_PROPFIND(self): 301 """ Retrieve properties on defined resource. """ 302 303 dc = self.IFACE_CLASS 304 305 # read the body containing the xml request 306 # iff there is no body then this is an ALLPROP request 307 body = None 308 if 'Content-Length' in self.headers: 309 l = self.headers['Content-Length'] 310 body = self.rfile.read(atoi(l)) 311 312 uri = urlparse.urljoin(self.get_baseuri(dc), self.path) 313 uri = urllib.unquote(uri) 314 315 try: 316 pf = PROPFIND(uri, dc, self.headers.get('Depth', 'infinity'), body) 317 except ExpatError: 318 # parse error 319 return self.send_status(400) 320 321 try: 322 DATA = '%s\n' % pf.createResponse() 323 except DAV_Error, (ec, dd): 324 return self.send_status(ec) 325 326 # work around MSIE DAV bug for creation and modified date 327 # taken from Resource.py @ Zope webdav 328 if (self.headers.get('User-Agent') == 329 'Microsoft Data Access Internet Publishing Provider DAV 1.1'): 330 DATA = DATA.replace('<ns0:getlastmodified xmlns:ns0="DAV:">', 331 '<ns0:getlastmodified xmlns:n="DAV:" ' 332 'xmlns:b="urn:uuid:' 333 'c2f41010-65b3-11d1-a29f-00aa00c14882/" ' 334 'b:dt="dateTime.rfc1123">') 335 DATA = DATA.replace('<ns0:creationdate xmlns:ns0="DAV:">', 336 '<ns0:creationdate xmlns:n="DAV:" ' 337 'xmlns:b="urn:uuid:' 338 'c2f41010-65b3-11d1-a29f-00aa00c14882/" ' 339 'b:dt="dateTime.tz">') 340 341 self.send_body_chunks_if_http11(DATA, 207, 'Multi-Status', 342 'Multiple responses') 343 344 def do_REPORT(self): 345 """ Query properties on defined resource. """ 346 347 dc = self.IFACE_CLASS 348 349 # read the body containing the xml request 350 # iff there is no body then this is an ALLPROP request 351 body = None 352 if 'Content-Length' in self.headers: 353 l = self.headers['Content-Length'] 354 body = self.rfile.read(atoi(l)) 355 356 uri = urlparse.urljoin(self.get_baseuri(dc), self.path) 357 uri = urllib.unquote(uri) 358 359 rp = REPORT(uri, dc, self.headers.get('Depth', '0'), body) 360 361 try: 362 DATA = '%s\n' % rp.createResponse() 363 except DAV_Error, (ec, dd): 364 return self.send_status(ec) 365 366 self.send_body_chunks_if_http11(DATA, 207, 'Multi-Status', 367 'Multiple responses') 368 369 def do_MKCOL(self): 370 """ create a new collection """ 371 372 # according to spec body must be empty 373 body = None 374 if 'Content-Length' in self.headers: 375 l = self.headers['Content-Length'] 376 body = self.rfile.read(atoi(l)) 377 378 if body: 379 return self.send_status(415) 380 381 dc = self.IFACE_CLASS 382 uri = urlparse.urljoin(self.get_baseuri(dc), self.path) 383 uri = urllib.unquote(uri) 384 385 try: 386 dc.mkcol(uri) 387 self.send_status(201) 388 self.log_request(201) 389 except DAV_Error, (ec, dd): 390 self.log_request(ec) 391 return self.send_status(ec) 392 393 def do_DELETE(self): 394 """ delete an resource """ 395 396 dc = self.IFACE_CLASS 397 uri = urlparse.urljoin(self.get_baseuri(dc), self.path) 398 uri = urllib.unquote(uri) 399 400 # hastags not allowed 401 if uri.find('#') >= 0: 402 return self.send_status(404) 403 404 # locked resources are not allowed to delete 405 if self._l_isLocked(uri): 406 return self.send_body(None, 423, 'Locked', 'Locked') 407 408 # Handle If-Match 409 if 'If-Match' in self.headers: 410 test = False 411 etag = None 412 try: 413 etag = dc.get_prop(uri, "DAV:", "getetag") 414 except: 415 pass 416 for match in self.headers['If-Match'].split(','): 417 if match == '*': 418 if dc.exists(uri): 419 test = True 420 break 421 else: 422 if match == etag: 423 test = True 424 break 425 if not test: 426 self.send_status(412) 427 self.log_request(412) 428 return 429 430 # Handle If-None-Match 431 if 'If-None-Match' in self.headers: 432 test = True 433 etag = None 434 try: 435 etag = dc.get_prop(uri, "DAV:", "getetag") 436 except: 437 pass 438 for match in self.headers['If-None-Match'].split(','): 439 if match == '*': 440 if dc.exists(uri): 441 test = False 442 break 443 else: 444 if match == etag: 445 test = False 446 break 447 if not test: 448 self.send_status(412) 449 self.log_request(412) 450 return 451 452 try: 453 dl = DELETE(uri, dc) 454 if dc.is_collection(uri): 455 res = dl.delcol() 456 if res: 457 self.send_status(207, body=res) 458 else: 459 self.send_status(204) 460 else: 461 res = dl.delone() or 204 462 self.send_status(res) 463 except DAV_NotFound: 464 self.send_body(None, 404, 'Not Found', 'Not Found') 465 466 def do_PUT(self): 467 dc = self.IFACE_CLASS 468 uri = urlparse.urljoin(self.get_baseuri(dc), self.path) 469 uri = urllib.unquote(uri) 470 471 log.debug("do_PUT: uri = %s" % uri) 472 log.debug('do_PUT: headers = %s' % self.headers) 473 # Handle If-Match 474 if 'If-Match' in self.headers: 475 log.debug("do_PUT: If-Match %s" % self.headers['If-Match']) 476 test = False 477 etag = None 478 try: 479 etag = dc.get_prop(uri, "DAV:", "getetag") 480 except: 481 pass 482 483 log.debug("do_PUT: etag = %s" % etag) 484 485 for match in self.headers['If-Match'].split(','): 486 if match == '*': 487 if dc.exists(uri): 488 test = True 489 break 490 else: 491 if match == etag: 492 test = True 493 break 494 if not test: 495 self.send_status(412) 496 self.log_request(412) 497 return 498 499 # Handle If-None-Match 500 if 'If-None-Match' in self.headers: 501 log.debug("do_PUT: If-None-Match %s" % 502 self.headers['If-None-Match']) 503 504 test = True 505 etag = None 506 try: 507 etag = dc.get_prop(uri, "DAV:", "getetag") 508 except: 509 pass 510 511 log.debug("do_PUT: etag = %s" % etag) 512 513 for match in self.headers['If-None-Match'].split(','): 514 if match == '*': 515 if dc.exists(uri): 516 test = False 517 break 518 else: 519 if match == etag: 520 test = False 521 break 522 if not test: 523 self.send_status(412) 524 self.log_request(412) 525 return 526 527 # locked resources are not allowed to be overwritten 528 ifheader = self.headers.get('If') 529 if ( 530 (self._l_isLocked(uri)) and 531 (not ifheader) 532 ): 533 return self.send_body(None, 423, 'Locked', 'Locked') 534 535 if ((self._l_isLocked(uri)) and (ifheader)): 536 uri_token = self._l_getLockForUri(uri) 537 taglist = IfParser(ifheader) 538 found = False 539 for tag in taglist: 540 for listitem in tag.list: 541 token = tokenFinder(listitem) 542 if ( 543 token and 544 (self._l_hasLock(token)) and 545 (self._l_getLock(token) == uri_token) 546 ): 547 found = True 548 break 549 if found: 550 break 551 if not found: 552 res = self.send_body(None, 423, 'Locked', 'Locked') 553 self.log_request(423) 554 return res 555 556 # Handle expect 557 expect = self.headers.get('Expect', '') 558 if (expect.lower() == '100-continue' and 559 self.protocol_version >= 'HTTP/1.1' and 560 self.request_version >= 'HTTP/1.1'): 561 self.send_status(100) 562 563 content_type = None 564 if 'Content-Type' in self.headers: 565 content_type = self.headers['Content-Type'] 566 567 headers = {} 568 headers['Location'] = uri 569 570 try: 571 etag = dc.get_prop(uri, "DAV:", "getetag") 572 headers['ETag'] = etag 573 except: 574 pass 575 576 expect = self.headers.get('transfer-encoding', '') 577 if ( 578 expect.lower() == 'chunked' and 579 self.protocol_version >= 'HTTP/1.1' and 580 self.request_version >= 'HTTP/1.1' 581 ): 582 self.send_body(None, 201, 'Created', '', headers=headers) 583 584 dc.put(uri, self._readChunkedData(), content_type) 585 else: 586 # read the body 587 body = None 588 if 'Content-Length' in self.headers: 589 l = self.headers['Content-Length'] 590 log.debug("do_PUT: Content-Length = %s" % l) 591 body = self._readNoChunkedData(atoi(l)) 592 else: 593 log.debug("do_PUT: Content-Length = empty") 594 595 try: 596 dc.put(uri, body, content_type) 597 except DAV_Error, (ec, dd): 598 return self.send_status(ec) 599 600 self.send_body(None, 201, 'Created', '', headers=headers) 601 self.log_request(201) 602 603 def _readChunkedData(self): 604 l = int(self.rfile.readline(), 16) 605 while l > 0: 606 buf = self.rfile.read(l) 607 yield buf 608 self.rfile.readline() 609 l = int(self.rfile.readline(), 16) 610 611 def _readNoChunkedData(self, content_length): 612 if self._config.DAV.getboolean('http_request_use_iterator'): 613 # Use iterator to reduce using memory 614 return self.__readNoChunkedDataWithIterator(content_length) 615 else: 616 # Don't use iterator, it's a compatibility option 617 return self.__readNoChunkedDataWithoutIterator(content_length) 618 619 def __readNoChunkedDataWithIterator(self, content_length): 620 while True: 621 if content_length > BUFFER_SIZE: 622 buf = self.rfile.read(BUFFER_SIZE) 623 content_length -= BUFFER_SIZE 624 yield buf 625 else: 626 buf = self.rfile.read(content_length) 627 yield buf 628 break 629 630 def __readNoChunkedDataWithoutIterator(self, content_length): 631 return self.rfile.read(content_length) 632 633 def do_COPY(self): 634 """ copy one resource to another """ 635 try: 636 self.copymove(COPY) 637 except DAV_Error, (ec, dd): 638 return self.send_status(ec) 639 640 def do_MOVE(self): 641 """ move one resource to another """ 642 try: 643 self.copymove(MOVE) 644 except DAV_Error, (ec, dd): 645 return self.send_status(ec) 646 647 def copymove(self, CLASS): 648 """ common method for copying or moving objects """ 649 dc = self.IFACE_CLASS 650 651 # get the source URI 652 source_uri = urlparse.urljoin(self.get_baseuri(dc), self.path) 653 source_uri = urllib.unquote(source_uri) 654 655 # get the destination URI 656 dest_uri = self.headers['Destination'] 657 dest_uri = urllib.unquote(dest_uri) 658 659 # check locks on source and dest 660 if self._l_isLocked(source_uri) or self._l_isLocked(dest_uri): 661 return self.send_body(None, 423, 'Locked', 'Locked') 662 663 # Overwrite? 664 overwrite = 1 665 result_code = 204 666 if 'Overwrite' in self.headers: 667 if self.headers['Overwrite'] == "F": 668 overwrite = None 669 result_code = 201 670 671 # instanciate ACTION class 672 cp = CLASS(dc, source_uri, dest_uri, overwrite) 673 674 # Depth? 675 d = "infinity" 676 if 'Depth' in self.headers: 677 d = self.headers['Depth'] 678 679 if d != "0" and d != "infinity": 680 self.send_status(400) 681 return 682 683 if d == "0": 684 res = cp.single_action() 685 self.send_status(res or 201) 686 return 687 688 # now it only can be "infinity" but we nevertheless check for a 689 # collection 690 if dc.is_collection(source_uri): 691 try: 692 res = cp.tree_action() 693 except DAV_Error, (ec, dd): 694 self.send_status(ec) 695 return 696 else: 697 try: 698 res = cp.single_action() 699 except DAV_Error, (ec, dd): 700 self.send_status(ec) 701 return 702 703 if res: 704 self.send_body_chunks_if_http11(res, 207, self.responses[207][0], 705 self.responses[207][1], 706 ctype='text/xml; charset="utf-8"') 707 else: 708 self.send_status(result_code) 709 710 def get_userinfo(self, user, pw): 711 """ Dummy method which lets all users in """ 712 return 1 713 714 def send_status(self, code=200, mediatype='text/xml; charset="utf-8"', 715 msg=None, body=None): 716 717 if not msg: 718 msg = self.responses.get(code, ['', ''])[1] 719 720 self.send_body(body, code, self.responses.get(code, [''])[0], msg, 721 mediatype) 722 723 def get_baseuri(self, dc): 724 baseuri = dc.baseuri 725 if 'Host' in self.headers: 726 uparts = list(urlparse.urlparse(dc.baseuri)) 727 uparts[1] = self.headers['Host'] 728 baseuri = urlparse.urlunparse(uparts) 729 return baseuri 730