carddav: Implement DAV Push

This commit is contained in:
Lennart
2025-01-15 17:14:33 +01:00
parent 618ed3b327
commit 751c2d1ce7
15 changed files with 276 additions and 113 deletions

View File

@@ -1,2 +1,3 @@
pub mod mkcol;
pub mod post;
pub mod report;

View File

@@ -0,0 +1,59 @@
use crate::Error;
use actix_web::http::header;
use actix_web::web::{Data, Path};
use actix_web::{HttpRequest, HttpResponse};
use rustical_dav::push::PushRegister;
use rustical_store::auth::User;
use rustical_store::{AddressbookStore, Subscription, SubscriptionStore};
use rustical_xml::XmlDocument;
use tracing::instrument;
use tracing_actix_web::RootSpan;
#[instrument(parent = root_span.id(), skip(store, subscription_store, root_span, req))]
pub async fn route_post<A: AddressbookStore + ?Sized, S: SubscriptionStore + ?Sized>(
path: Path<(String, String)>,
body: String,
user: User,
store: Data<A>,
subscription_store: Data<S>,
root_span: RootSpan,
req: HttpRequest,
) -> Result<HttpResponse, Error> {
let (principal, addressbook_id) = path.into_inner();
if principal != user.id {
return Err(Error::Unauthorized);
}
let addressbook = store.get_addressbook(&principal, &addressbook_id).await?;
let request = PushRegister::parse_str(&body)?;
let sub_id = uuid::Uuid::new_v4().to_string();
let expires = if let Some(expires) = request.expires {
chrono::DateTime::parse_from_rfc2822(&expires)
.map_err(|err| crate::Error::Other(err.into()))?
} else {
chrono::Utc::now().fixed_offset() + chrono::Duration::weeks(1)
};
let subscription = Subscription {
id: sub_id.to_owned(),
push_resource: request
.subscription
.web_push_subscription
.push_resource
.to_owned(),
topic: addressbook.push_topic,
expiration: expires.naive_local(),
};
subscription_store.upsert_subscription(subscription).await?;
let location = req
.resource_map()
.url_for(&req, "subscription", &[sub_id])
.unwrap();
Ok(HttpResponse::Created()
.append_header((header::LOCATION, location.to_string()))
.append_header((header::EXPIRES, expires.to_rfc2822()))
.finish())
}

View File

