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