23 KiB
23 KiB
23 - DAV Integration
WebDAV, CalDAV, and CardDAV extend the platform to support clients and devices that speak these standard protocols. The implementation follows the existing hexagonal architecture -- each protocol gets its own adapter, service, and domain layer.
Table of Contents
- Introduction
- Implementation Architecture
- WebDAV
- CalDAV
- CardDAV
- Security Considerations
- Testing and Compatibility
Introduction
WebDAV (Web Distributed Authoring and Versioning)
An HTTP extension that lets clients create, modify, move, and delete files and directories on a remote server.
CalDAV (Calendaring Extensions to WebDAV)
A WebDAV-based protocol for accessing and managing calendar data (events and tasks).
CardDAV (vCard Extensions to WebDAV)
Extends WebDAV to allow access and management of contact data in vCard format.
Implementation Architecture
DAV protocols plug into the existing hexagonal architecture:
┌────────────────────────────────────────────────────────────────────┐
│ INTERFACES │
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────────────┐ │
│ │ │ │ │ │ │ │
│ │ REST API │ │ WebDAV API │ │ CalDAV/CardDAV API │ │
│ │ │ │ │ │ │ │
│ └───────┬───────┘ └───────┬───────┘ └───────────┬───────────┘ │
│ │ │ │ │
└──────────┼──────────────────┼──────────────────────┼──────────────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────┐
│ APPLICATION │
│ │
│ ┌───────────┐ ┌────────────┐ ┌───────────┐ ┌──────────────┐ │
│ │ │ │ │ │ │ │ │ │
│ │FileService│ │FolderService│ │CalendarSvc│ │ContactService│ │
│ │ │ │ │ │ │ │ │ │
│ └─────┬─────┘ └──────┬─────┘ └─────┬─────┘ └──────┬───────┘ │
│ │ │ │ │ │
└────────┼───────────────┼──────────────┼───────────────┼─────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌────────────────────────────────────────────────────────────────┐
│ DOMAIN │
│ │
│ ┌─────────┐ ┌──────────┐ ┌────────────┐ ┌───────────────┐ │
│ │ │ │ │ │ │ │ │ │
│ │ File │ │ Folder │ │ Calendar │ │ Contact │ │
│ │ │ │ │ │ │ │ │ │
│ └─────────┘ └──────────┘ └────────────┘ └───────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘
Main Components
- DAV Adapters -- convert between DAV specs and internal models
- Application Services -- extended to include DAV-specific functionality
- Domain Models -- new entities for Calendar and Contact
- Repositories -- storage implementations for calendars and contacts
WebDAV
Required Endpoints
| HTTP Method | Endpoint | Description |
|---|---|---|
| OPTIONS | /webdav/{path} | Reports supported WebDAV capabilities |
| PROPFIND | /webdav/{path} | Retrieves resource properties |
| PROPPATCH | /webdav/{path} | Modifies resource properties |
| MKCOL | /webdav/{path} | Creates collections (directories) |
| GET | /webdav/{path} | Retrieves resource content |
| HEAD | /webdav/{path} | Retrieves resource metadata |
| PUT | /webdav/{path} | Creates or updates resources |
| DELETE | /webdav/{path} | Deletes resources |
| COPY | /webdav/{path} | Copies resources |
| MOVE | /webdav/{path} | Moves resources |
| LOCK | /webdav/{path} | Locks resources |
| UNLOCK | /webdav/{path} | Unlocks resources |
Implementation
- WebDAV Handler:
// src/interfaces/api/handlers/webdav_handler.rs
use std::sync::Arc;
use axum::{
Router,
routing::get,
extract::{Path, State, Request, Extension},
http::StatusCode,
response::Response,
};
use http::{Method, header};
use crate::common::di::AppState;
use crate::interfaces::middleware::auth::CurrentUser;
use crate::application::ports::file_ports::{FileRetrievalUseCase, FileUploadUseCase};
use crate::application::ports::folder_ports::FolderUseCase;
use crate::common::errors::AppError;
pub fn webdav_routes() -> Router<Arc<AppState>> {
Router::new()
.route("/webdav/*path", get(handle_get))
.route_with_tsr("/webdav/*path", axum::routing::on(
Method::OPTIONS, handle_options,
Method::PROPFIND, handle_propfind,
Method::PROPPATCH, handle_proppatch,
Method::MKCOL, handle_mkcol,
Method::PUT, handle_put,
Method::DELETE, handle_delete,
Method::COPY, handle_copy,
Method::MOVE, handle_move,
Method::LOCK, handle_lock,
Method::UNLOCK, handle_unlock,
))
}
// Implement functions for each WebDAV method...
- WebDAV Adapter:
// src/application/adapters/webdav_adapter.rs
use xml::reader::{EventReader, XmlEvent};
use xml::writer::{EventWriter, EmitterConfig, XmlEvent as WriteEvent};
use std::io::{Read, Write};
use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::FolderDto;
/// Converts between internal objects and WebDAV representations
pub struct WebDavAdapter;
impl WebDavAdapter {
/// Parses a PROPFIND XML property into a request object
pub fn parse_propfind<R: Read>(reader: R) -> Result<PropFindRequest, Error> {
// Implementation...
}
/// Generates PROPFIND XML response from files and folders
pub fn generate_propfind_response<W: Write>(
writer: W,
files: &[FileDto],
folders: &[FolderDto],
base_url: &str,
) -> Result<(), Error> {
// Implementation...
}
// Other methods for different WebDAV operations...
}
CalDAV
Required Endpoints
| HTTP Method | Endpoint | Description |
|---|---|---|
| PROPFIND | /caldav/{calendar} | Retrieves calendar properties |
| REPORT | /caldav/{calendar} | Queries calendar events |
| MKCALENDAR | /caldav/{calendar} | Creates a new calendar |
| PUT | /caldav/{calendar}/{event}.ics | Creates or updates an event |
| GET | /caldav/{calendar}/{event}.ics | Retrieves an event |
| DELETE | /caldav/{calendar}/{event}.ics | Deletes an event |
Implementation
- Domain Entities:
// src/domain/entities/calendar.rs
use uuid::Uuid;
use chrono::{DateTime, Utc};
#[derive(Debug, Clone)]
pub struct Calendar {
id: Uuid,
name: String,
owner_id: String,
description: Option<String>,
color: Option<String>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
// src/domain/entities/calendar_event.rs
use uuid::Uuid;
use chrono::{DateTime, Utc};
#[derive(Debug, Clone)]
pub struct CalendarEvent {
id: Uuid,
calendar_id: Uuid,
summary: String,
description: Option<String>,
location: Option<String>,
start_time: DateTime<Utc>,
end_time: DateTime<Utc>,
all_day: bool,
rrule: Option<String>, // Recurrence rule
ical_data: String, // Full iCalendar data
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
- Repositories:
// src/domain/repositories/calendar_repository.rs
use async_trait::async_trait;
use uuid::Uuid;
use crate::domain::entities::calendar::Calendar;
use crate::common::errors::Result;
#[async_trait]
pub trait CalendarRepository: Send + Sync {
async fn create_calendar(&self, calendar: Calendar) -> Result<Calendar>;
async fn get_calendar_by_id(&self, id: &Uuid) -> Result<Calendar>;
async fn get_calendars_by_owner(&self, owner_id: &str) -> Result<Vec<Calendar>>;
async fn update_calendar(&self, calendar: Calendar) -> Result<Calendar>;
async fn delete_calendar(&self, id: &Uuid) -> Result<()>;
}
// src/domain/repositories/calendar_event_repository.rs
use async_trait::async_trait;
use uuid::Uuid;
use chrono::{DateTime, Utc};
use crate::domain::entities::calendar_event::CalendarEvent;
use crate::common::errors::Result;
#[async_trait]
pub trait CalendarEventRepository: Send + Sync {
async fn create_event(&self, event: CalendarEvent) -> Result<CalendarEvent>;
async fn get_event_by_id(&self, id: &Uuid) -> Result<CalendarEvent>;
async fn get_events_by_calendar(&self, calendar_id: &Uuid) -> Result<Vec<CalendarEvent>>;
async fn get_events_in_timerange(
&self,
calendar_id: &Uuid,
start: &DateTime<Utc>,
end: &DateTime<Utc>
) -> Result<Vec<CalendarEvent>>;
async fn update_event(&self, event: CalendarEvent) -> Result<CalendarEvent>;
async fn delete_event(&self, id: &Uuid) -> Result<()>;
}
- CalDAV Service:
// src/application/services/calendar_service.rs
use std::sync::Arc;
use crate::application::ports::calendar_ports::CalendarStoragePort;
use crate::application::dtos::calendar_dto::*;
pub struct CalendarService {
storage: Arc<dyn CalendarStoragePort>,
}
impl CalendarService {
pub fn new(storage: Arc<dyn CalendarStoragePort>) -> Self {
Self { storage }
}
// Implements CalendarUseCase for calendar and event operations...
}
- CalDAV Handler:
// src/interfaces/api/handlers/caldav_handler.rs
use std::sync::Arc;
use axum::{
Router,
routing::{get, put, delete},
extract::{Path, State, Request, Extension},
http::StatusCode,
response::Response,
};
use http::{Method, header};
use crate::common::di::AppState;
use crate::interfaces::middleware::auth::CurrentUser;
use crate::application::services::caldav_service::CalDavService;
use crate::common::errors::AppError;
pub fn caldav_routes() -> Router<Arc<AppState>> {
Router::new()
.route("/caldav/", get(get_calendars))
.route("/caldav/:calendar", get(get_calendar))
.route_with_tsr("/caldav/:calendar", axum::routing::on(
Method::PROPFIND, handle_calendar_propfind,
Method::REPORT, handle_calendar_report,
Method::MKCALENDAR, handle_mkcalendar,
))
.route("/caldav/:calendar/:event", get(get_event))
.route("/caldav/:calendar/:event", put(put_event))
.route("/caldav/:calendar/:event", delete(delete_event))
}
// Implement functions for each CalDAV method...
CardDAV
Required Endpoints
| HTTP Method | Endpoint | Description |
|---|---|---|
| PROPFIND | /carddav/addressbooks/{addressbook} | Retrieves address book properties |
| REPORT | /carddav/addressbooks/{addressbook} | Queries contacts |
| MKCOL | /carddav/addressbooks/{addressbook} | Creates a new address book |
| PUT | /carddav/addressbooks/{addressbook}/{contact}.vcf | Creates or updates a contact |
| GET | /carddav/addressbooks/{addressbook}/{contact}.vcf | Retrieves a contact |
| DELETE | /carddav/addressbooks/{addressbook}/{contact}.vcf | Deletes a contact |
Implementation
- Domain Entities:
// Note: AddressBook and Contact are both defined in src/domain/entities/contact.rs
#[derive(Debug, Clone)]
pub struct AddressBook {
id: Uuid,
name: String,
owner_id: String,
description: Option<String>,
color: Option<String>,
is_public: bool,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone)]
pub struct Contact {
id: Uuid,
address_book_id: Uuid,
uid: String,
full_name: Option<String>,
first_name: Option<String>,
last_name: Option<String>,
nickname: Option<String>,
email: Vec<Email>, // Struct with email, type, is_primary
phone: Vec<Phone>, // Struct with number, type, is_primary
address: Vec<Address>, // Struct with street, city, state, postal_code, country, type, is_primary
organization: Option<String>,
title: Option<String>,
notes: Option<String>,
photo_url: Option<String>,
birthday: Option<NaiveDate>,
anniversary: Option<NaiveDate>,
vcard: String, // Full vCard data
etag: String,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
- Repositories:
// src/domain/repositories/address_book_repository.rs
use async_trait::async_trait;
use uuid::Uuid;
use crate::domain::entities::address_book::AddressBook;
use crate::common::errors::Result;
#[async_trait]
pub trait AddressBookRepository: Send + Sync {
async fn create_address_book(&self, address_book: AddressBook) -> Result<AddressBook>;
async fn get_address_book_by_id(&self, id: &Uuid) -> Result<AddressBook>;
async fn get_address_books_by_owner(&self, owner_id: &str) -> Result<Vec<AddressBook>>;
async fn update_address_book(&self, address_book: AddressBook) -> Result<AddressBook>;
async fn delete_address_book(&self, id: &Uuid) -> Result<()>;
}
// src/domain/repositories/contact_repository.rs
use async_trait::async_trait;
use uuid::Uuid;
use crate::domain::entities::contact::Contact;
use crate::common::errors::Result;
#[async_trait]
pub trait ContactRepository: Send + Sync {
async fn create_contact(&self, contact: Contact) -> Result<Contact>;
async fn get_contact_by_id(&self, id: &Uuid) -> Result<Contact>;
async fn get_contacts_by_address_book(&self, address_book_id: &Uuid) -> Result<Vec<Contact>>;
async fn search_contacts(&self, address_book_id: &Uuid, query: &str) -> Result<Vec<Contact>>;
async fn update_contact(&self, contact: Contact) -> Result<Contact>;
async fn delete_contact(&self, id: &Uuid) -> Result<()>;
}
- CardDAV Service:
// src/application/services/contact_service.rs
use std::sync::Arc;
use crate::application::dtos::contact_dto::*;
use crate::application::dtos::address_book_dto::*;
pub struct ContactService {
// Implements AddressBookUseCase and ContactUseCase
// Uses ContactStorageAdapter as infrastructure
}
impl ContactService {
// Implements methods for CardDAV operations...
}
- CardDAV Handler:
// src/interfaces/api/handlers/carddav_handler.rs
use std::sync::Arc;
use axum::{
Router,
routing::{get, put, delete},
extract::{Path, State, Request, Extension},
http::StatusCode,
response::Response,
};
use http::{Method, header};
use crate::common::di::AppState;
use crate::interfaces::middleware::auth::CurrentUser;
use crate::application::services::carddav_service::CardDavService;
use crate::common::errors::AppError;
pub fn carddav_routes() -> Router<Arc<AppState>> {
Router::new()
.route("/carddav/addressbooks/", get(get_address_books))
.route("/carddav/addressbooks/:addressbook", get(get_address_book))
.route_with_tsr("/carddav/addressbooks/:addressbook", axum::routing::on(
Method::PROPFIND, handle_addressbook_propfind,
Method::REPORT, handle_addressbook_report,
Method::MKCOL, handle_mkaddressbook,
))
.route("/carddav/addressbooks/:addressbook/:contact", get(get_contact))
.route("/carddav/addressbooks/:addressbook/:contact", put(put_contact))
.route("/carddav/addressbooks/:addressbook/:contact", delete(delete_contact))
}
// Implement functions for each CardDAV method...
Database Schema
-- CalDAV schema
CREATE SCHEMA IF NOT EXISTS caldav;
CREATE TABLE IF NOT EXISTS caldav.calendars (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
owner_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
description TEXT,
color VARCHAR(50),
is_public BOOLEAN NOT NULL DEFAULT FALSE,
ctag VARCHAR(255),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS caldav.calendar_events (
id UUID PRIMARY KEY,
calendar_id UUID NOT NULL REFERENCES caldav.calendars(id) ON DELETE CASCADE,
summary VARCHAR(255) NOT NULL,
description TEXT,
location TEXT,
start_time TIMESTAMPTZ NOT NULL,
end_time TIMESTAMPTZ NOT NULL,
all_day BOOLEAN NOT NULL DEFAULT FALSE,
rrule TEXT,
ical_uid VARCHAR(255) NOT NULL,
ical_data TEXT NOT NULL,
etag VARCHAR(255),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS caldav.calendar_shares (
id SERIAL PRIMARY KEY,
calendar_id UUID NOT NULL REFERENCES caldav.calendars(id) ON DELETE CASCADE,
user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
access_level VARCHAR(20) NOT NULL DEFAULT 'read',
UNIQUE(calendar_id, user_id)
);
CREATE TABLE IF NOT EXISTS caldav.calendar_properties (
id SERIAL PRIMARY KEY,
calendar_id UUID NOT NULL REFERENCES caldav.calendars(id) ON DELETE CASCADE,
property_name VARCHAR(255) NOT NULL,
property_value TEXT,
UNIQUE(calendar_id, property_name)
);
-- CardDAV schema
CREATE SCHEMA IF NOT EXISTS carddav;
CREATE TABLE IF NOT EXISTS carddav.address_books (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
owner_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
description TEXT,
color VARCHAR(50),
is_public BOOLEAN NOT NULL DEFAULT FALSE,
ctag VARCHAR(255),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(owner_id, name)
);
CREATE TABLE IF NOT EXISTS carddav.contacts (
id UUID PRIMARY KEY,
address_book_id UUID NOT NULL REFERENCES carddav.address_books(id) ON DELETE CASCADE,
uid VARCHAR(255) NOT NULL,
full_name VARCHAR(255),
first_name VARCHAR(255),
last_name VARCHAR(255),
nickname VARCHAR(255),
organization VARCHAR(255),
title VARCHAR(255),
notes TEXT,
photo_url TEXT,
birthday DATE,
anniversary DATE,
email JSONB,
phone JSONB,
address JSONB,
vcard TEXT NOT NULL,
etag VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(address_book_id, uid)
);
CREATE TABLE IF NOT EXISTS carddav.address_book_shares (
id SERIAL PRIMARY KEY,
address_book_id UUID NOT NULL REFERENCES carddav.address_books(id) ON DELETE CASCADE,
user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
can_write BOOLEAN NOT NULL DEFAULT FALSE,
UNIQUE(address_book_id, user_id)
);
CREATE TABLE IF NOT EXISTS carddav.contact_groups (
id UUID PRIMARY KEY,
address_book_id UUID NOT NULL REFERENCES carddav.address_books(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS carddav.group_memberships (
id SERIAL PRIMARY KEY,
group_id UUID NOT NULL REFERENCES carddav.contact_groups(id) ON DELETE CASCADE,
contact_id UUID NOT NULL REFERENCES carddav.contacts(id) ON DELETE CASCADE,
UNIQUE(group_id, contact_id)
);
-- Indexes for efficient lookup
CREATE INDEX IF NOT EXISTS idx_calendars_owner ON caldav.calendars(owner_id);
CREATE INDEX IF NOT EXISTS idx_calendar_events_calendar ON caldav.calendar_events(calendar_id);
CREATE INDEX IF NOT EXISTS idx_address_books_owner ON carddav.address_books(owner_id);
CREATE INDEX IF NOT EXISTS idx_contacts_address_book ON carddav.contacts(address_book_id);
CREATE INDEX IF NOT EXISTS idx_contacts_full_name ON carddav.contacts(full_name);
Security Considerations
-
Authentication
- Uses existing authentication
- Supports HTTP Basic Authentication for DAV clients
- Digest authentication can be added if needed
-
Authorization
- Verify user permissions before granting resource access
- Owner-based and shared-permission access control
- Users can only access their own calendars and address books
-
Attack Prevention
- Validate and sanitize all XML input
- Limit maximum payload size
- Rate limiting on DAV endpoints
Testing and Compatibility
Clients to Test
-
WebDAV
- Windows Explorer
- macOS Finder
- Cyberduck
- FileZilla (with WebDAV extension)
-
CalDAV
- Apple Calendar
- Mozilla Thunderbird (Lightning)
- Microsoft Outlook (with CalDAV add-in)
- Google Calendar (via sync)
-
CardDAV
- Apple Contacts
- Mozilla Thunderbird
- Microsoft Outlook (with CardDAV add-in)
- Google Contacts (via sync)
Compliance Testing
- Use the CalDAVTester suite to verify standards conformance
- Validate RFC compliance for each protocol
- Stress tests to evaluate performance under load
Debugging
- Detailed logging for DAV operations
- Diagnostic tools for debugging complex DAV requests
- Clear error messages for troubleshooting