2008-12-30 16:25:42 +00:00
# This file is part of Radicale Server - Calendar Server
2009-07-27 15:04:54 +00:00
# Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter
2016-03-31 19:57:40 +02:00
# Copyright © 2008-2016 Guillaume Ayoub
2008-12-30 16:25:42 +00:00
#
# This library 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 library 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 Radicale. If not, see <http://www.gnu.org/licenses/>.
2010-01-19 20:31:21 +01:00
"""
Radicale Server module .
2011-05-01 14:46:29 +02:00
This module offers a WSGI application class .
2010-01-19 20:31:21 +01:00
To use this module , you should take a look at the file ` ` radicale . py ` ` that
should have been included in this package .
"""
2010-01-21 18:52:53 +01:00
import base64
2016-06-10 14:36:44 +02:00
import contextlib
2016-08-25 04:33:14 +02:00
import io
2016-08-11 02:10:09 +02:00
import itertools
2016-08-02 14:37:39 +02:00
import os
2016-08-04 06:08:08 +02:00
import posixpath
2016-08-02 14:37:39 +02:00
import pprint
2011-05-01 15:25:52 +02:00
import socket
2016-05-21 00:52:22 +02:00
import socketserver
2011-05-11 06:21:35 +02:00
import ssl
2016-08-02 14:37:39 +02:00
import threading
2011-05-01 15:25:52 +02:00
import wsgiref . simple_server
2016-05-21 02:26:03 +02:00
import zlib
2016-09-17 15:35:43 +02:00
import datetime
2016-03-31 19:57:40 +02:00
from http import client
from urllib . parse import unquote , urlparse
2008-12-30 16:25:42 +00:00
2016-04-22 11:37:02 +09:00
import vobject
from . import auth , rights , storage , xmlutils
2010-02-10 18:57:21 +01:00
2010-01-21 18:52:53 +01:00
2016-07-30 16:43:29 +02:00
VERSION = " 2.0.0rc0 "
2010-05-31 00:49:52 +02:00
2016-09-02 11:04:29 +02:00
NOT_ALLOWED = (
2016-10-12 14:50:53 +02:00
client . FORBIDDEN , ( ( " Content-Type " , " text/plain " ) , ) ,
2016-09-02 11:04:29 +02:00
" Access to the requested resource forbidden. " )
NOT_FOUND = (
2016-10-12 14:50:53 +02:00
client . NOT_FOUND , ( ( " Content-Type " , " text/plain " ) , ) ,
2016-09-02 11:04:29 +02:00
" The requested resource could not be found. " )
WEBDAV_PRECONDITION_FAILED = (
2016-10-12 14:50:53 +02:00
client . CONFLICT , ( ( " Content-Type " , " text/plain " ) , ) ,
2016-09-02 11:04:29 +02:00
" WebDAV precondition failed. " )
PRECONDITION_FAILED = (
client . PRECONDITION_FAILED ,
2016-10-12 14:50:53 +02:00
( ( " Content-Type " , " text/plain " ) , ) , " Precondition failed. " )
2016-09-02 11:04:29 +02:00
REQUEST_TIMEOUT = (
2016-10-12 14:50:53 +02:00
client . REQUEST_TIMEOUT , ( ( " Content-Type " , " text/plain " ) , ) ,
2016-09-02 11:04:29 +02:00
" Connection timed out. " )
REQUEST_ENTITY_TOO_LARGE = (
2016-10-12 14:50:53 +02:00
client . REQUEST_ENTITY_TOO_LARGE , ( ( " Content-Type " , " text/plain " ) , ) ,
2016-09-02 11:04:29 +02:00
" Request body too large. " )
REMOTE_DESTINATION = (
2016-10-12 14:50:53 +02:00
client . BAD_GATEWAY , ( ( " Content-Type " , " text/plain " ) , ) ,
2016-09-02 11:04:29 +02:00
" Remote destination not supported. " )
DIRECTORY_LISTING = (
2016-10-12 14:50:53 +02:00
client . FORBIDDEN , ( ( " Content-Type " , " text/plain " ) , ) ,
2016-09-02 11:04:29 +02:00
" Directory listings are not supported. " )
2016-08-31 00:41:08 +02:00
2016-08-05 02:14:49 +02:00
DAV_HEADERS = " 1, 2, 3, calendar-access, addressbook, extended-mkcol "
2012-08-15 23:39:18 +02:00
2011-02-08 19:27:00 +01:00
2016-05-12 18:57:59 +02:00
class HTTPServer ( wsgiref . simple_server . WSGIServer ) :
2011-05-01 15:25:52 +02:00
""" HTTP server. """
2016-06-10 14:33:25 +02:00
# These class attributes must be set before creating instance
client_timeout = None
2016-06-10 14:36:44 +02:00
max_connections = None
2016-06-10 14:33:25 +02:00
2011-05-01 15:25:52 +02:00
def __init__ ( self , address , handler , bind_and_activate = True ) :
""" Create server. """
ipv6 = " : " in address [ 0 ]
if ipv6 :
self . address_family = socket . AF_INET6
2011-05-01 16:45:04 +02:00
# Do not bind and activate, as we might change socket options
2016-03-31 19:57:40 +02:00
super ( ) . __init__ ( address , handler , False )
2011-05-01 15:25:52 +02:00
if ipv6 :
# Only allow IPv6 connections to the IPv6 socket
self . socket . setsockopt ( socket . IPPROTO_IPV6 , socket . IPV6_V6ONLY , 1 )
if bind_and_activate :
self . server_bind ( )
self . server_activate ( )
2016-06-10 14:36:44 +02:00
if self . max_connections :
self . connections_guard = threading . BoundedSemaphore (
self . max_connections )
else :
# use dummy context manager
self . connections_guard = contextlib . suppress ( )
2016-06-10 14:33:25 +02:00
def get_request ( self ) :
# Set timeout for client
_socket , address = super ( ) . get_request ( )
if self . client_timeout :
_socket . settimeout ( self . client_timeout )
return _socket , address
2011-05-01 15:25:52 +02:00
class HTTPSServer ( HTTPServer ) :
""" HTTPS server. """
2011-08-29 16:07:30 +02:00
2016-04-22 11:37:02 +09:00
# These class attributes must be set before creating instance
certificate = None
key = None
protocol = None
2016-08-29 12:07:30 +02:00
ciphers = None
2016-03-31 19:57:40 +02:00
2016-04-22 11:37:02 +09:00
def __init__ ( self , address , handler ) :
""" Create server by wrapping HTTP socket in an SSL socket. """
super ( ) . __init__ ( address , handler , bind_and_activate = False )
2014-03-19 14:04:25 +01:00
2016-04-22 11:37:02 +09:00
self . socket = ssl . wrap_socket (
self . socket , self . key , self . certificate , server_side = True ,
2016-08-29 12:07:30 +02:00
ssl_version = self . protocol , ciphers = self . ciphers )
2011-05-01 15:25:52 +02:00
2011-05-01 16:45:04 +02:00
self . server_bind ( )
self . server_activate ( )
2011-05-01 15:25:52 +02:00
2016-05-21 00:52:22 +02:00
class ThreadedHTTPServer ( socketserver . ThreadingMixIn , HTTPServer ) :
2016-06-10 14:36:44 +02:00
def process_request_thread ( self , request , client_address ) :
with self . connections_guard :
return super ( ) . process_request_thread ( request , client_address )
2016-05-21 00:52:22 +02:00
class ThreadedHTTPSServer ( socketserver . ThreadingMixIn , HTTPSServer ) :
2016-06-10 14:36:44 +02:00
def process_request_thread ( self , request , client_address ) :
with self . connections_guard :
return super ( ) . process_request_thread ( request , client_address )
2016-05-21 00:52:22 +02:00
2011-05-07 12:18:32 +02:00
class RequestHandler ( wsgiref . simple_server . WSGIRequestHandler ) :
""" HTTP requests handler. """
2016-08-25 04:33:14 +02:00
# These class attributes must be set before creating instance
logger = None
def __init__ ( self , * args , * * kwargs ) :
# Store exception for logging
self . error_stream = io . StringIO ( )
super ( ) . __init__ ( * args , * * kwargs )
def get_stderr ( self ) :
return self . error_stream
2011-05-07 12:18:32 +02:00
def log_message ( self , * args , * * kwargs ) :
""" Disable inner logging management. """
2016-08-25 05:30:46 +02:00
def get_environ ( self ) :
env = super ( ) . get_environ ( )
# Parent class only tries latin1 encoding
2016-09-04 20:17:58 +02:00
env [ " PATH_INFO " ] = unquote ( self . path . split ( " ? " , 1 ) [ 0 ] )
2016-08-25 05:30:46 +02:00
return env
2016-08-25 04:33:14 +02:00
def handle ( self ) :
super ( ) . handle ( )
# Log exception
error = self . error_stream . getvalue ( ) . strip ( " \n " )
if error :
2016-08-25 11:52:12 +02:00
self . logger . error (
" An exception occurred during request: \n %s " % error )
2016-08-25 04:33:14 +02:00
2012-03-13 09:35:01 +01:00
2016-04-22 11:37:02 +09:00
class Application :
2011-12-31 13:31:22 +01:00
""" WSGI application managing collections. """
2016-08-05 02:14:49 +02:00
2016-04-22 11:37:02 +09:00
def __init__ ( self , configuration , logger ) :
2011-05-01 14:46:29 +02:00
""" Initialize application. """
2016-03-31 19:57:40 +02:00
super ( ) . __init__ ( )
2016-04-22 11:37:02 +09:00
self . configuration = configuration
self . logger = logger
2016-08-30 23:13:33 +02:00
self . Auth = auth . load ( configuration , logger )
2016-04-22 11:37:02 +09:00
self . Collection = storage . load ( configuration , logger )
self . authorized = rights . load ( configuration , logger )
self . encoding = configuration . get ( " encoding " , " request " )
2016-06-11 12:53:58 +02:00
def headers_log ( self , environ ) :
""" Sanitize headers for logging. """
2011-05-11 17:09:44 +02:00
request_environ = dict ( environ )
2016-08-05 02:14:49 +02:00
2016-06-11 12:53:58 +02:00
# Remove environment variables
if not self . configuration . getboolean ( " logging " , " full_environment " ) :
for shell_variable in os . environ :
request_environ . pop ( shell_variable , None )
2016-08-05 02:14:49 +02:00
# Mask passwords
mask_passwords = self . configuration . getboolean (
" logging " , " mask_passwords " )
authorization = request_environ . get (
" HTTP_AUTHORIZATION " , " " ) . startswith ( " Basic " )
if mask_passwords and authorization :
2016-06-11 12:53:58 +02:00
request_environ [ " HTTP_AUTHORIZATION " ] = " Basic **masked** "
2016-08-05 02:14:49 +02:00
2011-05-11 17:09:44 +02:00
return request_environ
2008-12-30 16:25:42 +00:00
2011-05-01 14:46:29 +02:00
def decode ( self , text , environ ) :
""" Try to magically decode ``text`` according to given ``environ``. """
2010-01-21 18:52:53 +01:00
# List of charsets to try
charsets = [ ]
# First append content charset given in the request
2011-05-01 14:46:29 +02:00
content_type = environ . get ( " CONTENT_TYPE " )
2010-02-10 18:57:21 +01:00
if content_type and " charset= " in content_type :
2015-04-29 19:07:17 +02:00
charsets . append (
content_type . split ( " charset= " ) [ 1 ] . split ( " ; " ) [ 0 ] . strip ( ) )
2010-01-21 18:52:53 +01:00
# Then append default Radicale charset
2011-05-01 14:46:29 +02:00
charsets . append ( self . encoding )
2010-01-21 18:52:53 +01:00
# Then append various fallbacks
charsets . append ( " utf-8 " )
charsets . append ( " iso8859-1 " )
# Try to decode
for charset in charsets :
try :
return text . decode ( charset )
except UnicodeDecodeError :
pass
raise UnicodeDecodeError
2012-08-15 15:12:18 +02:00
def collect_allowed_items ( self , items , user ) :
2012-09-15 09:08:01 +02:00
""" Get items from request that user is allowed to access. """
2012-08-15 22:36:42 +02:00
read_allowed_items = [ ]
write_allowed_items = [ ]
2012-08-15 15:12:18 +02:00
for item in items :
2016-04-22 11:37:02 +09:00
if isinstance ( item , self . Collection ) :
2016-08-04 06:08:08 +02:00
path = item . path
2012-09-15 09:08:01 +02:00
else :
2016-08-04 06:08:08 +02:00
path = item . collection . path
if self . authorized ( user , path , " r " ) :
self . logger . debug (
2016-08-10 23:42:48 +02:00
" %s has read access to collection %s " ,
user or " Anonymous " , path or " / " )
2016-08-04 06:08:08 +02:00
read_allowed_items . append ( item )
else :
self . logger . debug (
2016-08-10 23:42:48 +02:00
" %s has NO read access to collection %s " ,
user or " Anonymous " , path or " / " )
2016-08-04 06:08:08 +02:00
if self . authorized ( user , path , " w " ) :
self . logger . debug (
2016-08-10 23:42:48 +02:00
" %s has write access to collection %s " ,
user or " Anonymous " , path or " / " )
2016-08-04 06:08:08 +02:00
write_allowed_items . append ( item )
else :
self . logger . debug (
2016-08-10 23:42:48 +02:00
" %s has NO write access to collection %s " ,
user or " Anonymous " , path or " / " )
2012-09-15 09:08:01 +02:00
return read_allowed_items , write_allowed_items
2012-08-15 15:12:18 +02:00
2011-05-01 14:46:29 +02:00
def __call__ ( self , environ , start_response ) :
""" Manage a request. """
2016-08-05 02:14:49 +02:00
2016-09-02 11:05:35 +02:00
def response ( status , headers = ( ) , answer = None ) :
2016-09-02 11:04:29 +02:00
headers = dict ( headers )
2016-09-02 04:10:11 +02:00
# Set content length
if answer :
self . logger . debug ( " Response content: \n %s " , answer )
answer = answer . encode ( self . encoding )
accept_encoding = [
encoding . strip ( ) for encoding in
environ . get ( " HTTP_ACCEPT_ENCODING " , " " ) . split ( " , " )
if encoding . strip ( ) ]
if " gzip " in accept_encoding :
zcomp = zlib . compressobj ( wbits = 16 + zlib . MAX_WBITS )
answer = zcomp . compress ( answer ) + zcomp . flush ( )
headers [ " Content-Encoding " ] = " gzip "
headers [ " Content-Length " ] = str ( len ( answer ) )
2016-10-12 14:50:53 +02:00
headers [ " Content-Type " ] + = " ; charset= %s " % self . encoding
2016-09-02 04:10:11 +02:00
# Add extra headers set in configuration
if self . configuration . has_section ( " headers " ) :
for key in self . configuration . options ( " headers " ) :
headers [ key ] = self . configuration . get ( " headers " , key )
2016-06-10 14:30:58 +02:00
# Start response
2016-09-17 15:35:43 +02:00
time_end = datetime . datetime . now ( )
2016-08-02 14:37:39 +02:00
status = " %i %s " % (
status , client . responses . get ( status , " Unknown " ) )
2016-09-19 20:11:52 +02:00
self . logger . info ( " %s answer status for %s in %s sec: %s " , environ [ " REQUEST_METHOD " ] , environ [ " PATH_INFO " ] + depthinfo , ( time_end - time_begin ) . total_seconds ( ) , status )
2016-06-10 14:30:58 +02:00
start_response ( status , list ( headers . items ( ) ) )
# Return response content
return [ answer ] if answer else [ ]
2016-09-19 19:59:47 +02:00
remote_host = " UNKNOWN "
2016-09-17 15:35:43 +02:00
if " REMOTE_HOST " in environ :
if environ [ " REMOTE_HOST " ] :
remote_host = environ [ " REMOTE_HOST " ]
if " HTTP_X_FORWARDED_FOR " in environ :
if environ [ " HTTP_X_FORWARDED_FOR " ] :
remote_host = environ [ " HTTP_X_FORWARDED_FOR " ]
remote_useragent = " [-no-user-agent-provided-] "
if " HTTP_USER_AGENT " in environ :
if environ [ " HTTP_USER_AGENT " ] :
remote_useragent = environ [ " HTTP_USER_AGENT " ]
2016-09-19 19:59:47 +02:00
depthinfo = " "
2016-09-19 20:04:11 +02:00
if " HTTP_DEPTH " in environ :
if environ [ " HTTP_DEPTH " ] :
depthinfo = " with depth " + environ [ " HTTP_DEPTH " ]
2016-09-17 15:35:43 +02:00
time_begin = datetime . datetime . now ( )
self . logger . info ( " %s request for %s received from %s using \" %s \" " ,
2016-09-19 19:59:47 +02:00
environ [ " REQUEST_METHOD " ] , environ [ " PATH_INFO " ] + depthinfo , remote_host , remote_useragent )
2011-05-11 17:09:44 +02:00
headers = pprint . pformat ( self . headers_log ( environ ) )
2016-08-10 23:42:48 +02:00
self . logger . debug ( " Request headers: \n %s " , headers )
2011-05-01 14:46:29 +02:00
2016-09-04 21:10:58 +02:00
# Let reverse proxies overwrite SCRIPT_NAME
if " HTTP_X_SCRIPT_NAME " in environ :
environ [ " SCRIPT_NAME " ] = environ [ " HTTP_X_SCRIPT_NAME " ]
self . logger . debug ( " Script name overwritten by client: %s " ,
environ [ " SCRIPT_NAME " ] )
2016-09-04 20:15:08 +02:00
# Sanitize base prefix
environ [ " SCRIPT_NAME " ] = storage . sanitize_path (
environ . get ( " SCRIPT_NAME " , " " ) ) . rstrip ( " / " )
self . logger . debug ( " Sanitized script name: %s " , environ [ " SCRIPT_NAME " ] )
base_prefix = environ [ " SCRIPT_NAME " ]
2015-12-24 08:19:12 +01:00
# Sanitize request URI
2016-09-04 20:16:23 +02:00
environ [ " PATH_INFO " ] = storage . sanitize_path ( environ [ " PATH_INFO " ] )
2016-04-22 11:37:02 +09:00
self . logger . debug ( " Sanitized path: %s " , environ [ " PATH_INFO " ] )
2012-08-03 13:10:20 +02:00
path = environ [ " PATH_INFO " ]
2011-05-01 14:46:29 +02:00
# Get function corresponding to method
2015-12-24 05:57:33 +01:00
function = getattr ( self , " do_ %s " % environ [ " REQUEST_METHOD " ] . upper ( ) )
2010-06-27 01:45:49 +02:00
2012-08-09 14:15:20 +02:00
# Ask authentication backend to check rights
authorization = environ . get ( " HTTP_AUTHORIZATION " , None )
2016-05-26 12:21:09 +02:00
if authorization and authorization . startswith ( " Basic " ) :
authorization = authorization [ len ( " Basic " ) : ] . strip ( )
2016-08-30 23:13:33 +02:00
login , password = self . decode ( base64 . b64decode (
2013-09-19 14:40:03 +02:00
authorization . encode ( " ascii " ) ) , environ ) . split ( " : " , 1 )
2016-08-30 23:13:33 +02:00
user = self . Auth . map_login_to_user ( login )
2012-08-09 14:15:20 +02:00
else :
2016-08-30 23:13:33 +02:00
user = self . Auth . map_login_to_user ( environ . get ( " REMOTE_USER " , " " ) )
2016-09-03 10:01:52 +02:00
password = " "
2012-08-09 14:15:20 +02:00
2016-08-05 02:14:49 +02:00
# If "/.well-known" is not available, clients query "/"
2016-08-01 09:31:25 +02:00
if path == " /.well-known " or path . startswith ( " /.well-known/ " ) :
2016-08-31 00:41:08 +02:00
return response ( * NOT_FOUND )
2014-10-20 17:03:23 +02:00
2016-08-01 09:10:23 +02:00
if user and not storage . is_safe_path_component ( user ) :
# Prevent usernames like "user/calendar.ics"
self . logger . info ( " Refused unsafe username: %s " , user )
is_authenticated = False
else :
2016-08-30 23:13:33 +02:00
is_authenticated = self . Auth . is_authenticated ( user , password )
2014-01-15 10:39:28 +01:00
is_valid_user = is_authenticated or not user
2014-01-05 20:54:17 +01:00
2016-08-01 20:51:27 +02:00
# Create principal collection
if user and is_authenticated :
principal_path = " / %s / " % user
2016-08-04 06:08:08 +02:00
if self . authorized ( user , principal_path , " w " ) :
2016-08-25 05:37:22 +02:00
with self . Collection . acquire_lock ( " r " , user ) :
2016-08-02 14:00:42 +02:00
principal = next (
2016-08-05 02:14:49 +02:00
self . Collection . discover ( principal_path , depth = " 1 " ) ,
None )
2016-08-04 06:08:08 +02:00
if not principal :
2016-08-25 05:37:22 +02:00
with self . Collection . acquire_lock ( " w " , user ) :
2016-08-02 14:00:42 +02:00
self . Collection . create_collection ( principal_path )
2016-08-01 20:51:27 +02:00
2016-08-08 07:00:24 +02:00
# Verify content length
2014-10-17 17:45:16 +02:00
content_length = int ( environ . get ( " CONTENT_LENGTH " ) or 0 )
if content_length :
2016-06-10 14:34:52 +02:00
max_content_length = self . configuration . getint (
" server " , " max_content_length " )
if max_content_length and content_length > max_content_length :
self . logger . debug (
" Request body too large: %d " , content_length )
2016-08-31 00:41:08 +02:00
return response ( * REQUEST_ENTITY_TOO_LARGE )
2016-08-08 07:00:24 +02:00
if is_valid_user :
2016-06-10 14:33:25 +02:00
try :
2016-09-04 20:15:08 +02:00
status , headers , answer = function (
environ , base_prefix , path , user )
2016-06-10 14:33:25 +02:00
except socket . timeout :
2016-08-31 00:41:08 +02:00
return response ( * REQUEST_TIMEOUT )
2012-08-08 18:44:25 +02:00
else :
2013-09-13 15:05:02 +02:00
status , headers , answer = NOT_ALLOWED
2016-08-02 17:24:04 +02:00
if ( status , headers , answer ) == NOT_ALLOWED and not (
user and is_authenticated ) :
2012-08-08 18:44:25 +02:00
# Unknown or unauthorized user
2016-04-22 11:37:02 +09:00
self . logger . info ( " %s refused " % ( user or " Anonymous user " ) )
2013-09-12 13:48:49 +02:00
status = client . UNAUTHORIZED
2016-04-22 11:37:02 +09:00
realm = self . configuration . get ( " server " , " realm " )
2016-09-02 14:41:31 +02:00
headers = dict ( headers )
2016-10-12 14:30:18 +02:00
headers . update ( {
2013-09-12 13:48:49 +02:00
" WWW-Authenticate " :
2016-08-31 00:41:08 +02:00
" Basic realm= \" %s \" " % realm } )
2011-05-01 14:46:29 +02:00
2016-06-10 14:30:58 +02:00
return response ( status , headers , answer )
2012-08-08 18:29:09 +02:00
2016-08-04 06:08:08 +02:00
def _access ( self , user , path , permission , item = None ) :
2016-08-05 02:14:49 +02:00
""" Check if ``user`` can access ``path`` or the parent collection.
2016-08-04 06:08:08 +02:00
2016-08-05 02:14:49 +02:00
` ` permission ` ` must either be " r " or " w " .
2016-08-04 06:08:08 +02:00
2016-08-05 02:14:49 +02:00
If ` ` item ` ` is given , only access to that class of item is checked .
2016-08-04 06:08:08 +02:00
"""
path = storage . sanitize_path ( path )
parent_path = storage . sanitize_path (
" / %s / " % posixpath . dirname ( path . strip ( " / " ) ) )
allowed = False
if not item or isinstance ( item , self . Collection ) :
allowed | = self . authorized ( user , path , permission )
if not item or not isinstance ( item , self . Collection ) :
allowed | = self . authorized ( user , parent_path , permission )
return allowed
2016-08-08 07:00:24 +02:00
def _read_content ( self , environ ) :
content_length = int ( environ . get ( " CONTENT_LENGTH " ) or 0 )
if content_length > 0 :
content = self . decode (
environ [ " wsgi.input " ] . read ( content_length ) , environ )
2016-08-10 23:42:48 +02:00
self . logger . debug ( " Request content: \n %s " , content . strip ( ) )
2016-08-08 07:00:24 +02:00
else :
content = None
return content
2016-09-04 20:15:08 +02:00
def do_DELETE ( self , environ , base_prefix , path , user ) :
2011-06-29 11:04:09 +02:00
""" Manage DELETE request. """
2016-08-04 06:08:08 +02:00
if not self . _access ( user , path , " w " ) :
2014-07-28 11:28:12 +02:00
return NOT_ALLOWED
2016-08-25 05:37:22 +02:00
with self . Collection . acquire_lock ( " w " , user ) :
2016-08-05 02:14:49 +02:00
item = next ( self . Collection . discover ( path ) , None )
2016-08-04 06:08:08 +02:00
if not self . _access ( user , path , " w " , item ) :
return NOT_ALLOWED
if not item :
2016-08-31 01:42:43 +02:00
return NOT_FOUND
2016-04-08 14:41:05 +02:00
if_match = environ . get ( " HTTP_IF_MATCH " , " * " )
2016-08-04 06:08:08 +02:00
if if_match not in ( " * " , item . etag ) :
# ETag precondition not verified, do not delete item
2016-08-31 00:41:08 +02:00
return PRECONDITION_FAILED
2016-08-04 06:08:08 +02:00
if isinstance ( item , self . Collection ) :
2016-09-04 20:15:08 +02:00
answer = xmlutils . delete ( base_prefix , path , item )
2016-08-04 06:08:08 +02:00
else :
2016-09-04 20:15:08 +02:00
answer = xmlutils . delete (
base_prefix , path , item . collection , item . href )
2016-10-12 14:50:53 +02:00
return client . OK , { " Content-Type " : " text/xml " } , answer
2011-06-29 11:04:09 +02:00
2016-09-04 20:15:08 +02:00
def do_GET ( self , environ , base_prefix , path , user ) :
2016-04-07 19:25:10 +02:00
""" Manage GET request. """
2011-08-09 14:35:34 +02:00
# Display a "Radicale works!" message if the root URL is requested
2016-08-04 06:08:08 +02:00
if not path . strip ( " / " ) :
2016-10-12 14:50:53 +02:00
return client . OK , { " Content-Type " : " text/plain " } , " Radicale works! "
2016-08-04 06:08:08 +02:00
if not self . _access ( user , path , " r " ) :
2012-08-15 22:36:42 +02:00
return NOT_ALLOWED
2016-08-25 05:37:22 +02:00
with self . Collection . acquire_lock ( " r " , user ) :
2016-08-05 02:14:49 +02:00
item = next ( self . Collection . discover ( path ) , None )
2016-08-04 06:08:08 +02:00
if not self . _access ( user , path , " r " , item ) :
return NOT_ALLOWED
if not item :
2016-08-31 00:41:08 +02:00
return NOT_FOUND
2016-08-04 06:08:08 +02:00
if isinstance ( item , self . Collection ) :
collection = item
2016-10-12 14:50:53 +02:00
if collection . get_meta ( " tag " ) not in (
" VADDRESSBOOK " , " VCALENDAR " ) :
2016-08-31 00:45:14 +02:00
return DIRECTORY_LISTING
2016-07-14 01:39:57 +02:00
else :
2016-08-04 06:08:08 +02:00
collection = item . collection
2016-08-05 02:14:49 +02:00
content_type = xmlutils . MIMETYPES . get (
collection . get_meta ( " tag " ) , " text/plain " )
2016-05-12 18:55:03 +02:00
headers = {
2016-08-04 06:08:08 +02:00
" Content-Type " : content_type ,
2016-05-12 18:55:03 +02:00
" Last-Modified " : collection . last_modified ,
2016-08-04 06:08:08 +02:00
" ETag " : item . etag }
answer = item . serialize ( )
return client . OK , headers , answer
2012-08-09 16:00:31 +02:00
2016-09-04 20:15:08 +02:00
def do_HEAD ( self , environ , base_prefix , path , user ) :
2011-05-01 14:46:29 +02:00
""" Manage HEAD request. """
2016-09-04 20:15:08 +02:00
status , headers , answer = self . do_GET (
environ , base_prefix , path , user )
2011-05-01 14:46:29 +02:00
return status , headers , None
2016-09-04 20:15:08 +02:00
def do_MKCALENDAR ( self , environ , base_prefix , path , user ) :
2011-02-01 17:01:30 +01:00
""" Manage MKCALENDAR request. """
2016-08-04 06:08:08 +02:00
if not self . authorized ( user , path , " w " ) :
2012-08-09 15:39:01 +02:00
return NOT_ALLOWED
2016-08-08 07:00:24 +02:00
content = self . _read_content ( environ )
2016-08-25 05:37:22 +02:00
with self . Collection . acquire_lock ( " w " , user ) :
2016-08-05 02:14:49 +02:00
item = next ( self . Collection . discover ( path ) , None )
2016-08-04 06:08:08 +02:00
if item :
2016-08-31 00:41:08 +02:00
return WEBDAV_PRECONDITION_FAILED
2016-08-04 06:08:08 +02:00
props = xmlutils . props_from_request ( content )
2016-08-04 23:35:01 +02:00
props [ " tag " ] = " VCALENDAR "
2016-08-04 06:08:08 +02:00
# TODO: use this?
# timezone = props.get("C:calendar-timezone")
2016-08-04 23:35:01 +02:00
self . Collection . create_collection ( path , props = props )
2016-08-04 06:08:08 +02:00
return client . CREATED , { } , None
2016-09-04 20:15:08 +02:00
def do_MKCOL ( self , environ , base_prefix , path , user ) :
2011-12-31 13:31:22 +01:00
""" Manage MKCOL request. """
2016-08-04 06:08:08 +02:00
if not self . authorized ( user , path , " w " ) :
2012-08-09 15:39:01 +02:00
return NOT_ALLOWED
2016-08-08 07:00:24 +02:00
content = self . _read_content ( environ )
2016-08-25 05:37:22 +02:00
with self . Collection . acquire_lock ( " w " , user ) :
2016-08-05 02:14:49 +02:00
item = next ( self . Collection . discover ( path ) , None )
2016-08-04 06:08:08 +02:00
if item :
2016-08-31 00:41:08 +02:00
return WEBDAV_PRECONDITION_FAILED
2016-08-04 06:08:08 +02:00
props = xmlutils . props_from_request ( content )
2016-08-05 17:05:32 +02:00
self . Collection . create_collection ( path , props = props )
2016-08-04 06:08:08 +02:00
return client . CREATED , { } , None
2016-09-04 20:15:08 +02:00
def do_MOVE ( self , environ , base_prefix , path , user ) :
2011-06-29 23:57:56 +02:00
""" Manage MOVE request. """
2016-08-04 06:08:08 +02:00
to_url = urlparse ( environ [ " HTTP_DESTINATION " ] )
if to_url . netloc != environ [ " HTTP_HOST " ] :
# Remote destination server, not supported
2016-08-31 00:41:08 +02:00
return REMOTE_DESTINATION
2016-08-05 02:14:49 +02:00
if not self . _access ( user , path , " w " ) :
return NOT_ALLOWED
2016-08-04 06:08:08 +02:00
to_path = storage . sanitize_path ( to_url . path )
2016-08-05 02:14:49 +02:00
if not self . _access ( user , to_path , " w " ) :
2012-08-15 22:36:42 +02:00
return NOT_ALLOWED
2016-08-05 02:14:49 +02:00
2016-08-25 05:37:22 +02:00
with self . Collection . acquire_lock ( " w " , user ) :
2016-08-05 02:14:49 +02:00
item = next ( self . Collection . discover ( path ) , None )
if not self . _access ( user , path , " w " , item ) :
return NOT_ALLOWED
if not self . _access ( user , to_path , " w " , item ) :
2016-08-04 06:08:08 +02:00
return NOT_ALLOWED
if not item :
2016-08-31 01:42:43 +02:00
return NOT_FOUND
2016-08-05 02:14:49 +02:00
if isinstance ( item , self . Collection ) :
2016-08-31 00:41:08 +02:00
return WEBDAV_PRECONDITION_FAILED
2016-08-05 02:14:49 +02:00
to_item = next ( self . Collection . discover ( to_path ) , None )
2016-08-06 04:45:44 +02:00
if ( isinstance ( to_item , self . Collection ) or
to_item and environ . get ( " HTTP_OVERWRITE " , " F " ) != " T " ) :
2016-08-31 00:41:08 +02:00
return WEBDAV_PRECONDITION_FAILED
2016-08-04 06:08:08 +02:00
to_parent_path = storage . sanitize_path (
" / %s / " % posixpath . dirname ( to_path . strip ( " / " ) ) )
2016-08-05 02:14:49 +02:00
to_collection = next (
self . Collection . discover ( to_parent_path ) , None )
2016-08-06 04:45:44 +02:00
if not to_collection :
2016-08-31 00:41:08 +02:00
return WEBDAV_PRECONDITION_FAILED
2016-08-05 02:14:49 +02:00
to_href = posixpath . basename ( to_path . strip ( " / " ) )
2016-08-06 04:47:17 +02:00
self . Collection . move ( item , to_collection , to_href )
2016-08-04 06:08:08 +02:00
return client . CREATED , { } , None
2016-09-04 20:15:08 +02:00
def do_OPTIONS ( self , environ , base_prefix , path , user ) :
2010-01-19 20:31:21 +01:00
""" Manage OPTIONS request. """
2011-05-01 14:46:29 +02:00
headers = {
2016-08-05 02:14:49 +02:00
" Allow " : " , " . join (
name [ 3 : ] for name in dir ( self ) if name . startswith ( " do_ " ) ) ,
" DAV " : DAV_HEADERS }
2011-05-01 14:46:29 +02:00
return client . OK , headers , None
2016-09-04 20:15:08 +02:00
def do_PROPFIND ( self , environ , base_prefix , path , user ) :
2010-01-19 20:31:21 +01:00
""" Manage PROPFIND request. """
2016-08-08 06:59:15 +02:00
if not self . _access ( user , path , " r " ) :
return NOT_ALLOWED
2016-08-08 07:00:24 +02:00
content = self . _read_content ( environ )
2016-08-25 05:37:22 +02:00
with self . Collection . acquire_lock ( " r " , user ) :
2016-08-05 02:14:49 +02:00
items = self . Collection . discover (
path , environ . get ( " HTTP_DEPTH " , " 0 " ) )
2016-08-11 02:10:09 +02:00
# take root item for rights checking
item = next ( items , None )
if not self . _access ( user , path , " r " , item ) :
return NOT_ALLOWED
if not item :
2016-08-31 00:41:08 +02:00
return NOT_FOUND
2016-08-11 02:10:09 +02:00
# put item back
items = itertools . chain ( [ item ] , items )
2016-08-04 06:08:08 +02:00
read_items , write_items = self . collect_allowed_items ( items , user )
2016-08-05 02:14:49 +02:00
headers = { " DAV " : DAV_HEADERS , " Content-Type " : " text/xml " }
2016-08-12 23:34:08 +02:00
status , answer = xmlutils . propfind (
2016-09-04 20:15:08 +02:00
base_prefix , path , content , read_items , write_items , user )
2016-08-12 23:34:08 +02:00
if status == client . FORBIDDEN :
return NOT_ALLOWED
else :
return status , headers , answer
2016-08-04 06:08:08 +02:00
2016-09-04 20:15:08 +02:00
def do_PROPPATCH ( self , environ , base_prefix , path , user ) :
2011-04-28 18:04:34 +02:00
""" Manage PROPPATCH request. """
2016-08-04 06:08:08 +02:00
if not self . authorized ( user , path , " w " ) :
2012-08-09 15:39:01 +02:00
return NOT_ALLOWED
2016-08-08 07:00:24 +02:00
content = self . _read_content ( environ )
2016-08-25 05:37:22 +02:00
with self . Collection . acquire_lock ( " w " , user ) :
2016-08-05 02:14:49 +02:00
item = next ( self . Collection . discover ( path ) , None )
2016-08-04 06:08:08 +02:00
if not isinstance ( item , self . Collection ) :
2016-08-31 00:41:08 +02:00
return WEBDAV_PRECONDITION_FAILED
2016-08-05 02:14:49 +02:00
headers = { " DAV " : DAV_HEADERS , " Content-Type " : " text/xml " }
2016-09-04 20:15:08 +02:00
answer = xmlutils . proppatch ( base_prefix , path , content , item )
2016-08-04 06:08:08 +02:00
return client . MULTI_STATUS , headers , answer
2012-09-15 09:08:01 +02:00
2016-09-04 20:15:08 +02:00
def do_PUT ( self , environ , base_prefix , path , user ) :
2010-01-19 20:31:21 +01:00
""" Manage PUT request. """
2016-08-04 06:08:08 +02:00
if not self . _access ( user , path , " w " ) :
2012-08-15 22:36:42 +02:00
return NOT_ALLOWED
2016-08-08 07:00:24 +02:00
content = self . _read_content ( environ )
2016-08-25 05:37:22 +02:00
with self . Collection . acquire_lock ( " w " , user ) :
2016-08-04 06:08:08 +02:00
parent_path = storage . sanitize_path (
" / %s / " % posixpath . dirname ( path . strip ( " / " ) ) )
2016-08-05 02:14:49 +02:00
item = next ( self . Collection . discover ( path ) , None )
parent_item = next ( self . Collection . discover ( parent_path ) , None )
2016-08-04 06:08:08 +02:00
write_whole_collection = (
isinstance ( item , self . Collection ) or
2016-08-05 02:14:49 +02:00
not parent_item or (
not next ( parent_item . list ( ) , None ) and
parent_item . get_meta ( " tag " ) not in (
" VADDRESSBOOK " , " VCALENDAR " ) ) )
if write_whole_collection :
if not self . authorized ( user , path , " w " ) :
return NOT_ALLOWED
elif not self . authorized ( user , parent_path , " w " ) :
2016-08-04 06:08:08 +02:00
return NOT_ALLOWED
2016-08-05 02:14:49 +02:00
2016-08-04 06:08:08 +02:00
etag = environ . get ( " HTTP_IF_MATCH " , " " )
2016-08-05 02:14:49 +02:00
if not item and etag :
# Etag asked but no item found: item has been removed
2016-08-31 00:41:08 +02:00
return PRECONDITION_FAILED
2016-08-05 02:14:49 +02:00
if item and etag and item . etag != etag :
# Etag asked but item not matching: item has changed
2016-08-31 00:41:08 +02:00
return PRECONDITION_FAILED
2016-08-04 23:35:01 +02:00
2016-08-05 02:14:49 +02:00
match = environ . get ( " HTTP_IF_NONE_MATCH " , " " ) == " * "
if item and match :
# Creation asked but item found: item can't be replaced
2016-08-31 00:41:08 +02:00
return PRECONDITION_FAILED
2012-09-15 09:08:01 +02:00
2016-05-28 22:46:20 +02:00
items = list ( vobject . readComponents ( content or " " ) )
2016-08-05 02:14:49 +02:00
content_type = environ . get ( " CONTENT_TYPE " , " " ) . split ( " ; " ) [ 0 ]
tags = { value : key for key , value in xmlutils . MIMETYPES . items ( ) }
tag = tags . get ( content_type )
2016-08-04 06:08:08 +02:00
if write_whole_collection :
2016-08-05 02:24:52 +02:00
new_item = self . Collection . create_collection (
path , items , { " tag " : tag } )
2016-05-29 01:18:29 +02:00
else :
2016-08-04 06:08:08 +02:00
if tag :
2016-08-04 23:35:01 +02:00
parent_item . set_meta ( { " tag " : tag } )
2016-08-04 06:08:08 +02:00
href = posixpath . basename ( path . strip ( " / " ) )
2016-08-25 06:28:20 +02:00
new_item = parent_item . upload ( href , items [ 0 ] )
2016-08-04 06:08:08 +02:00
headers = { " ETag " : new_item . etag }
return client . CREATED , headers , None
2008-12-30 16:25:42 +00:00
2016-09-04 20:15:08 +02:00
def do_REPORT ( self , environ , base_prefix , path , user ) :
2010-01-19 20:31:21 +01:00
""" Manage REPORT request. """
2016-08-04 06:08:08 +02:00
if not self . _access ( user , path , " w " ) :
2012-08-09 15:39:01 +02:00
return NOT_ALLOWED
2016-08-08 07:00:24 +02:00
content = self . _read_content ( environ )
2016-08-25 05:37:22 +02:00
with self . Collection . acquire_lock ( " r " , user ) :
2016-08-05 02:14:49 +02:00
item = next ( self . Collection . discover ( path ) , None )
2016-08-04 06:08:08 +02:00
if not self . _access ( user , path , " w " , item ) :
return NOT_ALLOWED
if not item :
2016-08-31 00:41:08 +02:00
return NOT_FOUND
2016-08-04 06:08:08 +02:00
if isinstance ( item , self . Collection ) :
collection = item
else :
collection = item . collection
headers = { " Content-Type " : " text/xml " }
2016-09-04 20:15:08 +02:00
answer = xmlutils . report ( base_prefix , path , content , collection )
2016-08-04 06:08:08 +02:00
return client . MULTI_STATUS , headers , answer