Files
Oxicloud/doc/dav-integration.md
T
2026-04-01 23:14:42 +02:00

638 lines
23 KiB
Markdown

# 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
1. [Introduction](#introduction)
2. [Implementation Architecture](#implementation-architecture)
3. [WebDAV](#webdav)
4. [CalDAV](#caldav)
5. [CardDAV](#carddav)
6. [Security Considerations](#security-considerations)
7. [Testing and Compatibility](#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
1. **DAV Adapters** -- convert between DAV specs and internal models
2. **Application Services** -- extended to include DAV-specific functionality
3. **Domain Models** -- new entities for **Calendar** and **Contact**
4. **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
1. **WebDAV Handler**:
```rust
// 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...
```
2. **WebDAV Adapter**:
```rust
// 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
1. **Domain Entities**:
```rust
// 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>,
}
```
2. **Repositories**:
```rust
// 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<()>;
}
```
3. **CalDAV Service**:
```rust
// 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...
}
```
4. **CalDAV Handler**:
```rust
// 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
1. **Domain Entities**:
```rust
// 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>,
}
```
2. **Repositories**:
```rust
// 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<()>;
}
```
3. **CardDAV Service**:
```rust
// 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...
}
```
4. **CardDAV Handler**:
```rust
// 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
```sql
-- 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
1. **Authentication**
- Uses existing authentication
- Supports HTTP Basic Authentication for DAV clients
- Digest authentication can be added if needed
2. **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
3. **Attack Prevention**
- Validate and sanitize all XML input
- Limit maximum payload size
- Rate limiting on DAV endpoints
## Testing and Compatibility
### Clients to Test
1. **WebDAV**
- Windows Explorer
- macOS Finder
- Cyberduck
- FileZilla (with WebDAV extension)
2. **CalDAV**
- Apple Calendar
- Mozilla Thunderbird (Lightning)
- Microsoft Outlook (with CalDAV add-in)
- Google Calendar (via sync)
3. **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