Files
Oxicloud/doc/DAV-INTEGRATION.md
T
2025-04-02 23:14:12 +02:00

22 KiB

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
  2. Arquitectura de la Implementación
  3. WebDAV
  4. CalDAV
  5. CardDAV
  6. Consideraciones de Seguridad
  7. 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:
// 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...
  1. Adaptador WebDAV:
// 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:
// 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>,
}
  1. Repositorios:
// 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<()>;
}
  1. Servicio CalDAV:
// 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...
}
  1. Manejador CalDAV:
// 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:
// 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>,
}
  1. Repositorios:
// 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<()>;
}
  1. Servicio CardDAV:
// 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...
}
  1. Manejador CardDAV:
// 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

-- 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