Files
Oxicloud/doc/DAV-INTEGRATION.md
T

603 lines
22 KiB
Markdown
Raw Normal View History

2025-04-02 23:14:12 +02:00
# Integración de WebDAV, CalDAV y CardDAV en OxiCloud
Este documento describe el diseño e implementación de los protocolos WebDAV, CalDAV y CardDAV en OxiCloud, extendiendo la plataforma para soportar clientes y dispositivos que utilizan estos estándares.
## Tabla de Contenidos
1. [Introducción](#introducción)
2. [Arquitectura de la Implementación](#arquitectura-de-la-implementación)
3. [WebDAV](#webdav)
4. [CalDAV](#caldav)
5. [CardDAV](#carddav)
6. [Consideraciones de Seguridad](#consideraciones-de-seguridad)
7. [Pruebas y Compatibilidad](#pruebas-y-compatibilidad)
## Introducción
### WebDAV (Web Distributed Authoring and Versioning)
WebDAV es una extensión del protocolo HTTP que permite a los clientes realizar operaciones sobre archivos en un servidor remoto, como crear, modificar, mover y eliminar archivos y directorios.
### CalDAV (Calendaring Extensions to WebDAV)
CalDAV es un protocolo basado en WebDAV que permite a los clientes acceder y gestionar datos de calendario, como eventos y tareas.
### CardDAV (vCard Extensions to WebDAV)
CardDAV es un protocolo que extiende WebDAV para permitir el acceso y gestión de datos de contactos en formato vCard.
## Arquitectura de la Implementación
La implementación de los protocolos DAV se integra en la arquitectura hexagonal existente de OxiCloud:
```
┌────────────────────────────────────────────────────────────────────┐
│ INTERFACES │
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────────────┐ │
│ │ │ │ │ │ │ │
│ │ REST API │ │ WebDAV API │ │ CalDAV/CardDAV API │ │
│ │ │ │ │ │ │ │
│ └───────┬───────┘ └───────┬───────┘ └───────────┬───────────┘ │
│ │ │ │ │
└──────────┼──────────────────┼──────────────────────┼──────────────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────┐
│ APLICACIÓN │
│ │
│ ┌───────────┐ ┌────────────┐ ┌───────────┐ ┌──────────────┐ │
│ │ │ │ │ │ │ │ │ │
│ │FileService│ │FolderService│ │CalService │ │ContactService│ │
│ │ │ │ │ │ │ │ │ │
│ └─────┬─────┘ └──────┬─────┘ └─────┬─────┘ └──────┬───────┘ │
│ │ │ │ │ │
└────────┼───────────────┼──────────────┼───────────────┼─────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌────────────────────────────────────────────────────────────────┐
│ DOMINIO │
│ │
│ ┌─────────┐ ┌──────────┐ ┌────────────┐ ┌───────────────┐ │
│ │ │ │ │ │ │ │ │ │
│ │ File │ │ Folder │ │ Calendar │ │ Contact │ │
│ │ │ │ │ │ │ │ │ │
│ └─────────┘ └──────────┘ └────────────┘ └───────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘
```
### Componentes Principales
1. **Adaptadores DAV**: Convertirán entre las especificaciones DAV y los modelos de OxiCloud
2. **Servicios de Aplicación**: Se extenderán para incluir funcionalidades específicas DAV
3. **Modelos de Dominio**: Se añadirán nuevas entidades para Calendar y Contact
4. **Repositorios**: Implementaciones de almacenamiento para calendarios y contactos
## WebDAV
### Endpoints Requeridos
| Método HTTP | Endpoint | Descripción |
|-------------|----------|-------------|
| OPTIONS | /webdav/{path} | Indica las capacidades WebDAV soportadas |
| PROPFIND | /webdav/{path} | Recupera propiedades de recursos |
| PROPPATCH | /webdav/{path} | Modifica propiedades de recursos |
| MKCOL | /webdav/{path} | Crea colecciones (directorios) |
| GET | /webdav/{path} | Recupera contenido de recursos |
| HEAD | /webdav/{path} | Recupera metadatos de recursos |
| PUT | /webdav/{path} | Crea o actualiza recursos |
| DELETE | /webdav/{path} | Elimina recursos |
| COPY | /webdav/{path} | Copia recursos |
| MOVE | /webdav/{path} | Mueve recursos |
| LOCK | /webdav/{path} | Bloquea recursos |
| UNLOCK | /webdav/{path} | Desbloquea recursos |
### Implementación
1. **Manejador WebDAV**:
```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,
))
}
// Implementar funciones para cada método WebDAV...
```
2. **Adaptador WebDAV**:
```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;
/// Convierte entre objetos de OxiCloud y representaciones WebDAV
pub struct WebDavAdapter;
impl WebDavAdapter {
/// Convierte una propiedad PROPFIND en XML a un objeto de solicitud
pub fn parse_propfind<R: Read>(reader: R) -> Result<PropFindRequest, Error> {
// Implementación...
}
/// Genera respuesta XML para PROPFIND basada en archivos y carpetas
pub fn generate_propfind_response<W: Write>(
writer: W,
files: &[FileDto],
folders: &[FolderDto],
base_url: &str,
) -> Result<(), Error> {
// Implementación...
}
// Otros métodos para manejar diferentes operaciones WebDAV...
}
```
## CalDAV
### Endpoints Requeridos
| Método HTTP | Endpoint | Descripción |
|-------------|----------|-------------|
| PROPFIND | /caldav/{calendar} | Recupera propiedades del calendario |
| REPORT | /caldav/{calendar} | Consulta eventos del calendario |
| MKCALENDAR | /caldav/{calendar} | Crea un nuevo calendario |
| PUT | /caldav/{calendar}/{event}.ics | Crea o actualiza un evento |
| GET | /caldav/{calendar}/{event}.ics | Recupera un evento |
| DELETE | /caldav/{calendar}/{event}.ics | Elimina un evento |
### Implementación
1. **Nuevas Entidades de Dominio**:
```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>, // Regla de recurrencia
ical_data: String, // Datos iCalendar completos
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
```
2. **Repositorios**:
```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. **Servicio CalDAV**:
```rust
// src/application/services/caldav_service.rs
use std::sync::Arc;
use uuid::Uuid;
use chrono::{DateTime, Utc};
use crate::domain::repositories::calendar_repository::CalendarRepository;
use crate::domain::repositories::calendar_event_repository::CalendarEventRepository;
use crate::domain::entities::calendar::Calendar;
use crate::domain::entities::calendar_event::CalendarEvent;
use crate::application::dtos::calendar_dto::{CalendarDto, CalendarEventDto};
use crate::common::errors::{Result, DomainError};
pub struct CalDavService {
calendar_repository: Arc<dyn CalendarRepository>,
event_repository: Arc<dyn CalendarEventRepository>,
}
impl CalDavService {
pub fn new(
calendar_repository: Arc<dyn CalendarRepository>,
event_repository: Arc<dyn CalendarEventRepository>,
) -> Self {
Self {
calendar_repository,
event_repository,
}
}
// Implementar métodos para operaciones CalDAV...
}
```
4. **Manejador CalDAV**:
```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))
}
// Implementar funciones para cada método CalDAV...
```
## CardDAV
### Endpoints Requeridos
| Método HTTP | Endpoint | Descripción |
|-------------|----------|-------------|
| PROPFIND | /carddav/addressbooks/{addressbook} | Recupera propiedades de la libreta de direcciones |
| REPORT | /carddav/addressbooks/{addressbook} | Consulta contactos |
| MKCOL | /carddav/addressbooks/{addressbook} | Crea una nueva libreta de direcciones |
| PUT | /carddav/addressbooks/{addressbook}/{contact}.vcf | Crea o actualiza un contacto |
| GET | /carddav/addressbooks/{addressbook}/{contact}.vcf | Recupera un contacto |
| DELETE | /carddav/addressbooks/{addressbook}/{contact}.vcf | Elimina un contacto |
### Implementación
1. **Nuevas Entidades de Dominio**:
```rust
// src/domain/entities/address_book.rs
use uuid::Uuid;
use chrono::{DateTime, Utc};
#[derive(Debug, Clone)]
pub struct AddressBook {
id: Uuid,
name: String,
owner_id: String,
description: Option<String>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
// src/domain/entities/contact.rs
use uuid::Uuid;
use chrono::{DateTime, Utc};
#[derive(Debug, Clone)]
pub struct Contact {
id: Uuid,
address_book_id: Uuid,
full_name: String,
first_name: Option<String>,
last_name: Option<String>,
email: Option<String>,
phone: Option<String>,
address: Option<String>,
organization: Option<String>,
vcard_data: String, // Datos vCard completos
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
```
2. **Repositorios**:
```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. **Servicio CardDAV**:
```rust
// src/application/services/carddav_service.rs
use std::sync::Arc;
use uuid::Uuid;
use crate::domain::repositories::address_book_repository::AddressBookRepository;
use crate::domain::repositories::contact_repository::ContactRepository;
use crate::domain::entities::address_book::AddressBook;
use crate::domain::entities::contact::Contact;
use crate::application::dtos::address_book_dto::{AddressBookDto, ContactDto};
use crate::common::errors::{Result, DomainError};
pub struct CardDavService {
address_book_repository: Arc<dyn AddressBookRepository>,
contact_repository: Arc<dyn ContactRepository>,
}
impl CardDavService {
pub fn new(
address_book_repository: Arc<dyn AddressBookRepository>,
contact_repository: Arc<dyn ContactRepository>,
) -> Self {
Self {
address_book_repository,
contact_repository,
}
}
// Implementar métodos para operaciones CardDAV...
}
```
4. **Manejador CardDAV**:
```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))
}
// Implementar funciones para cada método CardDAV...
```
## Esquema de Base de Datos
```sql
-- Esquema para CalDAV
CREATE TABLE calendar (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
owner_id VARCHAR(255) NOT NULL,
description TEXT,
color VARCHAR(50),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE calendar_event (
id UUID PRIMARY KEY,
calendar_id UUID NOT NULL REFERENCES calendar(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,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Esquema para CardDAV
CREATE TABLE address_book (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
owner_id VARCHAR(255) NOT NULL,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE contact (
id UUID PRIMARY KEY,
address_book_id UUID NOT NULL REFERENCES address_book(id) ON DELETE CASCADE,
full_name VARCHAR(255) NOT NULL,
first_name VARCHAR(255),
last_name VARCHAR(255),
email VARCHAR(255),
phone VARCHAR(100),
address TEXT,
organization VARCHAR(255),
vcard_uid VARCHAR(255) NOT NULL,
vcard_data TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Índices para búsqueda eficiente
CREATE INDEX idx_calendar_owner ON calendar(owner_id);
CREATE INDEX idx_calendar_event_calendar ON calendar_event(calendar_id);
CREATE INDEX idx_address_book_owner ON address_book(owner_id);
CREATE INDEX idx_contact_address_book ON contact(address_book_id);
CREATE INDEX idx_contact_name ON contact(full_name);
```
## Consideraciones de Seguridad
1. **Autenticación**
- Utilizar la autenticación existente de OxiCloud
- Soportar autenticación HTTP Basic para clientes DAV
- Implementar el esquema de autenticación Digest si es necesario
2. **Autorización**
- Verificar permisos de usuario para acceder a recursos
- Implementar control de acceso basado en propietario y permisos compartidos
- Asegurar que los usuarios solo puedan acceder a sus propios calendarios y libretas de direcciones
3. **Prevención de Ataques**
- Validar y sanitizar todas las entradas XML
- Limitar tamaño máximo de carga útil
- Implementar rate limiting en endpoints DAV
## Pruebas y Compatibilidad
### Clientes a Probar
1. **WebDAV**
- Windows Explorer
- macOS Finder
- Cyberduck
- FileZilla (con extensión WebDAV)
2. **CalDAV**
- Apple Calendar
- Mozilla Thunderbird (Lightning)
- Microsoft Outlook (con complemento CalDAV)
- Google Calendar (mediante sincronización)
3. **CardDAV**
- Apple Contacts
- Mozilla Thunderbird
- Microsoft Outlook (con complemento CardDAV)
- Google Contacts (mediante sincronización)
### Pruebas de Cumplimiento
- Utilizar la suite de pruebas CalDAVTester para verificar la conformidad con el estándar
- Validar cumplimiento de RFC para cada protocolo
- Pruebas de stress para evaluar rendimiento bajo carga
### Depuración
- Implementar logging detallado para operaciones DAV
- Crear herramientas de diagnóstico para depurar solicitudes DAV complejas
- Proporcionar mensajes de error claros para ayudar en la resolución de problemas