@@ -1,4 +1,5 @@
use super::methods::mkcol::route_mkcol;
use super::methods::post::route_post;
use super::methods::report::route_report_addressbook;
use super::prop::{SupportedAddressData, SupportedReportSet};
use crate::address_object::resource::AddressObjectResource;
@@ -10,22 +11,29 @@ use actix_web::web;
use async_trait::async_trait;
use derive_more::derive::{From, Into};
use rustical_dav::privileges::UserPrivilegeSet;
use rustical_dav::push::Transports;
use rustical_dav::resource::{Resource, ResourceService};
use rustical_dav::xml::{Resourcetype, ResourcetypeInner};
use rustical_store::auth::User;
use rustical_store::{Addressbook, AddressbookStore};
use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore};
use rustical_xml::{XmlDeserialize, XmlSerialize};
use std::marker::PhantomData;
use std::str::FromStr;
use std::sync::Arc;
use strum::{EnumDiscriminants, EnumString, IntoStaticStr, VariantNames};
pub struct AddressbookResourceService<AS: AddressbookStore + ?Sized> {
pub struct AddressbookResourceService<AS: AddressbookStore + ?Sized, S: SubscriptionStore + ?Sized>
{
addr_store: Arc<AS>,
__phantom_sub: PhantomData<S>,
}
impl<A: AddressbookStore + ?Sized> AddressbookResourceService<A> {
impl<A: AddressbookStore + ?Sized, S: SubscriptionStore + ?Sized> AddressbookResourceService<A, S> {
pub fn new(addr_store: Arc<A>) -> Self {
Self { addr_store }
Self {
addr_store,
__phantom_sub: PhantomData,
}
}
}
@@ -42,6 +50,13 @@ pub enum AddressbookProp {
#[xml(ns = "rustical_dav::namespace::NS_DAV", skip_deserializing)]
Getcontenttype(&'static str),
// WebDav Push
#[xml(skip_deserializing)]
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
Transports(Transports),
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
Topic(String),
// CardDAV (RFC 6352)
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
AddressbookDescription(Option<String>),
@@ -90,6 +105,8 @@ impl Resource for AddressbookResource {
AddressbookPropName::Getcontenttype => {
AddressbookProp::Getcontenttype("text/vcard;charset=utf-8")
}
AddressbookPropName::Transports => AddressbookProp::Transports(Default::default()),
AddressbookPropName::Topic => AddressbookProp::Topic(self.0.push_topic.to_owned()),
AddressbookPropName::MaxResourceSize => AddressbookProp::MaxResourceSize(10000000),
AddressbookPropName::SupportedReportSet => {
AddressbookProp::SupportedReportSet(SupportedReportSet::default())
@@ -116,6 +133,8 @@ impl Resource for AddressbookResource {
Ok(())
}
AddressbookProp::Getcontenttype(_) => Err(rustical_dav::Error::PropReadOnly),
AddressbookProp::Transports(_) => Err(rustical_dav::Error::PropReadOnly),
AddressbookProp::Topic(_) => Err(rustical_dav::Error::PropReadOnly),
AddressbookProp::MaxResourceSize(_) => Err(rustical_dav::Error::PropReadOnly),
AddressbookProp::SupportedReportSet(_) => Err(rustical_dav::Error::PropReadOnly),
AddressbookProp::SupportedAddressData(_) => Err(rustical_dav::Error::PropReadOnly),
@@ -135,6 +154,8 @@ impl Resource for AddressbookResource {
Ok(())
}
AddressbookPropName::Getcontenttype => Err(rustical_dav::Error::PropReadOnly),
AddressbookPropName::Transports => Err(rustical_dav::Error::PropReadOnly),
AddressbookPropName::Topic => Err(rustical_dav::Error::PropReadOnly),
AddressbookPropName::MaxResourceSize => Err(rustical_dav::Error::PropReadOnly),
AddressbookPropName::SupportedReportSet => Err(rustical_dav::Error::PropReadOnly),
AddressbookPropName::SupportedAddressData => Err(rustical_dav::Error::PropReadOnly),
@@ -153,7 +174,9 @@ impl Resource for AddressbookResource {
}
#[async_trait(?Send)]
impl<AS: AddressbookStore + ?Sized> ResourceService for AddressbookResourceService<AS> {
impl<AS: AddressbookStore + ?Sized, S: SubscriptionStore + ?Sized> ResourceService
for AddressbookResourceService<AS, S>
{
type MemberType = AddressObjectResource;
type PathComponents = (String, String); // principal, addressbook_id
type Resource = AddressbookResource;
@@ -220,5 +243,6 @@ impl<AS: AddressbookStore + ?Sized> ResourceService for AddressbookResourceServi
let report_method = web::method(Method::from_str("REPORT").unwrap());
res.route(mkcol_method.to(route_mkcol::<AS>))
.route(report_method.to(route_report_addressbook::<AS>))
.post(route_post::<AS, S>)
}
}

View File

@@ -16,7 +16,7 @@ use rustical_dav::resource::{NamedRoute, ResourceService};
use rustical_dav::resources::RootResourceService;
use rustical_store::{
auth::{AuthenticationMiddleware, AuthenticationProvider},
AddressbookStore,
AddressbookStore, SubscriptionStore,
};
use std::sync::Arc;
@@ -29,10 +29,15 @@ pub fn configure_well_known(cfg: &mut web::ServiceConfig, carddav_root: String)
cfg.service(web::redirect("/carddav", carddav_root).permanent());
}
pub fn configure_dav<AP: AuthenticationProvider, A: AddressbookStore + ?Sized>(
pub fn configure_dav<
AP: AuthenticationProvider,
A: AddressbookStore + ?Sized,
S: SubscriptionStore + ?Sized,
>(
cfg: &mut web::ServiceConfig,
auth_provider: Arc<AP>,
store: Arc<A>,
subscription_store: Arc<S>,
) {
cfg.service(
web::scope("")
@@ -58,6 +63,7 @@ pub fn configure_dav<AP: AuthenticationProvider, A: AddressbookStore + ?Sized>(
}),
)
.app_data(Data::from(store.clone()))
.app_data(Data::from(subscription_store))
.service(RootResourceService::<PrincipalResource>::default().actix_resource())
.service(
web::scope("/user").service(
@@ -70,7 +76,7 @@ pub fn configure_dav<AP: AuthenticationProvider, A: AddressbookStore + ?Sized>(
.service(
web::scope("/{addressbook}")
.service(
AddressbookResourceService::<A>::new(store.clone())
AddressbookResourceService::<A, S>::new(store.clone())
.actix_resource(),
)
.service(