🏗️(caldav) migrate from davical to sabre/dav

This commit is contained in:
Sylvain Zimmer
2026-01-11 02:28:04 +01:00
parent a36348ead1
commit bc801d3007
35 changed files with 1059 additions and 549 deletions

View File

@@ -1,4 +1,4 @@
"""CalDAV proxy views for forwarding requests to DAViCal."""
"""CalDAV proxy views for forwarding requests to CalDAV server."""
import logging
@@ -10,15 +10,13 @@ from django.views.decorators.csrf import csrf_exempt
import requests
from core.services.caldav_service import DAViCalClient
logger = logging.getLogger(__name__)
@method_decorator(csrf_exempt, name="dispatch")
class CalDAVProxyView(View):
"""
Proxy view that forwards all CalDAV requests to DAViCal.
Proxy view that forwards all CalDAV requests to CalDAV server.
Handles authentication and adds appropriate headers.
CSRF protection is disabled because CalDAV uses non-standard HTTP methods
@@ -27,7 +25,7 @@ class CalDAVProxyView(View):
"""
def dispatch(self, request, *args, **kwargs):
"""Forward all HTTP methods to DAViCal."""
"""Forward all HTTP methods to CalDAV server."""
# Handle CORS preflight requests
if request.method == "OPTIONS":
response = HttpResponse(status=200)
@@ -42,83 +40,40 @@ class CalDAVProxyView(View):
if not request.user.is_authenticated:
return HttpResponse(status=401)
# Ensure user exists in DAViCal before making requests
try:
davical_client = DAViCalClient()
davical_client.ensure_user_exists(request.user)
except Exception as e:
logger.warning("Failed to ensure user exists in DAViCal: %s", str(e))
# Continue anyway - user might already exist
# Build the DAViCal URL
davical_url = getattr(settings, "DAVICAL_URL", "http://davical:80")
# Build the CalDAV server URL
caldav_url = settings.CALDAV_URL
path = kwargs.get("path", "")
# Use user email as the principal (DAViCal uses email as username)
# Use user email as the principal (CalDAV server uses email as username)
user_principal = request.user.email
# Handle root CalDAV requests - return principal collection
if not path or path == user_principal:
# For PROPFIND on root, return the user's principal collection
if request.method == "PROPFIND":
# Get the request path to match the href in response
request_path = request.path
if not request_path.endswith("/"):
request_path += "/"
# Build target URL - CalDAV server uses base URI /api/v1.0/caldav/
# The proxy receives requests at /api/v1.0/caldav/... and forwards them
# to the CalDAV server at the same path (sabre/dav expects requests at its base URI)
base_uri_path = "/api/v1.0/caldav"
clean_path = path.lstrip("/") if path else ""
# Return multistatus with href matching request URL and calendar-home-set
multistatus = f"""<?xml version="1.0" encoding="utf-8"?>
<D:multistatus xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:response>
<D:href>{request_path}</D:href>
<D:propstat>
<D:prop>
<D:displayname>{user_principal}</D:displayname>
<C:calendar-home-set>
<D:href>/api/v1.0/caldav/{user_principal}/</D:href>
</C:calendar-home-set>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>"""
response = HttpResponse(
content=multistatus,
status=207,
content_type="application/xml; charset=utf-8",
)
return response
# For other methods, redirect to principal URL
target_url = f"{davical_url}/caldav.php/{user_principal}/"
# Construct target URL - always include the base URI path
if clean_path:
target_url = f"{caldav_url}{base_uri_path}/{clean_path}"
else:
# Build target URL with path
# Remove leading slash if present
clean_path = path.lstrip("/")
if clean_path.startswith(user_principal):
# Path already includes principal
target_url = f"{davical_url}/caldav.php/{clean_path}"
else:
# Path is relative to principal
target_url = f"{davical_url}/caldav.php/{user_principal}/{clean_path}"
# Root request - use base URI path
target_url = f"{caldav_url}{base_uri_path}/"
# Prepare headers for DAViCal
# Set headers to tell DAViCal it's behind a proxy so it generates correct URLs
script_name = "/api/v1.0/caldav"
# Prepare headers for CalDAV server
# CalDAV server Apache backend reads REMOTE_USER, which we set via X-Forwarded-User
headers = {
"Content-Type": request.content_type or "application/xml",
"X-Forwarded-User": user_principal,
"X-Forwarded-For": request.META.get("REMOTE_ADDR", ""),
"X-Forwarded-Prefix": script_name,
"X-Forwarded-Host": request.get_host(),
"X-Forwarded-Proto": request.scheme,
"X-Script-Name": script_name, # Tell DAViCal the base path
}
# DAViCal authentication: users with password '*' use external auth
# CalDAV server authentication: Apache backend reads REMOTE_USER
# We send the username via X-Forwarded-User header
# For HTTP Basic Auth, we use the email as username with empty password
# This works with DAViCal's external authentication when trust_x_forwarded is true
# CalDAV server converts X-Forwarded-User to REMOTE_USER
auth = (user_principal, "")
# Copy relevant headers from the original request
@@ -135,11 +90,11 @@ class CalDAVProxyView(View):
body = request.body if request.body else None
try:
# Forward the request to DAViCal
# Forward the request to CalDAV server
# Use HTTP Basic Auth with username (email) and empty password
# DAViCal will authenticate based on X-Forwarded-User header when trust_x_forwarded is true
# CalDAV server will authenticate based on X-Forwarded-User header (converted to REMOTE_USER)
logger.debug(
"Forwarding %s request to DAViCal: %s (user: %s)",
"Forwarding %s request to CalDAV server: %s (user: %s)",
request.method,
target_url,
user_principal,
@@ -157,7 +112,7 @@ class CalDAVProxyView(View):
# Log authentication failures for debugging
if response.status_code == 401:
logger.warning(
"DAViCal returned 401 for user %s at %s. Headers sent: %s",
"CalDAV server returned 401 for user %s at %s. Headers sent: %s",
user_principal,
target_url,
headers,
@@ -170,7 +125,7 @@ class CalDAVProxyView(View):
content_type=response.headers.get("Content-Type", "application/xml"),
)
# Copy relevant headers from DAViCal response
# Copy relevant headers from CalDAV server response
for header in ["ETag", "DAV", "Allow", "Location"]:
if header in response.headers:
django_response[header] = response.headers[header]
@@ -178,7 +133,7 @@ class CalDAVProxyView(View):
return django_response
except requests.exceptions.RequestException as e:
logger.error("DAViCal proxy error: %s", str(e))
logger.error("CalDAV server proxy error: %s", str(e))
return HttpResponse(
content=f"CalDAV server error: {str(e)}",
status=502,